使用C#方法组执行代码

14

我在更新我的UI代码(.NET 4.0应用程序中的C#)时,遇到了一个奇怪的崩溃,因为调用UI的线程错误执行。然而,我已经在主线程上调用了该方法,所以这个崩溃没有任何意义:MainThreadDispatcher.Invoke(new Action(View.Method)) 在View属性上发生 "The calling thread cannot access this object because a different thread owns it." 的错误。

进一步调查后,我找到了原因:我是通过方法组调用的。我曾认为使用方法组或委托/lambda本质上是相同的事情(参见这个问题这个问题)。但实际上,将方法组转换为委托会导致代码执行,检查View的值。这是立即完成的,即在原始(非UI)线程上完成,导致了崩溃。如果我使用Lambda而不是方法组,则检查属性会稍后完成,因此在正确的线程中完成。

这似乎非常有趣。 在C#规范中是否提到了这一点?还是由于需要找到正确的转换而隐含了这一点?

以下是一个测试程序。首先,是直接的方式。其次,是两个步骤,更好地展示了发生的情况。为了增加乐趣,我在创建委托后修改了Item.

namespace ConsoleApplication1 // Add a reference to WindowsBase to a standard ConsoleApplication
{
    using System.Threading;
    using System.Windows.Threading;
    using System;

    static class Program
    {
        static Dispatcher mainDispatcher;
        static void Main()
        {
            mainDispatcher = Dispatcher.CurrentDispatcher;
            mainDispatcher.Thread.Name = "Main thread";
            var childThread = new Thread(() =>
                {
                    Console.WriteLine("--- Method group ---");
                    mainDispatcher.Invoke(new Action(Item.DoSomething));

                    Console.WriteLine("\n--- Lambda ---");
                    mainDispatcher.Invoke(new Action(() => Item.DoSomething()));

                    Console.WriteLine("\n--- Method group (two steps) ---");
                    var action = new Action(Item.DoSomething);
                    Console.WriteLine("Invoking");
                    mainDispatcher.Invoke(action);

                    Console.WriteLine("\n--- Lambda (two steps) ---");
                    action = new Action(() => Item.DoSomething());
                    Console.WriteLine("Invoking");
                    mainDispatcher.Invoke(action);

                    Console.WriteLine("\n--- Method group (modifying Item) ---");
                    action = new Action(Item.DoSomething);
                    item = null;
                    mainDispatcher.Invoke(action);
                    item = new UIItem();

                    Console.WriteLine("\n--- Lambda (modifying Item) ---");
                    action = new Action(() => Item.DoSomething());
                    item = null;
                    Console.WriteLine("Invoking");
                    mainDispatcher.Invoke(action);

                    mainDispatcher.InvokeShutdown();
                });
            childThread.Name = "Child thread";
            childThread.Start();

            Dispatcher.Run();
        }

        static UIItem item = new UIItem();
        static UIItem Item
        {
            get
            {
                // mainDispatcher.VerifyAccess(); // Uncomment for crash.
                Console.WriteLine("UIItem: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name);
                return item;
            }
        }

        private class UIItem
        {
            public void DoSomething()
            {
                Console.WriteLine("DoSomething: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name);
            }
        }
    }
}

简短版:

namespace ConsoleApplication1 // Add a reference to WindowsBase to a standard ConsoleApplication
{
    using System.Threading;
    using System.Windows.Threading;
    using System;

    static class Program
    {
        static Dispatcher mainDispatcher;
        static void Main()
        {
            mainDispatcher = Dispatcher.CurrentDispatcher;
            mainDispatcher.Thread.Name = "Main thread";
            var childThread = new Thread(() =>
                {
                    Console.WriteLine("--- Method group ---");
                    mainDispatcher.Invoke(new Action(Item.DoSomething));

                    Console.WriteLine("\n--- Lambda ---");
                    mainDispatcher.Invoke(new Action(() => Item.DoSomething()));    

                    mainDispatcher.InvokeShutdown();
                });
            childThread.Name = "Child thread";
            childThread.Start();

            Dispatcher.Run();
        }

        static UIItem item = new UIItem();
        static UIItem Item
        {
            get
            {
                mainDispatcher.VerifyAccess();
                Console.WriteLine("UIItem: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name);
                return item;
            }
        }

        private class UIItem
        {
            public void DoSomething()
            {
                Console.WriteLine("DoSomething: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name);
            }
        }
    }
}

2
哪个调用失败了?调用堆栈是什么? - SLaks
删除带有VerifyAccess()的注释行,你会发现所有使用方法组的调用都会失败,因为Item属性在子线程上被访问。 - Daniel Rose
1
一个简短但完整的程序,只要显示出问题所在的调用就会非常有帮助。 - Jon Skeet
我并没有看到什么神秘的地方。Action类构造函数在工作线程上运行。因此,Item属性getter也是如此,除非您在lambda中使用它。 - Hans Passant
@HansPassant:那不是一个常规的构造函数(它读取表达式以获取this);我能理解他为什么感到困惑。 - SLaks
2个回答

6

您正在创建一个闭合委托, 它将this对象存储在委托中(作为传递给方法的隐藏第一个参数)。

因此,当您从方法组创建委托时,会立即访问该对象以存储在委托中。

相比之下,当您创建一个lambda表达式时,只有在调用委托时才会访问拥有委托的对象。
您的lambda表达式创建了一个开放式委托,在委托内部直接访问static属性。

如果它访问的是非静态属性或局部变量,则会创建一个闭包闭合委托,并且它仍然可以工作。


4
属性将被急切地访问并不是方法组成员特有的,这是成员表达式的一般特征。实际上,导致特殊情况的是 lambda:它的主体(因此是属性访问)将被推迟到委托实际执行时再执行。
根据规范:
7.6.4 成员访问
[...] 成员访问要么是 E.I 的形式,要么是 E.I 的形式,其中 E 是一个主表达式。
[...] 如果 E 是一个属性或索引器访问,则获取属性或索引器访问的值(§7.1.1),并将 E 重新分类为值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接