我试图弄清楚BGW如何在其工作完成后决定运行RunWorkerCompleted处理程序的线程。
我的初始测试使用WinForm应用程序:
在UI线程上,我启动bgw1.RunWorkerAsync()
。然后我尝试在两个不同的地方bgw2.RunWorkerAsync()
开始bgw1
:
bgw1_DoWork()
方法bgw1_RunWorkerCompleted()
方法。我最初的猜测是BGW应该记住它启动的线程并返回该线程以在其工作完成时执行RunWorkerCompleted
事件处理程序。
但测试结果很奇怪:
如果我在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
但如果我在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();
}
然后我尝试使用控制台应用程序。虽然我仍然在bgw2.RunWorkerAsync()
中启动bgw1_RunWorkerCompleted()
,就像测试1一样, <{strong> bgw1
或bgw2
都没有在主线程上完成。这与测试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();
}
一些参考文献:
来自here:
BackgroundWorker与线程池线程相同。它补充道 能够在UI线程上运行事件......
答案 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()。
之外什么也做不了