天上 發表於 2020-5-29 11:28:00

谈谈C#多线程开发:并行、并发与异步编程

<h3>阅读导航</h3>
<p>一、使用Task</p>
<p>二、并行编程</p>
<p>三、线程同步</p>
<p>四、异步编程模型</p>
<p>五、多线程数据安全</p>
<p>六、异常处理</p>
<p>&nbsp;</p>
<h3>概述</h3>
<p>现代程序开发过程中不可避免会使用到多线程相关的技术,之所以要使用多线程,主要原因或目的大致有以下几个:</p>
<p>1、 业务特性决定程序就是多任务的,比如,一边采集数据、一边分析数据、同时还要实时显示数据;</p>
<p>2、 在执行一个较长时间的任务时,不能阻塞UI界面响应,必须通过后台线程处理;</p>
<p>3、 在执行批量计算密集型任务时,采用多线程技术可以提高运行效率。</p>
<p>传统使用的多线程技术有:</p>
<ol>
<li>Thread &amp; ThreadPool</li>
<li>Timer</li>
<li>BackgroundWorker</li>
</ol>
<p align="left">目前,这些技术都不再推荐使用了,目前推荐采用基于任务的异步编程模型,包括并行编程和Task的使用。</p>
<p align="left">&nbsp;</p>
<h3>一、使用Task:</h3>
<p>大部分情况下,多线程的应用场景是在后台执行一个较长时间的任务时,不能阻塞界面响应,同时,任务还是可以取消的。</p>
<p>下面我们实现一个简单的示例功能:用户点击Start按钮时启动一个任务,任务执行过程中通过进度条显示任务进度,点击Stop按钮结束任务。</p>
<div class="cnblogs_code"><img id="code_img_closed_d7089f6a-b0af-41c9-8846-607cfe546396" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_d7089f6a-b0af-41c9-8846-607cfe546396" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_d7089f6a-b0af-41c9-8846-607cfe546396" class="cnblogs_code_hide">
<pre>    <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Form1 : Form
    {
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">volatile</span> <span style="color: rgba(0, 0, 255, 1)">bool</span> CancelWork = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;

      </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Form1()
      {
            InitializeComponent();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStart_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStop.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;

            CancelWork </span>= <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> WorkThread());
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStop_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            CancelWork </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> WorkThread()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i &lt; <span style="color: rgba(128, 0, 128, 1)">100</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.progressBar.Value =<span style="color: rgba(0, 0, 0, 1)"> i;
                }));

                Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">1000</span><span style="color: rgba(0, 0, 0, 1)">);

                </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(CancelWork)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
                }
            }

            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStop.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            }));            
      }
    }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>&nbsp;这个代码写的中规中矩,没什么特别的地方,仅仅是用Tsak取代了早期经常采用的Thread、ThreadPool等,虽然Task内部也是对ThreadPool的封装,但仍然建议尽量采用TASK来实现多任务。</p>
<p>&nbsp;注意:虽然可以通过代码强行结束一个任务,但强烈建议不要这样做,应该给它一个通知让其自己结束。</p>
<p>&nbsp;</p>
<h3 align="left"><strong>二、</strong>并行编程:</h3>
<p>目标:通过一个计算素数的方法,循环计算并打印出10000以内的素数。</p>
<p>计算一个数是否素数的方法:</p>
<div class="cnblogs_code"><img id="code_img_closed_b6a40899-2f14-465c-9472-d954db675836" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_b6a40899-2f14-465c-9472-d954db675836" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_b6a40899-2f14-465c-9472-d954db675836" class="cnblogs_code_hide">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">bool</span> IsPrimeNumber(<span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> number)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (number &lt; <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            }

            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (number == <span style="color: rgba(128, 0, 128, 1)">1</span> &amp;&amp; number == <span style="color: rgba(128, 0, 128, 1)">2</span><span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
            }

            </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">2</span>; i &lt; number; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (number % i == <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
                }
            }

            </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>如果不采用并行编程,常规实现方法:</p>
