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

时间:2017-09-14 02:51:17

标签: c# .net backgroundworker

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

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

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

  • 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

测试2

但如果我在bgw2.RunWorkerAsync()中启动bgw1_DoWork(),我认为bgw2应该记住bgw1.DoWork()主题,而bgw2_RunWorkerCompleted()应该始终返回使用bgw1_DoWork() 1}}线程。但实际上并非如此。

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

然后我尝试使用控制台应用程序。虽然我仍然在bgw2.RunWorkerAsync()中启动bgw1_RunWorkerCompleted(),就像测试1一样, <{strong> 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

一些参考文献:

来自here

  

BackgroundWorker与线程池线程相同。它补充道   能够在UI线程上运行事件......

1 个答案:

答案 0 :(得分:6)

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

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

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

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

值得注意的是为什么UI线程总是要解决这个问题。大块代码的共同点是很难使这些代码成为线程安全的。即使很小的代码块也难以实现线程安全。例如List<T>之类的简单事件,您必须使用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()。

之外什么也做不了