BackgroundWorker如何决定在哪个线程上运行RunWorkerCompleted处理程序?

3
我在研究BGW是如何决定在完成工作后运行哪个线程的RunWorkerCompleted处理程序。
我的初始测试使用一个WinForm应用程序:
在UI线程上,我启动bgw1.RunWorkerAsync()。然后我尝试在两个不同的地方通过bgw1启动bgw2.RunWorkerAsync():
1. bgw1_DoWork() 方法 2. 或 bgw1_RunWorkerCompleted()方法。
我最初的猜测是BGW应该记住它是在哪个线程上启动的,并在完成工作后返回该线程以执行RunWorkerCompleted事件处理程序。
但测试结果很奇怪:
测试1
如果我在bgw1_RunWorkerCompleted()中启动bgw2.RunWorkerAsync(),则bgw2_RunWorkerCompleted()始终在UI线程上执行。
UI @ thread: 9252
bgw1_DoWork @ thread: 7216
bgw1_RunWorkerCompleted @ thread: 9252 <------ ALWAYS same as UI thread 9252
bgw2_DoWork @ thread: 7216
bgw2_RunWorkerCompleted @ thread: 9252
bgw1_DoWork @ thread: 7216
bgw1_RunWorkerCompleted @ thread: 9252
bgw2_DoWork @ thread: 1976
bgw2_RunWorkerCompleted @ thread: 9252
bgw1_DoWork @ thread: 7216
bgw1_RunWorkerCompleted @ thread: 9252
bgw2_DoWork @ thread: 1976
bgw2_RunWorkerCompleted @ thread: 9252
bgw1_DoWork @ thread: 7216
bgw1_RunWorkerCompleted @ thread: 9252
bgw2_DoWork @ thread: 1976
bgw2_RunWorkerCompleted @ thread: 9252
bgw1_DoWork @ thread: 7216
bgw1_RunWorkerCompleted @ thread: 9252
bgw2_DoWork @ thread: 7216
bgw2_RunWorkerCompleted @ thread: 9252

测试 2

但如果我在bgw1_DoWork()中开始bgw2.RunWorkerAsync(),我认为bgw2应该记住bgw1.DoWork()线程,bgw2_RunWorkerCompleted()应该始终返回使用bgw1_DoWork()线程。但实际上并不是这样。

UI @ thread: 6352
bgw1_DoWork @ thread: 2472
bgw1_RunWorkerCompleted @ thread: 6352
bgw2_DoWork @ thread: 18308
bgw2_RunWorkerCompleted @ thread: 2472
bgw1_DoWork @ thread: 12060             <------- bgw1_DoWork
bgw1_RunWorkerCompleted @ thread: 6352
bgw2_DoWork @ thread: 8740
bgw2_RunWorkerCompleted @ thread: 12060 <------- SOME SAME AS bgw1_DoWork
bgw1_DoWork @ thread: 7028
bgw1_RunWorkerCompleted @ thread: 6352
bgw2_DoWork @ thread: 2640
bgw2_RunWorkerCompleted @ thread: 7028
bgw1_DoWork @ thread: 5572              <------- HERE is 5572
bgw1_RunWorkerCompleted @ thread: 6352
bgw2_DoWork @ thread: 32
bgw2_RunWorkerCompleted @ thread: 2640  <------- HERE is not 5572
bgw1_DoWork @ thread: 10924
bgw1_RunWorkerCompleted @ thread: 6352
bgw2_DoWork @ thread: 12932
bgw2_RunWorkerCompleted @ thread: 10924

那么,BGW 是如何决定运行完成事件的线程的呢?