<div class="cnblogs_code">
<pre>            <span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">1</span>; i &lt;= <span style="color: rgba(128, 0, 128, 1)">10000</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">bool</span> b =<span style="color: rgba(0, 0, 0, 1)"> IsPrimeNumber(i);            
                Console.WriteLine($</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">{i}:{b}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            }</span></pre>
</div>
<p>采用并行编程方法:</p>
<div class="cnblogs_code">
<pre>         Parallel.For(<span style="color: rgba(128, 0, 128, 1)">1</span>, <span style="color: rgba(128, 0, 128, 1)">10000</span>, x=&gt;<span style="color: rgba(0, 0, 0, 1)">
         {
                </span><span style="color: rgba(0, 0, 255, 1)">bool</span> b =<span style="color: rgba(0, 0, 0, 1)"> IsPrimeNumber(x);            
                Console.WriteLine($</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">{i}:{b}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            });</span></pre>
</div>
<p>运行程序发现时间差异并不大,主要原因是瓶颈在打印控制台上面,去掉打印代码,只保留计算代码,就可以看出性能差异。</p>
<p>Parallel实际是通过线程池进行任务的分配,线程池的最小线程数和最大线程数将影响到整个程序的性能,需要合理设置。(最小线程默认为8。)</p>
<div class="cnblogs_code">
<pre>            ThreadPool.SetMinThreads(<span style="color: rgba(128, 0, 128, 1)">10</span>, <span style="color: rgba(128, 0, 128, 1)">10</span><span style="color: rgba(0, 0, 0, 1)">);
            ThreadPool.SetMaxThreads(</span><span style="color: rgba(128, 0, 128, 1)">20</span>, <span style="color: rgba(128, 0, 128, 1)">20</span>);</pre>
</div>
<p>&nbsp;按照上述设置,假设线程任务耗时比较长不能很快结束。在启动前面10个线程时速度很快,第10~20个线程就比较慢一点,大约0.5秒,到达20个线程以后,如果前期任务没有结束就不能继续分配任务了。</p>
<p>和Task类似,Parallel类仍然是对ThreadPool的封装,但Parallel有一个优势,它能知道所有任务是否完成,如果采用线程池来实现批量任务,我们需要自己通过计数的方式确定所有子任务是否全部完成。</p>
<p>&nbsp;Parallel类还有一个ForEach方法,使用和For类似,就不重复描述了。</p>
<p>&nbsp;</p>
<p></p>
<h3>三、 线程(或任务)同步</h3>
<p>有时我们需要通知一个任务结束,或一个任务等待某个条件进入下一个状态,这就需要用到任务同步的技术。</p>
<p>一个比较简单的方法就是定义一个变量来表示状态。</p>
<p align="left">&nbsp; &nbsp;&nbsp;&nbsp; private volatile bool CancelWork = false;</p>
<p>后台任务可以轮询该变量进行判断:</p>
<div class="cnblogs_code">
<pre>            <span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i &lt; <span style="color: rgba(128, 0, 128, 1)">100</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(CancelWork)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
                }
            }</span></pre>
</div>
<p>&nbsp;这是我们常用的方法,可以称为线程状态机同步(虽然只有两个状态)。需要注意的是在通过轮询去读取状态时,循环体内至少应该有1ms的Sleep,不然CPU会很高。</p>
<p>线程同步还有一个比较好的办法就是采用ManualResetEvent&nbsp;和AutoResetEvent&nbsp;:</p>
<div class="cnblogs_code"><img id="code_img_closed_783b5fea-dff0-4f2b-8db8-084b9917b442" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_783b5fea-dff0-4f2b-8db8-084b9917b442" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_783b5fea-dff0-4f2b-8db8-084b9917b442" class="cnblogs_code_hide">
<pre>   <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Form1 : Form
    {
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> ManualResetEvent manualResetEvent = <span style="color: rgba(0, 0, 255, 1)">new</span> ManualResetEvent(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">);

      </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Form1()
      {
            InitializeComponent();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStart_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStop.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;

            manualResetEvent.Reset();
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> WorkThread());
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStop_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            manualResetEvent.Set();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> WorkThread()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i &lt; <span style="color: rgba(128, 0, 128, 1)">100</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.progressBar.Value =<span style="color: rgba(0, 0, 0, 1)"> i;
                }));

               </span><span style="color: rgba(0, 0, 255, 1)">if</span>(manualResetEvent.WaitOne(<span style="color: rgba(128, 0, 128, 1)">1000</span><span style="color: rgba(0, 0, 0, 1)">))
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
                }
            }

            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStop.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            }));            
      }
    }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>采用WaitOne来等待比通过Sleep进行延时要更好,因为当执行manualResetEvent.WaitOne(1000)时,如果manualResetEvent没有调用Set,该方法在等待1000ms后返回false,如果期间调用了manualResetEvent的Set方法,该方法会立即返回true,不用等待剩下的时间。</p>
