원본주소 : 링크를 열려면 클릭하세요
[Abstract]본 글에서는 C#을 소개합니다.
WinForm 다중 스레드 개발을 위한 Control.Invoke 및 자세한 샘플 코드가 참조용으로 제공됩니다.
아래에서는 Windows Form에서 Invoke를 사용할 때 주의해야 할 멀티스레딩 문제에 대해 소개하겠습니다. 소프트웨어.
먼저 멀티스레딩을 사용하면 어떤 작업을 고려해야 할까요? 일반적인 규칙은 사용자와의 상호 작용을 담당하는 스레드(이하 UI 스레드)가 원활하게 유지되어야 한다는 것입니다. UI 스레드에서 호출된 API가 30밀리초 이상의 차단 시간(예: 울트라 액세스)을 유발할 수 있는 경우 CD-ROM, 원격 호출 등과 같은 느린 주변 장치) 멀티스레딩 사용을 고려해야 합니다. 왜 30밀리초인가? 30밀리초라는 개념은 사람의 눈으로 감지할 수 있는 지연으로, 영화의 한 프레임 길이와 거의 동일하며 최대 100밀리초를 초과해서는 안 됩니다.
둘째, 가장 편리하고 간단한 멀티스레딩은 스레드 풀을 이용하는 것입니다. 스레드 풀의 스레드를 통해 코드를 실행하는 가장 쉬운 방법은 비동기 대리자 호출을 사용하는 것입니다. 대리자 호출은 일반적으로 동기식으로 완료되므로 호출할 메서드를 스레드 풀에 대기시켜 처리를 기다리면 프로그램 흐름이 즉시 호출자(여기서는 UI 스레드)로 반환됩니다. , 통화 따라서 막힘이 없습니다.
다음 예제를 보면 스레드 풀을 사용하여 코드를 비동기적으로 실행하는 것이 그리 복잡하지 않다는 것을 알 수 있습니다. 여기서는 System.Windows.Forms.MethodInvoker 대리자를 사용하여 비동기 호출을 수행합니다. MethodInvoker 대리자는 메서드 매개 변수를 허용하지 않습니다. 비동기적으로 실행되는 메서드에 매개 변수를 전달해야 하는 경우 다른 대리자를 사용하거나 직접 정의해야 합니다.
private void StartSomeWorkFromUIThread () { // 我们要做的工作相对UI线程而言台慢了,用下面的方法异步进行处理 MethodInvoker mi = new MethodInvoker(RunsOnWorkerThread);//这是入口方法 mi.BeginInvoke(null, null); // 这样就不会阻塞 } // 缓慢的工作在此方法内进行处理,使用线程池里的线程 private void RunsOnWorkerThread() { DoSomethingSlow(); }
위 방법을 요약하면 UI 스레드의 경우 실제로는 다음과 같습니다. 1. 전화 걸기, 2. 돌아오기 즉시, 특정 작업은 UI 스레드가 차단되지 않도록 프로세스가 무시됩니다. 이 접근 방식은 중요하므로 아래에서 자세히 다루겠습니다. 위의 방법 외에도 스레드 풀을 사용하는 다른 방법이 있습니다. 물론, 만족스러우면 자신만의 스레드를 생성할 수도 있습니다.
第三,在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)!