ホームページ >バックエンド開発 >C#.Net チュートリアル >C# WinForm マルチスレッド開発 (3) Control.Invoke

C# WinForm マルチスレッド開発 (3) Control.Invoke

黄舟
黄舟オリジナル
2017-02-20 10:51:302440ブラウズ

元のアドレス: クリックしてリンクを開きます

[概要]この記事では C# を紹介します WinForm マルチスレッド開発用の Control.Invoke と詳細なサンプル コードが参考のために提供されています。

次に、Windows フォーム ソフトウェアで Invoke を使用するときに注意する必要があるマルチスレッドの問題について紹介します。

まず、マルチスレッドを使用してどのような操作を考慮する必要があるでしょうか?一般的なルールは、UI スレッドによって呼び出される API によって 30 ミリ秒を超えるブロック時間が発生する可能性がある場合 (ウルトラスレッドへのアクセスなど)、ユーザーとの対話を担当するスレッド (以下、UI スレッドと呼びます) はスムーズな状態を維持する必要があります。 CD-ROM などの遅い周辺機器、リモート通話の発信など)、マルチスレッドの使用を検討する必要があります。なぜ 30 ミリ秒なのか? 30 ミリ秒の概念は、人間の目で検出できる遅延であり、映画の 1 フレームの継続時間にほぼ相当し、最大でも 100 ミリ秒を超えてはなりません。

2 番目に、最も便利でシンプルなマルチスレッドは、スレッド プールを使用することです。スレッド プール内のスレッドを通じてコードを実行する最も簡単な方法は、非同期デリゲート呼び出しを使用することです。デリゲートの呼び出しは通常、同期的に完了することに注意してください。これにより、呼び出されるメソッドをスレッド プールのキューに入れて処理を待機できるようになり、プログラム フローが呼び出し元 (ここでは 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)!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。