桐人 發表於 2020-4-28 11:00:00

C#多线程(13):任务基础①

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>多线程编程<ul><li>多线程编程模式</li><li>探究优点</li></ul></li><li>任务操作<ul><li>两种创建任务的方式</li><li>Task.Run() 创建任务</li><li>取消任务</li><li>父子任务</li><li>任务返回结果以及异步获取返回结果</li><li>捕获任务异常</li><li>全局捕获任务异常</li></ul></li></ul></div><p></p>
<h2 id="多线程编程">多线程编程</h2>
<h3 id="多线程编程模式">多线程编程模式</h3>
<p>.NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(APM)。</p>
<ul>
<li><strong>基于任务的异步模式 (TAP)</strong> :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成。包括我们常用的 async 、await 关键字,属于该模式的支持。</li>
<li>基于事件的异步模式 (EAP) :是提供异步行为的基于事件的旧模型。《C#多线程(12):线程池》中提到过此模式,.NET Core 已经不支持。</li>
<li>异步编程模型 (APM) 模式:也称为 IAsyncResult 模式,,这是使用 IAsyncResult 接口提供异步行为的旧模型。.NET Core 也不支持,请参考 《C#多线程(12):线程池》。</li>
</ul>
<p>前面,我们学习了三部分的内容:</p>
<ul>
<li>线程基础:如何创建线程、获取线程信息以及等待线程完成任务;</li>
<li>线程同步:探究各种方式实现进程和线程同步,以及线程等待;</li>
<li>线程池:线程池的优点和使用方法,基于任务的操作;</li>
</ul>
<p>这篇开始探究任务和异步,而任务和异步是十分复杂的,内容错综复杂,笔者可能讲不好。。。</p>
<h3 id="探究优点">探究优点</h3>
<p>在前面中,学习多线程(线程基础和线程同步),一共写了 10 篇,写了这么多代码,我们现在来探究一下多线程编程的复杂性。</p>
<ol>
<li>传递数据和返回结果</li>
</ol>
<p>传递数据倒是没啥问题,只是难以获取到线程的返回值,处理线程的异常也需要技巧。</p>
<ol start="2">
<li>监控线程的状态</li>
</ol>
<p>新建新的线程后,如果需要确定新线程在何时完成,需要自旋或阻塞等方式等待。</p>
<ol start="3">
<li>线程安全</li>
</ol>
<p>设计时要考虑如果避免死锁、合理使用各种同步锁,要考虑原子操作,同步信号的处理需要技巧。</p>
<ol start="4">
<li>性能</li>
</ol>
<p>玩多线程,最大需求就是提升性能,但是多线程中有很多坑,使用不当反而影响性能。</p>
<center>[以上总结可参考《C# 7.0本质论》19.3节,《C# 7.0核心技术指南》14.3 节]</center>
<p>我们通过使用线程池,可以解决上面的部分问题,但是还有更加好的选择,就是 Task(任务)。另外 Task 也是异步编程的基础类型,后面很多内容要围绕 Task 展开。</p>
<p>原理的东西,还是多参考微软官方文档和书籍,笔者讲得不一定准确,而且不会深入说明这些。</p>
<h2 id="任务操作">任务操作</h2>
<p>任务(Task)实在太多 API 了,也有各种骚操作,要讲清楚实在不容易,我们要慢慢来,一点点进步,一点点深入,多写代码测试。</p>
<p>下面与笔者一起,一步步熟悉、摸索 Task 的 API。</p>
<h3 id="两种创建任务的方式">两种创建任务的方式</h3>
<p>通过其构造函数创建一个任务,其构造函数定义为:</p>
<pre><code class="language-csharp">public Task (Action action);
</code></pre>
<p>其示例如下:</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main()
      {
            // 定义两个任务
            Task task1 = new Task(()=&gt;
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行即将结束");
            });

            Task task2 = new Task(MyTask);
            // 开始任务
            task1.Start();
            task2.Start();

            Console.ReadKey();
      }

      private static void MyTask()
      {
            Console.WriteLine("② 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));

            Console.WriteLine("② 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));

            Console.WriteLine("② 执行即将结束");
      }
    }
</code></pre>
<p><code>.Start()</code> 方法用于启动一个任务。微软文档解释:启动 Task,并将它安排到当前的 TaskScheduler 中执行。</p>
<p>TaskScheduler这个东西,我们后面讲,别急。</p>
<p>另一种方式则使用 <code>Task.Factory</code>,此属性用于创建和配置 <code>Task</code> 和 <code>Task&lt;TResult&gt;</code> 实例的工厂方法。</p>
<p>使用https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任务。</p>
<p>
    </p><div style="color: rgba(23, 23, 23, 1); font-family: &quot;Segoe UI&quot;, SegoeUI, &quot;Segoe WP&quot;, &quot;Helvetica Neue&quot;, Helvetica, Tahoma, Arial, sans-serif; background-color: rgba(255, 241, 204, 1); border-radius: 10px; padding: 20px">
当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法;<br>
官方推荐使用 Task.Run 方法启动计算限制任务。
      <br>
      Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。
</div>
<p></p>
<p><code>Task.Factory.StartNew()</code> 的重载方法是真的多,你可以参考: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--</p>
<p>这里我们使用两个重载方法编写示例:</p>
<pre><code class="language-csharp">public Task StartNew(Action action);
</code></pre>
<pre><code class="language-csharp">public Task StartNew(Action action, TaskCreationOptions creationOptions);
</code></pre>
<p>代码示例如下:</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main()
      {
            // 重载方法 1
            Task.Factory.StartNew(() =&gt;
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行即将结束");
            });

            // 重载方法 1
            Task.Factory.StartNew(MyTask);

            // 重载方法 2
            Task.Factory.StartNew(() =&gt;
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行即将结束");
            },TaskCreationOptions.LongRunning);

            Console.ReadKey();
      }

      // public delegate void TimerCallback(object? state);
      private static void MyTask()
      {
            Console.WriteLine("② 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));

            Console.WriteLine("② 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));

            Console.WriteLine("② 执行即将结束");
      }
    }
</code></pre>
<p>通过<code>Task.Factory.StartNew()</code> 方法添加的任务,会进入线程池任务队列然后自动执行,不需要手动启动。</p>
<p><code>TaskCreationOptions.LongRunning</code> 是控制任务创建特性的枚举,后面讲。</p>
<h3 id="taskrun-创建任务">Task.Run() 创建任务</h3>
<p><code>Task.Run()</code> 创建任务,跟<code>Task.Factory.StartNew()</code> 差不多,当然 <code>Task.Run()</code> 还有很多重载方法和骚操作,我们后面再来学。</p>
<p><code>Task.Run()</code> 创建任务示例代码如下:</p>
<pre><code class="language-csharp">      static void Main()
      {
            Task.Run(() =&gt;
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("① 执行即将结束");
            });
            Console.ReadKey();
      }
