【问题标题】:How does BackgroundWorker decide on which thread to run the RunWorkerCompleted handler?BackgroundWorker 如何决定运行 RunWorkerCompleted 处理程序的线程?
【发布时间】:2017-09-17 16:47:03
【问题描述】:

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

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

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

  • bgw1_DoWork()方法
  • 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();
}

添加 1

一些参考资料:

来自here

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

【问题讨论】:

  • 您检查过Reference Source 中的BackgroundWorker 吗?
  • 看起来事件通过AsyncOperation.Post,它通过AsyncOperationManager使用应用程序的SynchronizationContext.CurrentSyncronizationContext创建的SynchronizationContext
  • 这最终导致ThreadPool.QueueUserWorkItem,从那里it gets complicated
  • 我从 github 下载了 .net Foundation src 并浏览了代码。看来BGW是基于TPL的,而TPL是基于CLR ThreadPool的……

标签: c# .net backgroundworker


【解决方案1】:

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

您喜欢 BGW 的地方在于它能够在 UI 线程上运行代码。在特定线程上运行代码听起来应该很简单。然而事实并非如此,一个线程总是忙于执行代码,你不能任意中断它正在做的任何事情并让它运行其他东西。这将是可怕错误的来源,您有时也会在 UI 代码中遇到这种错误。 重入错误,与线程竞争错误一样难以解决。

必要的是线程合作并明确表示它处于安全状态并准备执行某些代码。这是一个普遍的问题,在非 UI 场景中也会出现。该线程必须解决producer-consumer problem

该问题的通用解决方案是从线程安全队列中获取数据的循环。该循环的通用名称是“消息循环”。在后来的 UI 框架中,术语“调度程序循环”变得很常见。该循环由 Application.Run() 启动。您看不到队列,它是内置在操作系统中的。但是您往往会在堆栈跟踪中看到从队列中检索消息的函数,它是 GetMessage()。当您解决非 UI 线程的问题时,您可以随意命名,通常使用 ConcurrentQueue&lt;T&gt; 类来实现队列。

值得注意的是为什么 UI 线程总是要解决这个问题。大块代码的共同点是很难使此类代码成为线程安全的。即使是一小段代码也很难保证线程安全。像List&lt;T&gt; 这样简单的事情不是例如,您必须在代码中添加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 会导致不同的结果。
猜你喜欢
  • 1970-01-01
  • 2020-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-21
相关资源
最近更新 更多