<p>采用这种同步方式优于采用通过内部字段变量进行同步的方式,另外尽量采用ManualResetEvent&nbsp;而不是AutoResetEvent&nbsp;。</p>
<p>&nbsp;</p>
<h3>四、异步编程模型(await、async)</h3>
<p>假设我们要实现一个简单的功能:当点击启动按钮时,运行一个任务,任务结束时要报告是否成功,如果成功就显示绿色图标、如果失败就显示红色图标,1秒后图标颜色恢复为白色;任务运行期间启动按钮要不可用。</p>
<p>&nbsp;我写了相关代码:</p>
<div class="cnblogs_code"><img id="code_img_closed_c64885f5-35e6-40b2-824e-d7c949fab4a2" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_c64885f5-35e6-40b2-824e-d7c949fab4a2" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_c64885f5-35e6-40b2-824e-d7c949fab4a2" class="cnblogs_code_hide">
<pre>    <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Form1 : Form
    {
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStart_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;

            </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(DoSomething())
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Green;
            }
            </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Red;
            }

            Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">1000</span><span style="color: rgba(0, 0, 0, 1)">);
         
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.White;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">bool</span><span style="color: rgba(0, 0, 0, 1)"> DoSomething()
      {
            Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">5000</span><span style="color: rgba(0, 0, 0, 1)">);
            </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }
    }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>&nbsp;这段代码逻辑清晰、条理清楚,一看就能明白,但存在两个问题:</p>
<p>1、运行期间UI线程阻塞了,用户界面没有响应;</p>
<p>2、根本不能实现需求,点击启动后,程序卡死6秒种,也没有看到颜色变化,因为UI线程已经阻塞,当重新获得句柄时图标已经是白色了。</p>
<p>为了实现需求,我们改用多任务来实现相关功能:</p>
<div class="cnblogs_code"><img id="code_img_closed_9fcfa3a9-363a-4028-b702-3f1aa12819db" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_9fcfa3a9-363a-4028-b702-3f1aa12819db" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_9fcfa3a9-363a-4028-b702-3f1aa12819db" class="cnblogs_code_hide">
<pre>    <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Form1 : Form
    {
      </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Form1()
      {
            InitializeComponent();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStart_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;

            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (DoSomething())
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
                  {
                        </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Green;
                  }));                  
                }
                </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
                  {
                        </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Red;
                  }));                  
                }

                Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">1000</span><span style="color: rgba(0, 0, 0, 1)">);

                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Invoke(<span style="color: rgba(0, 0, 255, 1)">new</span> Action(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.White;
                }));               
            });         
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">bool</span><span style="color: rgba(0, 0, 0, 1)"> DoSomething()
      {
            Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">5000</span><span style="color: rgba(0, 0, 0, 1)">);
            </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }
    }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>&nbsp;以上代码完全实现了最初的需求,但有几个不完美的地方:</p>
