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

2020年4月29日 2839点热度 0人点赞 3条评论
内容目录

任务基础一共三篇,本篇是第三篇,之后开始学习异步编程、并发、异步I/O的知识。

本篇会继续讲述 Task 的一些 API 和常用的操作。

TaskAwaiter

先说一下 TaskAwaiterTaskAwaiter 表示等待异步任务完成的对象并为结果提供参数。

Task 有个 GetAwaiter() 方法,会返回TaskAwaiterTaskAwaiter<TResult>TaskAwaiter 类型在 System.Runtime.CompilerServices 命名空间中定义。

TaskAwaiter 类型的属性和方法如下:

属性:

属性 说明
IsCompleted 获取一个值,该值指示异步任务是否已完成。

方法:

方法 说明
GetResult() 结束异步任务完成的等待。
OnCompleted(Action) 将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。
UnsafeOnCompleted(Action) 计划与此 awaiter 相关异步任务的延续操作。

使用示例如下:

        static void Main()
        {
            Task<int> task = new Task<int>(()=>
            {
                Console.WriteLine("我是前驱任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            TaskAwaiter<int> awaiter = task.GetAwaiter();

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

            Console.ReadKey();
        }

另外,我们前面提到过,任务发生未经处理的异常,任务被终止,也算完成任务。

延续的另一种方法

上一节我们介绍了 .ContinueWith() 方法来实现延续,这里我们介绍另一个延续方法 .ConfigureAwait()

.ConfigureAwait() 如果要尝试将延续任务封送回原始上下文,则为 true;否则为 false

我来解释一下, .ContinueWith() 延续的任务,当前驱任务完成后,延续任务会继续在此线程上继续执行。这种方式是同步的,前者和后者连续在一个线程上运行。

.ConfigureAwait(false) 方法可以实现异步,前驱方法完成后,可以不理会后续任务,而且后续任务可以在任意一个线程上运行。这个特性在 UI 界面程序上特别有用。

可以参考:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f

其使用方法如下:

        static void Main()
        {
            Task<int> task = new Task<int>(()=>
            {
                Console.WriteLine("我是前驱任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();

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

            Console.ReadKey();
        }

ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter 拥有跟 TaskAwaiter 一样的属性和方法。

.ContinueWith() .ConfigureAwait(false) 还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。

另一种创建任务的方法

前面提到提到过,创建任务的三种方法:new Task()Task.Run()Task.Factory.SatrtNew(),现在来学习第四种方法:TaskCompletionSource<TResult> 类型。

我们来看看 TaskCompletionSource<TResulr> 类型的属性和方法:

属性:

属性 说明
Task 获取由此 Task 创建的 TaskCompletionSource。

方法:

方法 说明
SetCanceled() 将基础 Task 转换为 Canceled 状态。
SetException(Exception) 将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。
SetException(IEnumerable) 将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。
SetResult(TResult) 将基础 Task 转换为 RanToCompletion 状态。
TrySetCanceled() 尝试将基础 Task 转换为 Canceled 状态。
TrySetCanceled(CancellationToken) 尝试将基础 Task 转换为 Canceled 状态并启用要存储在取消的任务中的取消标记。
TrySetException(Exception) 尝试将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。
TrySetException(IEnumerable) 尝试将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。
TrySetResult(TResult) 尝试将基础 Task 转换为 RanToCompletion 状态。

TaskCompletionSource<TResulr> 类可以对任务的生命周期做控制。

首先要通过 .Task 属性,获得一个 TaskTask<TResult>

            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task; //  Task myTask = task.Task;

然后通过 task.xxx() 方法来控制 myTask 的生命周期,但是呢,myTask 本身是没有任务内容的。

使用示例如下:

        static void Main()
        {
            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task;       // task 控制 myTask

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

            Console.WriteLine("开始等待 myTask 返回结果");
            Console.WriteLine(myTask.Result);
            Console.WriteLine("结束");
            Console.ReadKey();
        }

其它例如 SetException(Exception) 等方法,可以自行探索,这里就不再赘述。

参考资料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/

这篇文章讲得不错,而且有图:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/

实现一个支持同步和异步任务的类型

这部分内容对 TaskCompletionSource<TResult> 继续进行讲解。

这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。

  • 用户可以使用 GetResult() 同步获取结果;
  • 用户可以使用 RunAsync() 执行任务,使用 .Result 属性异步获取结果;

其实现如下:

/// <summary>
/// 实现同步任务和异步任务的类型
/// </summary>
/// <typeparam name="TResult"></typeparam>
public class MyTaskClass<TResult>
{
    private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>();
    private Task<TResult> task;
    // 保存用户需要执行的任务
    private Func<TResult> _func;

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

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

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

    /// <summary>
    /// 异步执行任务
    /// </summary>
    public void RunAsync()
    {
        Task.Factory.StartNew(() =>
        {
            source.SetResult(_func.Invoke());
            isCompleted = true;
        });
    }
}

我们在 Main 方法中,创建任务示例:

    class Program
    {
        static void Main()
        {
            // 实例化任务类
            MyTaskClass<string> myTask1 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

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

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

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

            Console.WriteLine(myTask2.Result);

            Console.ReadKey();
        }
    }

Task.FromCanceled()

微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。

这里笔者抄来了一个示例

var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task<int> genericTask = Task.FromCanceled<int>(token);

网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了?

带着疑问我们来探究一下,来个示例:

        public static Task Test()
        {
            CancellationTokenSource source = new CancellationTokenSource();
            source.Cancel();
            return Task.FromCanceled<object>(source.Token);
        }
        static void Main()
        {
            var t = Test(); // 在此设置断点,监控变量
            Console.WriteLine(t.IsCanceled);
         }

利用 Task.FromCanceled() 可以构造一个被取消的任务。我找了很久,没有找到很好的示例,如果一个任务在开始前就被取消,那么使用 Task.FromCanceled() 是很不错的。

这里有很多示例可以参考:https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken

如何在内部取消任务

之前我们讨论过,使用 CancellationToken 取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 CancellationToken 就能取消任务。

我们可以使用 CancellationTokenThrowIfCancellationRequested() 方法抛出 System.OperationCanceledException 异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。

这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。

示例如下:

    /// <summary>
    /// 能够完成多个任务的异步类型
    /// </summary>
    public class MyTaskClass
    {
        private List<Action> _actions = new List<Action>();
        private CancellationTokenSource _source = new CancellationTokenSource();
        private CancellationTokenSource _sourceBak = new CancellationTokenSource();
        private Task _task;

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

        /// <summary>
        /// 开始执行任务
        /// </summary>
        /// <returns></returns>
        public Task StartAsync()
        {
            // _ = new Task() 对本示例无效
            _task = Task.Factory.StartNew(() =>
             {
                 for (int i = 0; i < _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[tmp].Invoke();
                 }
             },_sourceBak.Token);
            return _task;
        }

        /// <summary>
        /// 取消任务
        /// </summary>
        /// <returns></returns>
        public Task Cancel()
        {
            _source.Cancel();

            // 这里可以省去
            _task = Task.FromCanceled<object>(_source.Token);
            return _task;
        }
    }

Main 方法中:

        static void Main()
        {
            // 实例化任务类
            MyTaskClass myTask = new MyTaskClass();

            for (int i = 0; i < 10; i++)
            {
                int tmp = i;
                myTask.AddTask(() =>
                {
                    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();
        }

你可以在任一阶段取消任务。

Yield 关键字

迭代器关键字,使得数据不需要一次性返回,可以在需要的时候一条条迭代,这个也相当于异步。

迭代器方法运行到 yield return 语句时,会返回一个 expression,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。

可以使用 yield break 语句来终止迭代。

官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield

网上的示例大多数都是 foreach 的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。

我们也可以这样写一个示例:

这里已经没有 foreach 了。

        private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                i++;
                yield return list[i];
            }
        }

但是,同学又问,这个 return 返回的对象 要实现这个 IEnumerable<T> 才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢?

我们可以先来改一下示例:


        private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                int num = list[i];
                i++;
                yield return num;
            }
        }

你在 Main 方法中调用,看看是不是正常运行?

        static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

这样说明了,yield return 返回的对象,并不需要实现 IEnumerable<int> 方法。

其实 yield 是语法糖关键字,你只要在循环中调用它就行了。

        static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < 100)
            {
                i++;
                yield return i;
            }
        }
    }

它会自动生成 IEnumerable<T> ,而不需要你先实现 IEnumerable<T>

补充知识点

  • 线程同步有多种方法:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphores)、事件(Event)、任务(Task);
  • Task.Run()Task.Factory.StartNew() 封装了 Task;
  • Task.Run()Task.Factory.StartNew() 的简化形式;
  • 有些地方 net Task() 是无效的;但是 Task.Run()Task.Factory.StartNew() 可以;

本篇是任务基础的终结篇,至此 C# 多线程系列,一共完成了 15 篇,后面会继续深入多线程和任务的更多使用方法和场景。

喜欢我的作者记得关注我哟~

痴者工良

高级程序员劝退师

文章评论