</code></pre>
<h3 id="取消任务">取消任务</h3>
<p>取消任务,《C#多线程(12):线程池》 中说过一次,不过控制太自由,全靠任务本身自觉判断是否取消。</p>
<p>这里我们通过 Task 来实现任务的取消,其取消是实时的、自动的,并且不需要手工控制。</p>
<p>其构造函数如下:</p>
<pre><code class="language-csharp">public Task StartNew(Action action, CancellationToken cancellationToken);
</code></pre>
<p>代码示例如下:</p>
<p>按下回车键的时候记得切换字母模式。</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main()
      {
            Console.WriteLine("任务开始启动,按下任意键,取消执行任务");
            CancellationTokenSource cts = new CancellationTokenSource();
            Task.Factory.StartNew(MyTask, cts.Token);

            Console.ReadKey();

            cts.Cancel();       // 取消任务
            Console.ReadKey();
      }

      // public delegate void TimerCallback(object? state);
      private static void MyTask()
      {
            Console.WriteLine(" 开始执行");
            int i = 0;
            while (true)
            {
                Console.WriteLine($" 第{i}次任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("   执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));

                Console.WriteLine("   执行结束");
                i++;
            }
      }
    }
</code></pre>
<h3 id="父子任务">父子任务</h3>
<p>前面创建任务的时候,我们碰到了 <code>TaskCreationOptions.LongRunning</code>这个枚举类型,这个枚举用于控制任务的创建以及设定任务的行为。</p>
<p>其枚举如下:</p>
<table>
<thead>
<tr>
<th>枚举</th>
<th>值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>AttachedToParent</td>
<td>4</td>
<td>指定将任务附加到任务层次结构中的某个父级。</td>
</tr>
<tr>
<td>DenyChildAttach</td>
<td>8</td>
<td>指定任何尝试作为附加的子任务执行的子任务都无法附加到父任务,会改成作为分离的子任务执行。</td>
</tr>
<tr>
<td>HideScheduler</td>
<td>16</td>
<td>防止环境计划程序被视为已创建任务的当前计划程序。</td>
</tr>
<tr>
<td>LongRunning</td>
<td>2</td>
<td>指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。</td>
</tr>
<tr>
<td>None</td>
<td>0</td>
<td>指定应使用默认行为。</td>
</tr>
<tr>
<td>PreferFairness</td>
<td>1</td>
<td>提示 TaskScheduler 以一种尽可能公平的方式安排任务。</td>
</tr>
<tr>
<td>RunContinuationsAsynchronously</td>
<td>64</td>
<td>强制异步执行添加到当前任务的延续任务。</td>
</tr>
</tbody>
</table>
<p>这个枚举在 <code>TaskFactory</code> 和 <code>TaskFactory&lt;TResult&gt;</code> 、<code>Task</code> 和 <code>Task&lt;TResult&gt;</code> 、</p>
<p><code>StartNew()</code>、<code>FromAsync()</code> 、<code>TaskCompletionSource&lt;TResult&gt;</code> 等地方可以使用到。</p>
<p>
    </p><div style="color: rgba(23, 23, 23, 1); font-family: &quot;Segoe UI&quot;, SegoeUI, &quot;Segoe WP&quot;, &quot;Helvetica Neue&quot;, Helvetica, Tahoma, Arial, sans-serif; background-color: rgba(255, 241, 204, 1); border-radius: 10px; padding: 20px">
