BackgroundWorker如何决定在哪个线程上运行RunWorkerCompleted处理程序? [英] How does BackgroundWorker decide on which thread to run the RunWorkerCompleted handler?

查看:79
本文介绍了BackgroundWorker如何决定在哪个线程上运行RunWorkerCompleted处理程序?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am trying to figure out how BGW decides which thread to run the RunWorkerCompleted handler when its work is done.

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

My initial test uses a WinForm application:

在UI线程上,我启动bgw1.RunWorkerAsync().然后,我尝试在2个不同的地方从bgw2.RunWorkerAsync()bgw1:

On the UI thread, I start bgw1.RunWorkerAsync(). Then I tried to start bgw2.RunWorkerAsync() through bgw1 in 2 different places:

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

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

My initial guess is BGW should remember which thread it is started on and return to that thread to execute the RunWorkerCompleted event handler when its work is done.

但是测试结果很奇怪:

如果我在bgw1_RunWorkerCompleted()中启动bgw2.RunWorkerAsync(),则bgw2_RunWorkerCompleted()总是在UI线程上执行.

If I start the bgw2.RunWorkerAsync() in bgw1_RunWorkerCompleted(), the bgw2_RunWorkerCompleted() is always executed on the UI thread.

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()线程.但实际上不是.

Test 2

But if I start the bgw2.RunWorkerAsync() in bgw1_DoWork(), I think bgw2 should remember the bgw1.DoWork() thread and the bgw2_RunWorkerCompleted() should always return to use bgw1_DoWork() thread. But actually not.

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有很大不同.

Test 3

Then I tried with a console application. Though I still start the bgw2.RunWorkerAsync() in bgw1_RunWorkerCompleted() just like Test 1, neither of bgw1 or bgw2 complete on the Main thread. This is very different from Test 1.

我期望这里的主线程是UI线程的 counterpart . 但似乎UI线程与控制台主线程的处理方式有所不同.

I was expecting the main thread here is the counterpart of the UI thread. But it seems UI thread is treated differently from a console main thread.

-------------
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

一些参考文献:

ADD 1

Some references:

此处:

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

BackgroundWorker is the same thing as a thread pool thread. It adds the ability to run events on the UI thread...

推荐答案

您发现程序的UI线程有一些特殊之处.确实存在,它所做的事情是典型程序中没有其他线程可以做到的.如您所知,在控制台模式应用程序中不是线程池线程,也不是主线程.它会调用Application.Run().

You discovered that there is something special about the UI thread of a program. There certainly is, it does something that no other thread in the typical program ever does. As you found out, not a threadpool thread and not the main thread in a console mode app. It calls Application.Run().

您对BGW的满意之处在于它能够在UI线程上运行代码.在特定于 线程上运行代码听起来应该很简单.但是事实并非如此,线程总是在忙着执行代码,您不能任意中断正在执行的任何操作并使它运行其他操作.那将是可怕的错误的来源,有时您也会在UI代码中遇到这种错误. 重新进入错误,几乎与线程竞赛错误一样难以解决.

What you like about BGW is that it is capable of running code on the UI thread. Running code on a specific thread sounds like something that ought to be simple. It however is not, a thread is always busy executing code, you cannot arbitrarily interrupt whatever it is doing and make it run something else. That would be a source of horrible bugs, the kind of bug you sometimes run into in UI code as well. A re-entrancy bug, about as hard to solve as a threading race bug.

必需的是,线程与线程进行协作并显式发出信号,表明它处于安全状态并准备执行某些代码.这是一个普遍的问题,在非UI方案中也会发生.该线程必须解决生产者-消费者问题.

What is necessary is that the thread co-operates and explicitly signals that it is in a safe state and ready to execute some code. It is a universal problem that also occurs in non-UI scenarios. The thread has to solve the producer-consumer problem.

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

The universal solution to that problem is a loop that takes data from a thread-safe queue. The common name for that loop is the "message loop". In later UI frameworks the term "dispatcher loop" became common. That loop gets started by Application.Run(). You cannot see the queue, it is built into the OS. But you tend to see the function that retrieves a message from the queue in stack traces, it is GetMessage(). When you solve the problem for non-UI threads then you named it whatever you preferred, you'd commonly use the ConcurrentQueue<T> class to implement the queue.

