在风里在雨里 發表於 2020-4-29 16:20:00

C#多线程(15):任务基础③

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>TaskAwaiter</li><li>延续的另一种方法</li><li>另一种创建任务的方法</li><li>实现一个支持同步和异步任务的类型</li><li>Task.FromCanceled()</li><li>如何在内部取消任务</li><li>Yield 关键字</li><li>补充知识点</li></ul></div><br>
任务基础一共三篇,本篇是第三篇,之后开始学习异步编程、并发、异步I/O的知识。<p></p>
<p>本篇会继续讲述 Task 的一些 API 和常用的操作。</p>
<h3 id="taskawaiter">TaskAwaiter</h3>
<p>先说一下 <code>TaskAwaiter</code>,<code>TaskAwaiter</code> 表示等待异步任务完成的对象并为结果提供参数。</p>
<p>Task 有个 <code>GetAwaiter()</code> 方法,会返回<code>TaskAwaiter</code> 或<code>TaskAwaiter&lt;TResult&gt;</code>,<code>TaskAwaiter</code> 类型在 <code>System.Runtime.CompilerServices</code> 命名空间中定义。</p>
<p><code>TaskAwaiter</code> 类型的属性和方法如下:</p>
<p>属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>IsCompleted</td>
<td>获取一个值,该值指示异步任务是否已完成。</td>
</tr>
</tbody>
</table>
<p>方法:</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>GetResult()</td>
<td>结束异步任务完成的等待。</td>
</tr>
<tr>
<td>OnCompleted(Action)</td>
<td>将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。</td>
</tr>
<tr>
<td>UnsafeOnCompleted(Action)</td>
<td>计划与此 awaiter 相关异步任务的延续操作。</td>
</tr>
</tbody>
</table>
<p>使用示例如下:</p>
<pre><code class="language-csharp">      static void Main()
      {
            Task&lt;int&gt; task = new Task&lt;int&gt;(()=&gt;
            {
                Console.WriteLine("我是前驱任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            TaskAwaiter&lt;int&gt; awaiter = task.GetAwaiter();

            awaiter.OnCompleted(()=&gt;
            {
                Console.WriteLine("前驱任务完成时,我就会继续执行");
            });
            task.Start();

            Console.ReadKey();
      }
</code></pre>
<p>另外,我们前面提到过,任务发生未经处理的异常,任务被终止,也算完成任务。</p>
<h3 id="延续的另一种方法">延续的另一种方法</h3>
<p>上一节我们介绍了 <code>.ContinueWith()</code> 方法来实现延续,这里我们介绍另一个延续方法 <code>.ConfigureAwait()</code>。</p>
<p><code>.ConfigureAwait()</code> 如果要尝试将延续任务封送回原始上下文,则为 <code>true</code>;否则为 <code>false</code>。</p>
<p>我来解释一下, <code>.ContinueWith()</code> 延续的任务,当前驱任务完成后,延续任务会继续在此线程上继续执行。这种方式是同步的,前者和后者连续在一个线程上运行。</p>
<p><code> .ConfigureAwait(false)</code> 方法可以实现异步,前驱方法完成后,可以不理会后续任务,而且后续任务可以在任意一个线程上运行。这个特性在 UI 界面程序上特别有用。</p>
<p>可以参考:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f</p>
<p>其使用方法如下:</p>
<pre><code class="language-csharp">      static void Main()
      {
            Task&lt;int&gt; task = new Task&lt;int&gt;(()=&gt;
            {
                Console.WriteLine("我是前驱任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            ConfiguredTaskAwaitable&lt;int&gt;.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();

            awaiter.OnCompleted(()=&gt;
            {
                Console.WriteLine("前驱任务完成时,我就会继续执行");
            });
            task.Start();

            Console.ReadKey();
      }
</code></pre>
<p><code>ConfiguredTaskAwaitable&lt;int&gt;.ConfiguredTaskAwaiter </code> 拥有跟 <code>TaskAwaiter</code> 一样的属性和方法。</p>
<p><code>.ContinueWith()</code>跟 <code> .ConfigureAwait(false)</code>还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。</p>
<h3 id="另一种创建任务的方法">另一种创建任务的方法</h3>
<p>前面提到提到过,创建任务的三种方法:<code>new Task()</code>、<code>Task.Run()</code>、<code>Task.Factory.SatrtNew()</code>,现在来学习第四种方法:<code>TaskCompletionSource&lt;TResult&gt;</code> 类型。</p>
<p>我们来看看 <code>TaskCompletionSource&lt;TResulr&gt;</code> 类型的属性和方法:</p>
<p>属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Task</td>
<td>获取由此 Task 创建的 TaskCompletionSource。</td>
</tr>
</tbody>
</table>
<p>方法:</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>SetCanceled()</td>
<td>将基础 Task 转换为 Canceled 状态。</td>
</tr>
<tr>
<td>SetException(Exception)</td>
<td>将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。</td>
</tr>
<tr>
<td>SetException(IEnumerable)</td>
<td>将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。</td>
</tr>
<tr>
<td>SetResult(TResult)</td>
<td>将基础 Task 转换为 RanToCompletion 状态。</td>
</tr>
<tr>
<td>TrySetCanceled()</td>
<td>尝试将基础 Task 转换为 Canceled 状态。</td>
</tr>
<tr>
<td>TrySetCanceled(CancellationToken)</td>
<td>尝试将基础 Task 转换为 Canceled 状态并启用要存储在取消的任务中的取消标记。</td>
</tr>
<tr>
<td>TrySetException(Exception)</td>
<td>尝试将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。</td>
</tr>
<tr>
<td>TrySetException(IEnumerable)</td>
<td>尝试将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。</td>
</tr>
<tr>
<td>TrySetResult(TResult)</td>
<td>尝试将基础 Task 转换为 RanToCompletion 状态。</td>
</tr>
</tbody>
</table>
<p><code>TaskCompletionSource&lt;TResulr&gt;</code> 类可以对任务的生命周期做控制。</p>
<p>首先要通过 <code>.Task</code> 属性,获得一个 <code>Task</code> 或 <code>Task&lt;TResult&gt;</code> 。</p>
<pre><code class="language-csharp">            TaskCompletionSource&lt;int&gt; task = new TaskCompletionSource&lt;int&gt;();
            Task&lt;int&gt; myTask = task.Task;        //Task myTask = task.Task;
</code></pre>
<p>然后通过 <code>task.xxx()</code> 方法来控制 <code>myTask</code> 的生命周期,但是呢,myTask 本身是没有任务内容的。</p>
<p>使用示例如下:</p>
<pre><code class="language-csharp">      static void Main()
      {
            TaskCompletionSource&lt;int&gt; task = new TaskCompletionSource&lt;int&gt;();
            Task&lt;int&gt; myTask = task.Task;       // task 控制 myTask

            // 新开一个任务做实验
            Task mainTask = new Task(() =&gt;
            {
                Console.WriteLine("我可以控制 myTask 任务");
                Console.WriteLine("按下任意键,我让 myTask 任务立即完成");
                Console.ReadKey();
                task.SetResult(666);
            });
            mainTask.Start();

            Console.WriteLine("开始等待 myTask 返回结果");
            Console.WriteLine(myTask.Result);
            Console.WriteLine("结束");
            Console.ReadKey();
      }
</code></pre>
<p>其它例如 <code>SetException(Exception)</code> 等方法,可以自行探索,这里就不再赘述。</p>
<p>参考资料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/</p>
<p>这篇文章讲得不错,而且有图:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/</p>
<h3 id="实现一个支持同步和异步任务的类型">实现一个支持同步和异步任务的类型</h3>
<p>这部分内容对 <code>TaskCompletionSource&lt;TResult&gt;</code> 继续进行讲解。</p>
<p>这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。</p>
<ul>
<li>用户可以使用 <code>GetResult()</code> 同步获取结果;</li>
<li>用户可以使用 <code>RunAsync()</code> 执行任务,使用 <code>.Result</code> 属性异步获取结果;</li>
</ul>
<p>其实现如下:</p>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 实现同步任务和异步任务的类型
/// &lt;/summary&gt;
/// &lt;typeparam name="TResult"&gt;&lt;/typeparam&gt;
public class MyTaskClass&lt;TResult&gt;
{
    private readonly TaskCompletionSource&lt;TResult&gt; source = new TaskCompletionSource&lt;TResult&gt;();
    private Task&lt;TResult&gt; task;
    // 保存用户需要执行的任务
    private Func&lt;TResult&gt; _func;

    // 是否已经执行完成,同步或异步执行都行
    private bool isCompleted = false;
    // 任务执行结果
    private TResult _result;

    /// &lt;summary&gt;
    /// 获取执行结果
    /// &lt;/summary&gt;
    public TResult Result
    {
      get
      {
            if (isCompleted)
                return _result;
            else return task.Result;
      }
    }
    public MyTaskClass(Func&lt;TResult&gt; func)
    {
      _func = func;
      task = source.Task;
    }

    /// &lt;summary&gt;
    /// 同步方法获取结果
    /// &lt;/summary&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public TResult GetResult()
    {
      _result = _func.Invoke();
      isCompleted = true;
      return _result;
    }

    /// &lt;summary&gt;
    /// 异步执行任务
    /// &lt;/summary&gt;
    public void RunAsync()
    {
      Task.Factory.StartNew(() =&gt;
      {
            source.SetResult(_func.Invoke());
            isCompleted = true;
      });
    }
}
</code></pre>
<p>我们在 Main 方法中,创建任务示例:</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main()
      {
            // 实例化任务类
            MyTaskClass&lt;string&gt; myTask1 = new MyTaskClass&lt;string&gt;(() =&gt;
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

            // 直接同步获取结果
            Console.WriteLine(myTask1.GetResult());


            // 实例化任务类
            MyTaskClass&lt;string&gt; myTask2 = new MyTaskClass&lt;string&gt;(() =&gt;
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

            // 异步获取结果
            myTask2.RunAsync();

            Console.WriteLine(myTask2.Result);


            Console.ReadKey();
      }
    }
</code></pre>
<h3 id="taskfromcanceled">Task.FromCanceled()</h3>
<p>微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。</p>
<p>这里笔者抄来了一个示例:</p>
<pre><code class="language-csharp">var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task&lt;int&gt; genericTask = Task.FromCanceled&lt;int&gt;(token);
</code></pre>
<p>网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了?</p>
<p>带着疑问我们来探究一下,来个示例:</p>
<pre><code class="language-csharp">      public static Task Test()
      {
            CancellationTokenSource source = new CancellationTokenSource();
            source.Cancel();
            return Task.FromCanceled&lt;object&gt;(source.Token);
      }
      static void Main()
      {
            var t = Test();        // 在此设置断点,监控变量
            Console.WriteLine(t.IsCanceled);
         }
</code></pre>
<p><code>Task.FromCanceled()</code> 可以构造一个被取消的任务。我找了很久,没有找到很好的示例,如果一个任务在开始前就被取消,那么使用 <code>Task.FromCanceled()</code> 是很不错的。</p>
<p>这里有很多示例可以参考:https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/</p>
<h3 id="如何在内部取消任务">如何在内部取消任务</h3>
<p>之前我们讨论过,使用 <code>CancellationToken</code> 取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 <code>CancellationToken</code> 就能取消任务。</p>
<p>我们可以使用 <code>CancellationToken</code> 的 <code>ThrowIfCancellationRequested()</code> 方法抛出 <code>System.OperationCanceledException</code> 异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。</p>
<p>这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。</p>
<p>示例如下:</p>
<pre><code class="language-csharp">    /// &lt;summary&gt;
    /// 能够完成多个任务的异步类型
    /// &lt;/summary&gt;
    public class MyTaskClass
    {
      private List&lt;Action&gt; _actions = new List&lt;Action&gt;();
      private CancellationTokenSource _source = new CancellationTokenSource();
      private CancellationTokenSource _sourceBak = new CancellationTokenSource();
      private Task _task;

      /// &lt;summary&gt;
      ///添加一个任务
      /// &lt;/summary&gt;
      /// &lt;param name="action"&gt;&lt;/param&gt;
      public void AddTask(Action action)
      {
            _actions.Add(action);
      }

      /// &lt;summary&gt;
      /// 开始执行任务
      /// &lt;/summary&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      public Task StartAsync()
      {
            // _ = new Task() 对本示例无效
            _task = Task.Factory.StartNew(() =&gt;
             {
               for (int i = 0; i &lt; _actions.Count; i++)
               {
                     int tmp = i;
                     Console.WriteLine($"第 {tmp} 个任务");
                     if (_source.Token.IsCancellationRequested)
                     {
                         Console.ForegroundColor = ConsoleColor.Red;
                         Console.WriteLine("任务已经被取消");
                         Console.ForegroundColor = ConsoleColor.White;
                         _sourceBak.Cancel();
                         _sourceBak.Token.ThrowIfCancellationRequested();
                     }
                     _actions.Invoke();
               }
             },_sourceBak.Token);
            return _task;
      }

      /// &lt;summary&gt;
      /// 取消任务
      /// &lt;/summary&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      public Task Cancel()
      {
            _source.Cancel();

            // 这里可以省去
            _task = Task.FromCanceled&lt;object&gt;(_source.Token);
            return _task;
      }
    }
</code></pre>
<p>Main 方法中:</p>
<pre><code class="language-csharp">      static void Main()
      {
            // 实例化任务类
            MyTaskClass myTask = new MyTaskClass();

            for (int i = 0; i &lt; 10; i++)
            {
                int tmp = i;
                myTask.AddTask(() =&gt;
                {
                  Console.WriteLine("   任务 1 Start");
                  Thread.Sleep(TimeSpan.FromSeconds(1));
                  Console.WriteLine("   任务 1 End");
                  Thread.Sleep(TimeSpan.FromSeconds(1));
                });
            }

            // 相当于 Task.WhenAll()
            Task task = myTask.StartAsync();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine($"任务是否被取消:{task.IsCanceled}");

            // 取消任务
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("按下任意键可以取消任务");
            Console.ForegroundColor = ConsoleColor.White;
            Console.ReadKey();

            var t = myTask.Cancel();    // 取消任务
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine($"任务是否被取消:【{task.IsCanceled}】");

            Console.ReadKey();
      }
</code></pre>
<p>你可以在任一阶段取消任务。</p>
<h3 id="yield-关键字">Yield 关键字</h3>
<p>迭代器关键字,使得数据不需要一次性返回,可以在需要的时候一条条迭代,这个也相当于异步。</p>
<p>迭代器方法运行到 <code>yield return</code> 语句时,会返回一个 <code>expression</code>,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。</p>
<p>可以使用 <code>yield break</code> 语句来终止迭代。</p>
<p>官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield</p>
<p>网上的示例大多数都是 <code>foreach</code> 的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。</p>
<p>我们也可以这样写一个示例:</p>
<p>这里已经没有 <code>foreach</code> 了。</p>
<pre><code class="language-csharp">      private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

      private static IEnumerable&lt;int&gt; ForAsync()
      {
            int i = 0;
            while (i &lt; list.Length)
            {
                i++;
                yield return list;
            }
      }
</code></pre>
<p>但是,同学又问,这个 return 返回的对象 要实现这个 <code>IEnumerable&lt;T&gt;</code> 才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢?</p>
<p>我们可以先来改一下示例:</p>
<pre><code class="language-csharp">      private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

      private static IEnumerable&lt;int&gt; ForAsync()
      {
            int i = 0;
            while (i &lt; list.Length)
            {
                int num = list;
                i++;
                yield return num;
            }
      }
</code></pre>
<p>你在 Main 方法中调用,看看是不是正常运行?</p>
<pre><code class="language-csharp">      static void Main()
      {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
      }
</code></pre>
<p>这样说明了,<code>yield return</code> 返回的对象,并不需要实现 <code>IEnumerable&lt;int&gt;</code> 方法。</p>
<p>其实 <code>yield</code> 是语法糖关键字,你只要在循环中调用它就行了。</p>
<pre><code class="language-csharp">      static void Main()
      {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
      }

      private static IEnumerable&lt;int&gt; ForAsync()
      {
            int i = 0;
            while (i &lt; 100)
            {
                i++;
                yield return i;
            }
      }
    }
</code></pre>
<p>它会自动生成 <code>IEnumerable&lt;T&gt;</code> ,而不需要你先实现<code>IEnumerable&lt;T&gt;</code> 。</p>
<h3 id="补充知识点">补充知识点</h3>
<ul>
<li>
<p>线程同步有多种方法:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphores)、事件(Event)、任务(Task);</p>
</li>
<li>
<p><code>Task.Run()</code> 和 <code>Task.Factory.StartNew()</code> 封装了 Task;</p>
</li>
<li>
<p><code>Task.Run()</code>是 <code>Task.Factory.StartNew()</code> 的简化形式;</p>
</li>
<li>
<p>有些地方 <code>net Task()</code> 是无效的;但是 <code>Task.Run()</code> 和 <code>Task.Factory.StartNew()</code> 可以;</p>
</li>
</ul>
<p>本篇是任务基础的终结篇,至此 C# 多线程系列,一共完成了 15 篇,后面会继续深入多线程和任务的更多使用方法和场景。</p>
<p>喜欢我的作者记得关注我哟~</p>


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