Home > Article > Backend Development > C# WinForm multi-threaded development (3) Control.Invoke
Original address: Click to open the link
[Abstract]This article introduces C#
Control.Invoke for WinForm multi-thread development, and detailed sample code is provided for reference.
Let’s give you an introduction to the multi-threading issues that need to be paid attention to when using Invoke in Windows Form software.
First of all, what kind of operations need to be considered using multi-threading? The general rule is that the thread responsible for interacting with the user (hereinafter referred to as the UI thread) should remain smooth. When the API called by the UI thread may cause a blocking time of more than 30 milliseconds (such as accessing ultra-slow peripherals such as CD-ROM, making remote calls, etc.) you should consider using multi-threading. Why 30 milliseconds? The concept of 30 milliseconds is a lag that can be detected by the human eye, which is approximately equivalent to the duration of a frame in a movie, and should not exceed 100 milliseconds at most.
Second, the most convenient and simple multi-threading is to use a thread pool. The easiest way to run code through threads in a thread pool is to use asynchronous delegate calls. Note that delegate calls are usually completed synchronously. Please use the BeginInvoke method, so that the method to be called can be queued into the thread pool to wait for processing, and the program flow will immediately return to the caller (here the UI thread), and the call Therefore, there will be no blockage.
Looking at the following example, we find that it is not very complicated to use the thread pool to execute code asynchronously. Here we use the System.Windows.Forms.MethodInvoker delegate to make asynchronous calls. Note that the MethodInvoker delegate does not accept method parameters. If you need to pass parameters to asynchronously executed methods, please use other delegates, or you need to define them yourself.
private void StartSomeWorkFromUIThread () { // 我们要做的工作相对UI线程而言台慢了,用下面的方法异步进行处理 MethodInvoker mi = new MethodInvoker(RunsOnWorkerThread);//这是入口方法 mi.BeginInvoke(null, null); // 这样就不会阻塞 } // 缓慢的工作在此方法内进行处理,使用线程池里的线程 private void RunsOnWorkerThread() { DoSomethingSlow(); }
To summarize the above method, for the UI thread, it is actually: 1. Make a call, 2. Return immediately, and run it specifically The process is ignored so that the UI thread will not be blocked. This approach is important and we’ll cover it in depth below. In addition to the above methods, there are other ways to use the thread pool. Of course, if you are happy, you can also create your own threads.
第三,在Windows Form中使用多线程的,最重要的一条注意事项是,除了创建控件的线程以外,绝对不要在任何其他线程里面调用控件的成员(只有极个别情况例外),也就是说控件属于创建它的线程,不能从其他线程里面访问。这一条适用于所有从System.Windows.Forms.Control派生的控件(因此可以说是几乎所有控件),包括Form控件本身也是。举一反三,我们很容易得出这样的结论,控件的子控件必须由创建控件的线程来创建,比如一个表单上的按钮,比如由创建表单的线程来创建,因此,一个窗口中的所有控件实际上都活在同一个线程之中。在实际编程时,大多数的软件的做法都是让同一线程负责全部的控件,这就是我们所说的UI线程。看下面的例子:
// 这是由UI线程定义的Label控件 private Label lblStatus; // 以下方法不在UI线程上执行 private void RunsOnWorkerThread() { DoSomethingSlow(); lblStatus.Text = "Finished!"; // 这是错的 }
我们要特别提醒大家,很多人刚开始的时候都会使用以上的方法来访问不在同一个线程里的控件(包括笔者本人),而且在1.0版.Net 框架上似乎没有发现问题,但是这根本就是错的,更糟糕的是,程序员在这里不会得到任何错误提示,一开始就上当受骗,之后会莫明其妙地发现其他错误,这就是Windows Form多线程编程的痛苦所在。笔者试过花很多时间来Debug自己写的Splash窗口突然消失的问题,结果还是失败了:笔者在软件的引导过程中,用另外一个线程里创建了一个Splash窗口来显示欢迎信息,然后尝试把主线程里引导的状态直接写入到Splash窗口上的控件中,开始还OK,可是过一会Splash窗口就莫明其妙消失了。
理解了这一点,我们应该留意到,有时候即使没有用System.Threading.Thread来显式创建一个线程,我们也可能因为使用了异步委托的BeginInvoke方法来隐式创建了线程(从线程池里),在这种线程里也同样不能调用UI线程所创建的控件的成员。
第四,由于上述限制,我们可能会感到很不方便,的确,当我们利用一个新创建的线程来执行某些花时间的运算时,怎样知道运算进度如何并通过UI反映给用户呢?解决方法很多!比如熟悉多线程编程的用户很快会想到,我们采用一些低级的同步方法,工作者线程把状态保存到一个同步对象中,让UI线程轮询(Polling)该对象并反馈给用户就可以了。不过,这还是挺麻烦的,实际上不用这样做,Control类(及其派生类)对象有一个Invoke方法很特别,这是少数几个不受线程限制的成员之一。我们前面说到,绝对不要在任何其他线程里面调用非本线程创建的控件的成员时,也说了“只有极个别情况例外”,这个Invoke方法就是极个别情况之一----Invoke方法可以从任何线程里面调用。下面我们来讲解Invoke方法。
Invoke方法的参数很简单,一个委托,一个参数表(可选),而Invoke方法的主要功能就是帮助你在UI线程(即创建控件的线程)上调用委托所指定的方法。Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。注意,使用Invoke方法时,UI线程不能处于阻塞状态。以下MSDN里关于Invoke方法的说明:
“控件上有四种方法可以安全地从任何线程进行调用:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。对于所有其他方法调用,则应使用调用 (invoke) 方法之一封送对控件的线程的调用。 委托可以是 EventHandler 的实例,在此情况下,发送方参数将包含此控件,而事件参数将包含 EventArgs.Empty。委托还可以是 MethodInvoker 的实例或采用 void 参数列表的其他任何委托。调用 EventHandler 或 MethodInvoker 委托比调用其他类型的委托速度更快。”
好了,说完Invoke,顺便说说BeginInvoke,毫无疑问这是Invoke的异步版本(Invoke是同步完成的),不过大家不要和上面的System.Windows.Forms.MethodInvoker委托中的BeginInvoke混淆,两者都是利用不同线程来完成工作,但是控件的BeginInvoke方法总是使用UI线程,而其他的异步委托调用方法则是利用线程池里的线程。相对Invoke而言,使用BeginInvoke稍稍麻烦一点,但还是那句话,异步比同步效果好,尽管复杂些。比如同步方法可能出现这样一种死锁情况:工作者线程通过Invoke同步调用UI线程里的方法时会阻塞,而万一UI线程正在等待工作者线程做某件事时怎么办?因此,能够使用异步方法时应尽量使用异步方法。
下面我们利用所学到的知识来改写上面那个简单的例子:
// 这是由UI线程定义的Label控件 private Label lblStatus; // 以下方法不在UI线程上执行 private void RunsOnWorkerThread() { DoSomethingSlow(); // Do UI update on UI thread object[] pList = { this, System.EventArgs.Empty }; lblStatus.BeginInvoke( new System.EventHandler(UpdateUI), pList); } // 切换回UI线程执行的入口 private void UpdateUI(object o, System.EventArgs e) { //现在没问题了,使用Invoke使得线程总是回到UI线程,所以我们可以放心大胆地调用控件的成员了 lblStatus.Text = "Finished!"; }
第五,关于多线程编程还要考虑线程之间的同步问题、死锁和争用条件,有关这类问题的文章很多,我们就不赘述了
以上就是C# WinForm多线程开发(三) Control.Invoke 的内容,更多相关内容请关注PHP中文网(www.php.cn)!