How does BackgroundWorker decide on which thread to run the RunWorkerCompleted handler?

≯℡__Kan透↙ 提交于 2020-02-19 07:04:11

问题


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

My initial test uses a WinForm application:

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

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

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.

But the test result is strange:

Test 1

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

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

So, how does BGW decide which thread to run the completed event?

Test code:

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();
}

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.

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

Test code:

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

Some references:

From here:

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


回答1:


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().

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.

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.

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.

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.

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.

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().



来源:https://stackoverflow.com/questions/46209741/how-does-backgroundworker-decide-on-which-thread-to-run-the-runworkercompleted-h

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!