值得注意的是,为什么UI线程总是必须解决该问题.大代码块的共同点是很难使此类代码成为线程安全的.即使是一小段代码也很难实现线程安全.例如,没有什么简单的事情,例如List<T>,您必须使用lock语句添加代码以使其安全.通常效果很好,但是您没有希望为UI代码正确地执行此操作.最大的问题是,您看不到很多代码,甚至都不知道,也无法更改以注入锁.确保其安全的唯一方法是确保仅从正确的线程进行调用. BGW可以帮助您做什么.

It is worth noting why the UI thread always has to solve that problem. Common to large chunks of code is that it is very difficult to make such code thread-safe. Even small chunks of code are hard to make thread-safe. Something simple as List<T> is not for example, you have to pepper your code with the lock statement to make it safe. That generally works out well, but you have no hope of doing this correctly for UI code. Biggest issue is that there is a lot of code you cannot see, don't even know about and can't change to inject a lock. The only way to make it safe is to ensure you only ever make the call from the correct thread. What BGW helps you to do.

值得一提的是,这对您的编程方式产生了巨大的影响. GUI程序必须将代码放入事件处理程序中(由分派器循环触发),并确保此类代码的执行时间不会太长.花费太多时间在分派器循环上,以防止等待消息被分派.您总是可以看出,UI冻结,不再发生绘画,并且用户输入没有响应.控制台模式的应用程序非常简单,很多.控制台不需要调度程序循环,这与GUI不同,它非常简单,并且OS会在控制台调用本身周围放置锁.它始终可以重新绘制,将其写入控制台缓冲区,另一个进程(conhost.exe)使用它来重新绘制控制台窗口.当然,停止控制台响应仍然很普遍,但是用户并不希望它保持响应状态. Ctrl + C和关闭"按钮是由操作系统而不是程序处理的.

Worth noting as well is what an enormous impact this has on the way you program. A GUI program has to put code in event handlers (fired by the dispatcher loop) and make sure that such code does not take too long to execute. Taking too long gums up the dispatcher loop, preventing waiting messages from getting dispatched. You can always tell, the UI freezes with painting no longer occurring and user input having no response. A console mode app is much, much simpler to program. The console does not need a dispatcher loop, unlike a GUI it is very simple and the OS puts locks around the console calls itself. It can always repaint, you write to the console buffer and another process (conhost.exe) uses it to repaint the console window. Still very common to stop the console from being responsive of course, but the user has no expectation that it stays responsive. Ctrl+C and the Close button are handled by the OS, not the program.

为了使这一切有意义,现在进行了详尽的介绍,最后介绍了使BGW正常工作的管道. BGW本身不知道程序中哪个特定线程是受膏UI线程.如您所知,您必须在UI线程上调用RunWorkerAsync()以确保其事件在UI线程上运行.它本身也不知道如何发送使代码在UI线程上运行的消息.它需要来自特定于UI框架的类的帮助. SynchronizationContext.Current属性包含对该类对象的引用,当您调用RunWorkerAsync()时,BGW会复制该对象,以便稍后可以使用它调用其Post()方法来触发该事件.对于Winforms应用程序,该类为WindowsFormsSynchronizationContext,其Send()和Post()方法使用Control.Begin/Invoke().对于WPF应用程序,它是DispatcherSynchronizationContext,它使用Dispatcher.Begin/Invoke.对于工作线程或控制台模式应用程序,该属性为null,然后BGW必须创建自己的SynchronizationContext对象.除了使用Threadpool.QueueUserWorkItem()之外什么都做不到.

Long introduction to make sense of it all, now down to the plumbing that makes BGW work. BGW by itself has no idea which specific thread in the program is the anointed UI thread. As you found out, you must call RunWorkerAsync() on the UI thread to get a guarantee that its events runs on the UI thread. It also has no idea itself how to send the message that gets the code to run on the UI thread. It needs help from a class that is specific to the UI framework. The SynchronizationContext.Current property contains a reference to an object of that class, BGW copies it when you call RunWorkerAsync() so it can use it later to call its Post() method to fire the event. For a Winforms app, that class is WindowsFormsSynchronizationContext, its Send() and Post() methods uses Control.Begin/Invoke(). For a WPF app it is DispatcherSynchronizationContext, it uses Dispatcher.Begin/Invoke. The property is null for a worker thread or a console mode app, BGW then has to create its own SynchronizationContext object. Which can't do anything but use Threadpool.QueueUserWorkItem().

这篇关于BackgroundWorker如何决定在哪个线程上运行RunWorkerCompleted处理程序?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