<p>1、主线程的btnStart_Click方法除了启动一个任务以外,啥事也没干;</p>
<p>2、由于非UI线程不能访问UI控件,代码里有很多Invoke,比较丑陋;</p>
<p>3、界面逻辑和业务逻辑掺和在一起,使得代码难以理解。</p>
<p>采用C#的异步编程模型,通过使用await、async关键字,可以更好地实现上述需求。&nbsp;</p>
<div class="cnblogs_code"><img id="code_img_closed_584ce641-0dec-4109-ac9c-baddcf0f40ae" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_584ce641-0dec-4109-ac9c-baddcf0f40ae" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_584ce641-0dec-4109-ac9c-baddcf0f40ae" class="cnblogs_code_hide">
<pre>    <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Form1 : Form
    {
      </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Form1()
      {
            InitializeComponent();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> <span style="color: rgba(0, 0, 255, 1)">void</span> btnStart_ClickAsync(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, EventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;

            </span><span style="color: rgba(0, 0, 255, 1)">var</span> result = <span style="color: rgba(0, 0, 255, 1)">await</span><span style="color: rgba(0, 0, 0, 1)"> DoSomethingAsync();
            </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(result)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Green;
            }
            </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Red;
            }

            </span><span style="color: rgba(0, 0, 255, 1)">await</span> Task.Delay(<span style="color: rgba(128, 0, 128, 1)">1000</span><span style="color: rgba(0, 0, 0, 1)">);
            
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.picShow.BackColor =<span style="color: rgba(0, 0, 0, 1)"> Color.White;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.btnStart.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> Task&lt;<span style="color: rgba(0, 0, 255, 1)">bool</span>&gt;<span style="color: rgba(0, 0, 0, 1)"> DoSomethingAsync()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">await</span> Task.Run(() =&gt;<span style="color: rgba(0, 0, 0, 1)">
            {
                Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">5000</span><span style="color: rgba(0, 0, 0, 1)">);               
            });
            </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }
    }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>这段代码看起来就像是同步代码,其业务逻辑是如此的清晰优雅,让人一目了然,关键是它还不阻塞线程,UI正常响应。</p>
<p>可以看到,通过使用await关键字,我们可以专注于业务功能实现,特别是后续任务需要前序任务的返回值的情况下,可以大量减少任务之间的同步操作,代码的可读性也大大增强。</p>
<p>&nbsp;</p>
<h3>五、 多线程环境下的数据安全</h3>
<p>目标:我们要向一个字典加入一些数据项,为了增加效率,我们使用了多个线程。</p>
<div class="cnblogs_code"><img id="code_img_closed_aa5ba943-ae77-4a28-95d4-935dcca834f8" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_aa5ba943-ae77-4a28-95d4-935dcca834f8" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_aa5ba943-ae77-4a28-95d4-935dcca834f8" class="cnblogs_code_hide">
<pre>       <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Test1()
      {
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> AddData()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i &lt; <span style="color: rgba(128, 0, 128, 1)">100</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span>(!<span style="color: rgba(0, 0, 0, 1)">Dic.ContainsKey(i))
                {
                  Dic.Add(i, i.ToString());
                }

                Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">50</span><span style="color: rgba(0, 0, 0, 1)">);
            }
      }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>向字典重复加入同样的关键字会引发异常,所以在增加数据前我们检查一下是否已经包含该关键字。以上代码看似没有问题,但有时还是会引发异常:“已添加了具有相同键的项。”原因在于我们在检查是否包含该Key时是不包含的,但在新增时其他线程加入了同样的KEY,当前线程再增加就报错了。</p>
<p>【注意:也许你多次运行上述程序都能顺利执行,不报异常,但还是要清楚认识到上述代码是有问题的!毕竟,程序在大部分情况下都运行正常,偶尔报一次故障才是最头疼的事情。】</p>
<p>上述问题传统的解决方案就是增加锁机制。对于核心的修改代码通过锁来确保不会重入。</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">object</span> locker4Add=<span style="color: rgba(0, 0, 255, 1)">new</span> <span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)">();
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> AddData()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i &lt; <span style="color: rgba(128, 0, 128, 1)">100</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">lock</span><span style="color: rgba(0, 0, 0, 1)"> (locker4Add)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">Dic.ContainsKey(i))
                  {
                        Dic.Add(i, i.ToString());
                  }
                }

                Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">50</span><span style="color: rgba(0, 0, 0, 1)">);
            }
      }</span></pre>
