BackgroundWorker如何决定运行RunWorkerCompleted处理程序的线程?

smw*_*dia 3 .net c# backgroundworker

我试图弄清楚BGW如何在其工作完成时决定运行RunWorkerCompleted处理程序的线程.

我的初始测试使用WinForm应用程序:

在UI线程上,我开始bgw1.RunWorkerAsync().然后我试图开始bgw2.RunWorkerAsync()通过bgw1在2个不同的地方:

  • bgw1_DoWork() 方法
  • bgw1_RunWorkerCompleted()方法.

我最初的猜测是BGW应该记住它启动的线程并返回该线程以RunWorkerCompleted在其工作完成时执行事件处理程序.

但测试结果很奇怪:

测试1

如果我开始bgw2.RunWorkerAsync()bgw1_RunWorkerCompleted()中,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
Run Code Online (Sandbox Code Playgroud)

测试2

但是如果我开始bgw2.RunWorkerAsync()进入bgw1_DoWork(),我认为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
Run Code Online (Sandbox Code Playgroud)

那么,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();
}
Run Code Online (Sandbox Code Playgroud)

测试3

然后我尝试使用控制台应用程序.虽然我还开始bgw2.RunWorkerAsync()bgw1_RunWorkerCompleted()就跟测试1,没有的 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
Run Code Online (Sandbox Code Playgroud)

测试代码:

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();
}
Run Code Online (Sandbox Code Playgroud)

添加1

一些参考:

这里:

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

Han*_*ant 6

您发现程序的UI线程有一些特殊之处.当然,它确实做了典型程序中没有其他线程做过的事情.正如您所发现的,不是线程池线程而不是控制台模式应用程序中的主线程.它叫Application.Run().

你喜欢BGW的是它能够在UI线程上运行代码.在特定线程上运行代码听起来像应该是简单的东西.然而,它不是,一个线程总是忙于执行代码,你不能随意中断它正在做的任何事情,并让它运行其他东西.这将是一个可怕的错误来源,你有时会遇到的UI代码中的错误.一个重新入侵的bug,就像线程竞争bug一样难以解决.

必要的是线程协作并明确地表明它处于安全状态并准备执行某些代码.这是一个普遍的问题,也发生在非UI场景中.线程必须解决生产者 - 消费者问题.

该问题的通用解决方案是从线程安全队列中获取数据的循环.该循环的通用名称是"消息循环".在后来的UI框架中,术语"调度程序循环"变得很普遍.该循环由Application.Run()启动.您无法看到队列,它内置于操作系统中.但是您倾向于看到在堆栈跟踪中从队列中检索消息的函数,它是GetMessage().当您解决非UI线程的问题时,您可以根据自己的喜好命名它,您通常会使用ConcurrentQueue<T>该类来实现队列.

值得注意的是为什么UI线程总是要解决这个问题.大块代码的共同点是很难使这些代码成为线程安全的.即使很小的代码块也难以实现线程安全.简单的事情List<T>不是例如,你必须使用lock语句来编写代码以使其安全.这通常很有效,但你没有希望为UI代码正确地做这件事.最大的问题是,有很多代码你看不到,甚至不知道也无法改变注入锁.使其安全的唯一方法是确保您只从正确的线程进行调用.BGW帮助您做什么.

值得注意的是,这对你的编程方式产生了巨大的影响.GUI程序必须将代码放在事件处理程序中(由调度程序循环触发),并确保此类代码执行时间不会太长.在调度程序循环中占用太长的时间,阻止等待消息被调度.你总是可以告诉,UI不再发生绘画而且用户输入没有响应.控制台模式应用程序编程简单得多.控制台不需要调度程序循环,与GUI不同,它非常简单,操作系统会在控制台调用本身周围进行锁定.它总是可以重新绘制,您可以写入控制台缓冲区,另一个进程(conhost.exe)使用它来重新绘制控制台窗口.当然,阻止控制台响应当然仍然很常见,但用户并不期望它能保持响应.Ctrl + C和关闭按钮由OS处理,而不是程序.

长期介绍,以了解这一切,现在直到使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()之外,什么都做不了.