笔者在这里犯了一个错误,在写下一篇文章时重新测试发现的。文档的中文翻译实在太可怕了。。。<br>
子任务使用了 TaskCreationOptions.AttachedToParent ,并不是指父任务要等待子任务完成后,父任务才能继续完往下执行;而是指父任务如果先执行完毕,那么必须等待子任务完成后,父任务才算完成。
      </div>
<p></p>
<p>这里来探究 <code>TaskCreationOptions.AttachedToParent</code>的使用。代码示例如下:</p>
<pre><code class="language-csharp">            // 父子任务
            Task task = new Task(() =&gt;
            {
                // TaskCreationOptions.AttachedToParent
                // 将此任务附加到父任务中
                // 父任务需要等待所有子任务完成后,才能算完成
                Task task1 = new Task(() =&gt;
                {
                  Thread.Sleep(TimeSpan.FromSeconds(1));
                  for (int i = 0; i &lt; 5; i++)
                  {
                        Console.WriteLine("   内层任务1");
                        Thread.Sleep(TimeSpan.FromSeconds(0.5));
                  }
                }, TaskCreationOptions.AttachedToParent);
                task1.Start();

                Console.WriteLine("最外层任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            });

            task.Start();
            task.Wait();

            Console.ReadKey();
</code></pre>
<p>而 <code>TaskCreationOptions.DenyChildAttach</code> 则不允许其它任务附加到外层任务中。</p>
<pre><code class="language-csharp">      static void Main()
      {
            // 不允许出现父子任务
            Task task = new Task(() =&gt;
            {
                Task task1 = new Task(() =&gt;
                {
                  Thread.Sleep(TimeSpan.FromSeconds(1));
                  for (int i = 0; i &lt; 5; i++)
                  {
                        Console.WriteLine("内层任务1");
                        Thread.Sleep(TimeSpan.FromSeconds(0.5));
                  }
                }, TaskCreationOptions.AttachedToParent);
                task1.Start();

                Console.WriteLine("最外层任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }, TaskCreationOptions.DenyChildAttach); // 不收儿子

            task.Start();
            task.Wait();

            Console.ReadKey();
      }
</code></pre>
<p>然后,这里也学习了一个新的 Task 方法:<code>Wait()</code> 等待 Task 完成执行过程。<code>Wait()</code> 也可以设置超时时间。</p>
<p>如果父任务是通过调用 Task.Run 方法而创建的,则可以隐式阻止子任务附加到其中。</p>
<p>关于附加的子任务,请参考:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1</p>
<h3 id="任务返回结果以及异步获取返回结果">任务返回结果以及异步获取返回结果</h3>
<p>要获取任务返回结果,要使用泛型类或方法创建任务,例如 <code>Task&lt;Tresult&gt;</code>、<code>Task.Factory.StartNew&lt;TResult&gt;()</code>、<code>Task.Run&lt;TResult&gt;</code>。</p>
<p>通过 其泛型的 的 <code>Result</code> 属性,可以获得返回结果。</p>
<p>异步获取任务执行结果:</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main()
      {
            // *******************************
            Task&lt;int&gt; task = new Task&lt;int&gt;(() =&gt;
            {
                return 666;
            });
            // 执行
            task.Start();
            // 获取结果,属于异步
            int number = task.Result;

            // *******************************
            task = Task.Factory.StartNew&lt;int&gt;(() =&gt;
            {
                return 666;
            });

            // 也可以异步获取结果
            number = task.Result;

            // *******************************
            task = Task.Run&lt;int&gt;(() =&gt;
            {
                  return 666;
            });

            // 也可以异步获取结果
            number = task.Result;
            Console.ReadKey();
      }
    }
</code></pre>
<p>如果要同步的话,可以改成:</p>
<pre><code class="language-csharp">            int number = Task.Factory.StartNew&lt;int&gt;(() =&gt;
            {
                return 666;
            }).Result;
</code></pre>
<h3 id="捕获任务异常">捕获任务异常</h3>
<p>进行中的任务发生了异常,不会直接抛出来阻止主线程执行,当获取任务处理结果或者等待任务完成时,异常会重新抛出。</p>
<p>示例如下:</p>
<pre><code class="language-csharp">      static void Main()
      {
            // *******************************
            Task&lt;int&gt; task = new Task&lt;int&gt;(() =&gt;
            {
                throw new Exception("反正就想弹出一个异常");
            });
            // 执行
            task.Start();
            Console.WriteLine("任务中的异常不会直接传播到主线程");
            Thread.Sleep(TimeSpan.FromSeconds(1));

            // 当任务发生异常,获取结果时会弹出
            int number = task.Result;

            // task.Wait(); 等待任务时,如果发生异常,也会弹出

            Console.ReadKey();
      }
</code></pre>
<p>乱抛出异常不是很好的行为噢~可以改成如下:</p>
<pre><code class="language-csharp">      static void Main()
      {
            Task&lt;Program&gt; task = new Task&lt;Program&gt;(() =&gt;
            {
                try
                {
                  throw new Exception("反正就想弹出一个异常");
                  return new Program();
                }
                catch
                {
                  return null;
                }
            });
            task.Start();

            var result = task.Result;
            if (result is null)
                Console.WriteLine("任务执行失败");
            else Console.WriteLine("任务执行成功");

            Console.ReadKey();
      }
</code></pre>
<h3 id="全局捕获任务异常">全局捕获任务异常</h3>
<p><code>TaskScheduler.UnobservedTaskException</code> 是一个事件,其委托定义如下:</p>
<pre><code class="language-csharp">public delegate void EventHandler&lt;TEventArgs&gt;(object? sender, TEventArgs e);
</code></pre>
<p>下面是一个示例:</p>
<p>请发布程序后,打开目录执行程序。</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main()
      {
            TaskScheduler.UnobservedTaskException += MyTaskException;

            Task.Factory.StartNew(() =&gt;
             {
               throw new ArgumentNullException();
             });
            Thread.Sleep(100);
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine("Done");
            Console.ReadKey();
      }
      public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs)
      {
            // eventArgs.SetObserved();
            ((AggregateException)eventArgs.Exception).Handle(ex =&gt;
            {
                Console.WriteLine("Exception type: {0}", ex.GetType());
                return true;
            });
      }
    }
</code></pre>
<p>TaskScheduler.UnobservedTaskException 到底怎么用,笔者不太清楚。而且效果难以观察。</p>
<p>请参考:</p>
<p>https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException</p>


</div>
<div id="MySignature" role="contentinfo">
    痴者工良(https://whuanle.cn)<br><br>
来源:https://www.cnblogs.com/whuanle/p/12792639.html
頁: [1]
查看完整版本: C#多线程(13):任务基础①