</div>
<p>以上代码可以解决问题,但不是最佳方案。更好的方案是使用线程安全的容器:ConcurrentDictionary。</p>
<div class="cnblogs_code"><img id="code_img_closed_f1e1cf42-7668-448c-b7a4-61c0f28e56c6" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_f1e1cf42-7668-448c-b7a4-61c0f28e56c6" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_f1e1cf42-7668-448c-b7a4-61c0f28e56c6" class="cnblogs_code_hide">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> ConcurrentDictionary&lt;<span style="color: rgba(0, 0, 255, 1)">int</span>, <span style="color: rgba(0, 0, 255, 1)">string</span>&gt; Dic = <span style="color: rgba(0, 0, 255, 1)">new</span> ConcurrentDictionary&lt;<span style="color: rgba(0, 0, 255, 1)">int</span>, <span style="color: rgba(0, 0, 255, 1)">string</span>&gt;<span style="color: rgba(0, 0, 0, 1)">();
      
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Test1()
      {
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());
            Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> AddData());   
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> AddData()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i &lt; <span style="color: rgba(128, 0, 128, 1)">100</span>; i++<span style="color: rgba(0, 0, 0, 1)">)
            {
                Dic.TryAdd(i, i.ToString());
                Thread.Sleep(</span><span style="color: rgba(128, 0, 128, 1)">50</span><span style="color: rgba(0, 0, 0, 1)">);
            }
      }</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>&nbsp;你可以在新增前继续检查一下容器是否已经包含该Key,你也可以不用检查,TryAdd方法确保不会重复添加且不会产生异常。</p>
<p>&nbsp;刚才是多个线程同时写某个对象,如果就单个线程写对象,其他多个线程仅仅是消费(访问)对象,是否可以使用非线程安全的容器呢?</p>
<p>&nbsp;基本上来说多个线程读取一个对象是没有太大问题的,但还是会存在一些要注意的地方:</p>
<p>1、对于常用的List,在对其进行foreach时List对象不能被修改,不仅不能Remove,Add也不可以;否则会报一个异常:异常信息:”集合已修改;可能无法执行枚举操作。”</p>
<p>2、还有一个类似的问题 就是调用Dictionary的ToList方法时<strong>有时</strong>会报错,将Dictionary 类型改成ConcurrentDictionary类型,问题依然存在,其原因是ToList会读取字典的Count,创建相关大小的区域后执行复制,而此时字典的长度增加了。</p>
<p>以上只是描述了多线程数据访问的两个小例子,实际使用中相关的问题一定会远远不止这些,多线程程序的大部分异常都是因为资源竞争引起的(包括死锁),一定要小心处理。</p>
<p>&nbsp;</p>
<p></p>
<h3>六、多线程的异常处理</h3>
<h4>(一) 异常处理的几个基本原则</h4>
<p>1、 基本原则:<strong>不要轻易捕获根异常</strong>;</p>
<p>2、 组件或控件抛出异常时可以根据需要自定义一些异常,不要抛出根异常,可以直接使用的常用异常有:FormatException、IndexOutOfRangException、InvalidOperationException、InvalidEnumArgumentException ;没有合适的就自定义;</p>
<p>3、 用户自定义异常从ApplicationException继承;</p>
<p>4、&nbsp;多线程的内部异常不会传播到主线程,应该在内部进行处理,可以通过事件推到主线程来;</p>
<p>5、应用程序层面可以捕获根异常,做一些记录工作,切不可隐匿异常。&nbsp;</p>
<h4>(二) 异常处理方案(基于WPF实现)</h4>
<p><strong>主线程的异常处理:</strong></p>
<p>捕获你知道的异常,并自行处理,但不要轻易捕获根异常,下面的代码令人深恶痛绝:</p>
<div class="cnblogs_code">
<pre>            <span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
            {
                DoSomething();               
            }
            </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(Exception)
            {
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Do Nothing</span>
            }</pre>