测试代码:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }


    private BackgroundWorker bgw1;
    private BackgroundWorker bgw2;

    private void Form1_Load(object sender, EventArgs e)
    {
        this.textBox1.Text += "UI @ thread: " + GetCurrentWin32ThreadId() + Environment.NewLine;
        bgw1 = new BackgroundWorker();
        bgw1.DoWork += bgw1_DoWork;
        bgw1.RunWorkerCompleted += bgw1_RunWorkerCompleted;


        bgw2 = new BackgroundWorker();
        bgw2.DoWork += bgw2_DoWork;
        bgw2.RunWorkerCompleted += bgw2_RunWorkerCompleted;
    }

    void bgw1_DoWork(object sender, DoWorkEventArgs e)
    {
        Int32 tid = GetCurrentWin32ThreadId();
        this.textBox1.Invoke(new MethodInvoker(() => { this.textBox1.Text += "bgw1_DoWork @ thread: " + tid + Environment.NewLine; })); //"invoked" on UI thread.
        Thread.Sleep(1000);
        //this.bgw2.RunWorkerAsync(); // <==== START bgw2 HERE
    }

    void bgw2_DoWork(object sender, DoWorkEventArgs e)
    {
        Int32 tid = GetCurrentWin32ThreadId();
        this.textBox1.Invoke(new MethodInvoker(() => { this.textBox1.Text += "bgw2_DoWork @ thread: " + tid + Environment.NewLine; })); //"invoked" on UI thread.
        Thread.Sleep(1000);
    }

    void bgw1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //this will go back to the UI thread, too.
        this.textBox1.Text += "bgw1_RunWorkerCompleted @ thread: " + GetCurrentWin32ThreadId() + Environment.NewLine;
        this.bgw2.RunWorkerAsync(); // <==== OR START bgw2 HERE
    }

    void bgw2_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        this.textBox1.Text += "bgw2_RunWorkerCompleted @ thread: " + GetCurrentWin32ThreadId() + Environment.NewLine;
    }


    private void button1_Click(object sender, EventArgs e)
    {
        this.bgw1.RunWorkerAsync();
    }

    [DllImport("Kernel32", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
    public static extern Int32 GetCurrentWin32ThreadId();
}

测试 3

然后我尝试了一个控制台应用程序。虽然我仍然像测试1一样在bgw1_RunWorkerCompleted()中启动bgw2.RunWorkerAsync(),但是bgw1bgw2都没有在主线程上完成。这与测试1非常不同。

我本以为主线程在这里是UI线程的对应物。但是似乎UI线程和控制台主线程有所不同。

-------------
Main @ thread: 11064
bgw1_DoWork @ thread: 15288
bgw1_RunWorkerCompleted @ thread: 17260
bgw2_DoWork @ thread: 17260
bgw2_RunWorkerCompleted @ thread: 15288
-------------
Main @ thread: 11064
bgw1_DoWork @ thread: 12584
bgw1_RunWorkerCompleted @ thread: 17260
bgw2_DoWork @ thread: 17260
bgw2_RunWorkerCompleted @ thread: 15288
-------------
Main @ thread: 11064
bgw1_DoWork @ thread: 5140
bgw1_RunWorkerCompleted @ thread: 12584
bgw2_DoWork @ thread: 12584
bgw2_RunWorkerCompleted @ thread: 17260
-------------
Main @ thread: 11064
bgw1_DoWork @ thread: 15288
bgw1_RunWorkerCompleted @ thread: 5140
bgw2_DoWork @ thread: 5140
bgw2_RunWorkerCompleted @ thread: 12584
-------------
Main @ thread: 11064
bgw1_DoWork @ thread: 15288
bgw1_RunWorkerCompleted @ thread: 17260
bgw2_DoWork @ thread: 17260
bgw2_RunWorkerCompleted @ thread: 12584

测试代码:

class Program
{
    static void Main(string[] args)
    {
        for (Int32 i = 0; i < 5; i++)
        {
            Console.WriteLine("-------------");
            Console.WriteLine("Main @ thread: " + GetCurrentWin32ThreadId());
            BackgroundWorker bgw1 = new BackgroundWorker();
            bgw1.DoWork += bgw1_DoWork;
            bgw1.RunWorkerCompleted += bgw1_RunWorkerCompleted;
            bgw1.RunWorkerAsync();

            Console.ReadKey();
        }
    }

    static void bgw1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Console.WriteLine("bgw1_RunWorkerCompleted @ thread: " + GetCurrentWin32ThreadId());
        BackgroundWorker bgw2 = new BackgroundWorker();
        bgw2.DoWork += bgw2_DoWork;
        bgw2.RunWorkerCompleted += bgw2_RunWorkerCompleted;
        bgw2.RunWorkerAsync();
    }

    static void bgw1_DoWork(object sender, DoWorkEventArgs e)
    {
        Console.WriteLine("bgw1_DoWork @ thread: " + GetCurrentWin32ThreadId());
        //BackgroundWorker bgw2 = new BackgroundWorker();
        //bgw2.DoWork += bgw2_DoWork;
        //bgw2.RunWorkerCompleted += bgw2_RunWorkerCompleted;
        //bgw2.RunWorkerAsync();
        Thread.Sleep(1000);            

    }


    static void bgw2_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Console.WriteLine("bgw2_RunWorkerCompleted @ thread: " + GetCurrentWin32ThreadId());
    }

    static void bgw2_DoWork(object sender, DoWorkEventArgs e)
    {
        Console.WriteLine("bgw2_DoWork @ thread: " + GetCurrentWin32ThreadId());
        Thread.Sleep(1000);
    }


    [DllImport("Kernel32", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
    public static extern Int32 GetCurrentWin32ThreadId();
}

ADD 1

一些参考资料:

来自这里

BackgroundWorker与线程池线程相同。它添加了在UI线程上运行事件的功能...


1
你是否查看了 BackgroundWorker参考源代码 - Bradley Uffner
看起来事件通过 AsyncOperation.Post 传递,该方法通过由 AsyncOperationManager 创建的 SynchronizationContext 传递,使用应用程序的 SynchronizationContext.Current SyncronizationContext - Bradley Uffner
这最终会导致 ThreadPool.QueueUserWorkItem,从那里开始就变得复杂了。 - Bradley Uffner
我从Github下载了.NET基金会的源代码并浏览了一下。看起来BGW是基于TPL的,而TPL又是基于CLR线程池的... - smwikipedia
1个回答

6
你发现程序中有一个UI线程很特殊。确实如此,它做了典型程序中其他线程所不做的事情。你知道了,既不是线程池线程,也不是控制台模式应用程序中的主线程。它调用了Application.Run()方法。
你喜欢BGW的原因是它能够在UI线程上运行代码。 在特定线程上运行代码听起来应该很简单。但实际上并不简单,因为线程总是忙于执行代码,你不能随意打断它正在做的任何事情,让它运行其他东西。否则会导致可怕的错误,就像在UI代码中经常遇到的那种重入性错误,这种错误和线程竞争问题一样难以解决。
必须的是,线程要协作并明确表示它处于安全状态并准备好执行一些代码。这是一个普遍存在的问题,也发生在非UI情况下。线程必须解决生产者-消费者问题
该问题的通用解决方案是一个从线程安全队列中获取数据的循环。该循环的通用名称是 "消息循环"。在后来的UI框架中, "调度程序循环"这个术语变得很常见。该循环由Application.Run()方法启动。你无法看到队列,因为它内置于操作系统中。但你会在堆栈跟踪中看到从队列中检索消息的函数,它就是GetMessage()函数。当你为非UI线程解决问题时,你可以按自己喜欢的方式命名它,通常会使用ConcurrentQueue类来实现队列。
值得注意的是,为什么UI线程总是必须解决这个问题。大块的代码共性是很难使这样的代码是线程安全的。即使像List这样的小块代码都很难保证线程安全。例如,必须通过lock语句来使其安全。这通常效果很好,但对于UI代码而言,你没有希望正确地实现它。最大的问题是有很多代码你看不到,甚至不知道它的存在,并且不能更改以注入锁。唯一的方法是确保只从正确的线程调用它。这就是BGW帮助你做到的。
值得注意的是,这对编程方式有着巨大的影响。GUI程序必须在事件处理程序中放置代码(由调度程序循环触发),并确保此类代码执行时间不会太长。时间过长会使调度程序循环阻塞,从而防止等待消息被分派。你总能发现,UI会冻结,绘图不再发生,用户输入没有响应。相比之下,控制台模式应用程序要简单得多。控制台不需要调度程序循环,与GUI不同,它非常简单,并且操作系统本身会在控制台调用周围设置锁定。它总是可以重新绘制,你只需写入控制台缓冲区,另一个进程(conhost.exe)将使用它重新绘制控制台窗口。当然,停止控制台响应仍然非常普遍,但用户并没有期望它保持响应。Ctrl+C和关闭按钮由操作系统处理,而不是程序。
为了让BGW工作,需要理解其背后的原理。BGW本身不知道程序中哪个特定线程是指定的UI线程。正如你发现的那样,必须在UI线程上调用RunWorkerAsync()才能保证其事件在UI线程上运行。它本身也不知道如何发送消息以在UI线程上运行代码。它需要来自特定于UI框架的类的帮助。SynchronizationContext.Current属性包含对该类对象的引用,BGW在调用RunWorkerAsync()时复制它,以便稍后使用它来调用其Post()方法触发事件。对于Winforms应用程序,该类是WindowsFormsSynchronizationContext,它的Send()和Post()方法使用Control.Begin/Invoke()。对于WPF应用程序,它是DispatcherSynchronizationContext,它使用Dispatcher.Begin/Invoke。对于工作线程或控制台模式应用程序,该属性为null,BGW然后必须创建自己的SynchronizationContext对象。但它除了使用Threadpool.QueueUserWorkItem()外什么也做不了。

谢谢。所以,BGW只是忠实地遵循SynchronizationContext.Current来决定它完成的线程。但是,在GUI应用程序、控制台应用程序和工作线程中提供的SynchronizationContext.Current不同会导致不同的结果。 - smwikipedia

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