</div>
<p>&nbsp;当然,如果你确定有能力捕获根异常,并且是业务逻辑的一部分,可以捕获根异常 :</p>
<div class="cnblogs_code">
<pre>            <span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
            {
                DoSomething();
                MessageBox.Show(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">OK</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);               
            }
            </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(Exception ex)
            {
                MessageBox.Show($</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">ERROR:{ex.Message}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            }</span></pre>
</div>
<p>&nbsp;&nbsp;</p>
<p><strong>可等待异步任务的异常处理:</strong></p>
<p>可等待的任务内的异常是可以传递到调用者线程的,可以按照主线程异常统一处理:</p>
<div class="cnblogs_code">
<pre>            <span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">await</span><span style="color: rgba(0, 0, 0, 1)"> DoSomething();                              
            }
            </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(FormatException ex)
            {
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Do Something</span>
            }</pre>
</div>
<p>&nbsp;&nbsp;</p>
<p><strong>Task任务内部异常处理:</strong></p>
<p>非可等待的Task任务内部异常是无法传递到调用者线程的,参考下面代码:</p>
<div class="cnblogs_code">
<pre>            <span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
            {
                Task.Run(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)">
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">string</span> s = <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">aaa</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
                  </span><span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)">.Parse(s);
                });
            }
            </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (FormatException ex)
            {
                MessageBox.Show(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Error</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            }</span></pre>
</div>
<p>&nbsp;上面代码不会实现你期望的效果,它只会造成程序的崩溃。(有时候不会立即崩溃,后面会有解释)</p>
<p>处理办法有两个:</p>
<p>1、自行处理:(1)处理可以预料的异常,(2)同时处理根异常(写日志等),也可以不处理根异常,后面统一处理;</p>
<p>2、或将异常包装成事件推送到主线程,交给主线程处理。</p>
<div class="cnblogs_code"><img id="code_img_closed_6bbeac37-dd6a-4b4a-8b43-963c920fe3de" class="code_img_closed" src="https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" alt=""><img id="code_img_opened_6bbeac37-dd6a-4b4a-8b43-963c920fe3de" class="code_img_opened" style="display: none" src="https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif" alt="">
<div id="cnblogs_code_open_6bbeac37-dd6a-4b4a-8b43-963c920fe3de" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> FormSync : Form
{
    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">event</span> EventHandler&lt;UnhangdledExceptionArgs&gt;<span style="color: rgba(0, 0, 0, 1)"> UnhandledExceptionCatched;

    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Form_Load()
    {
       UnhandledExceptionCatched </span>+=<span style="color: rgba(0, 0, 0, 1)"> MainWindow_UnhandledExceptionCatched;
    }
    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> MainWindow_UnhandledExceptionCatched(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, UnhangdledExceptionArgs e)
    {         
      MessageBox.Show($</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">CatchException:{e.InnerException.Message}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
    }

   </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Thread1()
   {
      Task.Run(()</span>=&gt;<span style="color: rgba(0, 0, 0, 1)">
      {
            </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ApplicationException(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Thread Exception</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            }
            </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception ex)
            {               
                UnhangdledExceptionArgs args </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> UnhangdledExceptionArgs()
                {
                  InnerException </span>=<span style="color: rgba(0, 0, 0, 1)"> ex
                };
                UnhandledExceptionCatched</span>?.Invoke(<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, args);
            }
      });
   }
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> UnhangdledExceptionArgs : EventArgs
{
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> Exception InnerException { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>&nbsp;&nbsp;</p>
<p><strong>Thread和ThreadPool内部异常:</strong></p>
<p>虽然不推荐使用Thread,如果实在要用,其处理原则和上述普通Task任务内部异常处理方案一致。</p>
<p>&nbsp;</p>
<p><strong>全局未处理异常的处理:</strong></p>
<p>虽然我们不推荐catch根异常,但如果一旦发生未知异常程序就崩溃,客户恐怕难以接受吧,如果要求所有业务模块都处理根异常并进行保存日志、弹出消息等操作又非常繁琐,所以,处理的思路是业务模块不处理根异常,但应用程序要对未处理异常进行统一处理。</p>
<div class="cnblogs_code">
<pre>    <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> App : Application
    {
      App()
      {            
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.Startup +=<span style="color: rgba(0, 0, 0, 1)"> App_Startup;
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> App_Startup(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, StartupEventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.DispatcherUnhandledException +=<span style="color: rgba(0, 0, 0, 1)"> App_DispatcherUnhandledException;                     
            AppDomain.CurrentDomain.UnhandledException </span>+=<span style="color: rgba(0, 0, 0, 1)"> CurrentDomain_UnhandledException;
            TaskScheduler.UnobservedTaskException </span>+=<span style="color: rgba(0, 0, 0, 1)"> TaskScheduler_UnobservedTaskException;      
      }

      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">主线程未处理异常</span>
      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> App_DispatcherUnhandledException(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
      {
            DoSomething(e.Exception);
            e.Handled </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
      }

      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">未处理线程异常(如果主线程未处理异常已经处理,该异常不会触发)</span>
      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> CurrentDomain_UnhandledException(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, UnhandledExceptionEventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (e.ExceptionObject <span style="color: rgba(0, 0, 255, 1)">is</span><span style="color: rgba(0, 0, 0, 1)"> Exception ex)
            {
                DoSomething(ex);
            }
      }

      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">未处理的Task内异常</span>
      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> TaskScheduler_UnobservedTaskException(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, UnobservedTaskExceptionEventArgs e)
      {
            DoSomething(e.Exception);
      }

      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">保存、显示异常信息</span>
      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> ProcessException(Exception exception)
      {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">保存日志
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">提醒用户</span>
<span style="color: rgba(0, 0, 0, 1)">      }
    }</span></pre>
</div>
<p>解释一下:</p>
<p>1、 当主线程发生未处理异常时会触发App_DispatcherUnhandledException事件,在该事件中如果设置e.Handled = true,那么系统不会崩溃,如果没有设置e.Handled = true,会继续触发CurrentDomain_UnhandledException事件(毕竟主线程也是线程),而CurrentDomain_UnhandledException事件和TaskScheduler_UnobservedTaskException事件触发后,操作系统都会强行关闭这个应用程序。所以我们应该在App_DispatcherUnhandledException事件中设置e.Handled = true。</p>
<p>2、Thread线程异常会触发CurrentDomain_UnhandledException事件,导致系统崩溃,所以建议尽量不要使用Thread和ThreadPool。</p>
<p>3、非可等待的Task内部异常会触发TaskScheduler_UnobservedTaskException事件,导致系统崩溃,所以建议Task内部自行处理根异常或将异常封装为事件推到主线程。需要额外注意一点:Task内的未处理异常不会被立即触发事件,而是要延迟到GC执行回收的时候才触发,这使得问题更复杂,需要小心处理。</p>
<p>&nbsp;</p>
<h3><strong>总之</strong></h3>
<p>当前,异步编程模型已经是.NET框架的基本功能了,特别是WEB开发,后台代码已经全面异步化了,所以每个C#开发人员都不能轻视它,必须熟练掌握。 虽然在一知半解的情况下也能写多线程程序,写的程序也能跑,但就是那些平时一切正常偶尔抽风一下的错误会让头痛不已。只有深刻了解多线程的内部原理,并遵循结构化的设计原则才能写出健壮、优美的代码。</p>
<p>&nbsp;</p>

</div>
<div id="MySignature" role="contentinfo">
    <hr>
签名区:<br>
如果您觉得这篇博客对您有帮助或启发,请点击右侧【推荐】支持,谢谢!
<hr><br><br>
来源:https://www.cnblogs.com/seabluescn/p/12973936.html
頁: [1]
查看完整版本: 谈谈C#多线程开发:并行、并发与异步编程