C#多线程系列之任务基础(三)

目录
  • TaskAwaiter
  • 延续的另一种方法
  • 另一种创建任务的方法
  • 实现一个支持同步和异步任务的类型
  • Task.FromCanceled()
  • 如何在内部取消任务
  • Yield 关键字
  • 补充知识点

TaskAwaiter

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

Task 有个 GetAwaiter() 方法,会返回TaskAwaiter 或TaskAwaiter<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 属性,获得一个 Task 或 Task<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 就能取消任务。

我们可以使用 CancellationToken 的 ThrowIfCancellationRequested() 方法抛出 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#多线程系列之任务基础(三)的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#多线程系列之任务基础(二)

    目录 判断任务状态 再说父子任务 组合任务/延续任务 复杂的延续任务 并行(异步)处理任务 并行(同步)处理任务 并行任务的 Task.WhenAny 并行任务状态 循环中值变化问题 定时任务 TaskScheduler 类 判断任务状态 属性 说明 IsCanceled 获取此 Task 实例是否由于被取消的原因而已完成执行. IsCompleted 获取一个值,它表示是否已完成任务. IsCompletedSuccessfully 了解任务是否运行到完成. IsFaulted 获取 Task

  • C#多线程系列之读写锁

    本篇的内容主要是介绍 ReaderWriterLockSlim 类,来实现多线程下的读写分离. ReaderWriterLockSlim ReaderWriterLock 类:定义支持单个写线程和多个读线程的锁. ReaderWriterLockSlim 类:表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问. 两者的 API 十分接近,而且 ReaderWriterLockSlim 相对 ReaderWriterLock 来说 更加安全.因此本文主要讲解 ReaderWrit

  • C#多线程系列之进程同步Mutex类

    Mutex 中文为互斥,Mutex 类叫做互斥锁.它还可用于进程间同步的同步基元. Mutex 跟 lock 相似,但是 Mutex 支持多个进程.Mutex 大约比 lock 慢 20 倍. 互斥锁(Mutex),用于多线程中防止两条线程同时对一个公共资源进行读写的机制. Windows 操作系统中,Mutex 同步对象有两个状态: signaled:未被任何对象拥有: nonsignaled:被一个线程拥有: Mutex 只能在获得锁的线程中,释放锁. 构造函数和方法 Mutex 类其构造函

  • C#多线程系列之线程完成数

    解决一个问题 假如,程序需要向一个 Web 发送 5 次请求,受网路波动影响,有一定几率请求失败.如果失败了,就需要重试. 示例代码如下: class Program { private static int count = 0; static void Main(string[] args) { for (int i = 0; i < 5; i++) new Thread(HttpRequest).Start(); // 创建线程 // 用于不断向另一个线程发送信号 while (count

  • C#多线程系列之线程的创建和生命周期

    目录 1,获取当前线程信息 2,管理线程状态 2.1 启动与参数传递 2.1.1 ParameterizedThreadStart 2.1.2 使用静态变量或类成员变量 2.1.3 委托与Lambda 2.2 暂停与阻塞 2.3 线程状态 2.4 终止 2.5 线程的不确定性 2.6 线程优先级.前台线程和后台线程 2.7 自旋和休眠 1,获取当前线程信息 Thread.CurrentThread 是一个 静态的 Thread 类,Thread 的CurrentThread 属性,可以获取到当前

  • C#多线程系列之多阶段并行线程

    前言 这一篇,我们将学习用于实现并行任务.使得多个线程有序同步完成多个阶段的任务. 应用场景主要是控制 N 个线程(可随时增加或减少执行的线程),使得多线程在能够在 M 个阶段中保持同步. 线程工作情况如下: 我们接下来 将学习C# 中的 Barrier ,用于实现并行协同工作. Barrier 类 使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作,使多个线程(称为“参与者” )分阶段同时处理算法. 可以使多个线程(称为“参与者” )分阶段同时处理算法.(注意算法这个词) 每个参与者完

  • C#多线程系列之任务基础(一)

    目录 多线程编程 多线程编程模式 探究优点 任务操作 两种创建任务的方式 Task.Run() 创建任务 取消任务 父子任务 任务返回结果以及异步获取返回结果 捕获任务异常 全局捕获任务异常 多线程编程 多线程编程模式 .NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP).基于事件的异步模式(EAP).异步编程模式(APM). 基于任务的异步模式 (TAP) :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成.包括我们常用的 async .await

  • C#多线程系列之资源池限制

    Semaphore.SemaphoreSlim 类 两者都可以限制同时访问某一资源或资源池的线程数. 这里先不扯理论,我们从案例入手,通过示例代码,慢慢深入了解. Semaphore 类 这里,先列出 Semaphore 类常用的 API. 其构造函数如下: 构造函数 说明 Semaphore(Int32, Int32) 初始化 Semaphore 类的新实例,并指定初始入口数和最大并发入口数. Semaphore(Int32, Int32, String) 初始化 Semaphore 类的新实

  • C#多线程系列之原子操作

    目录 知识点 竞争条件 线程同步 CPU时间片和上下文切换 阻塞 内核模式和用户模式 Interlocked 类 1,出现问题 2,Interlocked.Increment() 3,Interlocked.Exchange() 4,Interlocked.CompareExchange() 5,Interlocked.Add() 6,Interlocked.Read() 知识点 竞争条件 当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生争用的情况.它们所依赖的那部分共享数据,叫

  • C#多线程系列之工作流实现

    目录 前言 节点 Then Parallel Schedule Delay 试用一下 顺序节点 并行任务 编写工作流 接口构建器 工作流构建器 依赖注入 实现工作流解析 前言 前面学习了很多多线程和任务的基础知识,这里要来实践一下啦.通过本篇教程,你可以写出一个简单的工作流引擎. 本篇教程内容完成是基于任务的,只需要看过笔者的三篇关于异步的文章,掌握 C# 基础,即可轻松完成. C#多线程系列之任务基础(一) C#多线程系列之任务基础(二) C#多线程系列之任务基础(三) 由于本篇文章编写的工作

  • C#多线程系列之手动线程通知

    区别与示例 AutoResetEvent 和 ManualResetEvent 十分相似.两者之间的区别,在于前者是自动(Auto),后者是手动(Manua). 你可以先运行下面的示例,再测试两者的区别. AutoResetEvent 示例: class Program { // 线程通知 private static AutoResetEvent resetEvent = new AutoResetEvent(false); static void Main(string[] args) {

  • C#多线程系列之线程池

    目录 线程池 ThreadPool 常用属性和方法 线程池说明和示例 线程池线程数 线程池线程数说明 不支持的线程池异步委托 任务取消功能 计时器 线程池 线程池全称为托管线程池,线程池受 .NET 通用语言运行时(CLR)管理,线程的生命周期由 CLR 处理,因此我们可以专注于实现任务,而不需要理会线程管理. 线程池的应用场景:任务并行库 (TPL)操作.异步 I/O 完成.计时器回调.注册的等待操作.使用委托的异步方法调用和套接字连接. 很多人不清楚 Task.Task<TResult>

  • C#多线程系列之线程通知

    AutoRestEvent 类用于从一个线程向另一个线程发送通知. 微软文档是这样介绍的:表示线程同步事件在一个等待线程释放后收到信号时自动重置. 其构造函数只有一个: 构造函数里面的参数用于设置信号状态. 构造函数 说明 AutoResetEvent(Boolean) 用一个指示是否将初始状态设置为终止的布尔值初始化 AutoResetEvent 类的新实例. 真糟糕的机器翻译. 常用方法 AutoRestEvent 类是干嘛的,构造函数的参数又是干嘛的?不着急,我们来先来看看这个类常用的方法

  • C#多线程系列之多线程锁lock和Monitor

    目录 1,Lock lock 原型 lock 编写实例 2,Monitor 怎么用呢 解释一下 示例 设置获取锁的时效 1,Lock lock 用于读一个引用类型进行加锁,同一时刻内只有一个线程能够访问此对象.lock 是语法糖,是通过 Monitor 来实现的. Lock 锁定的对象,应该是静态的引用类型(字符串除外). 实际上字符串也可以作为锁的对象使用,只是由于字符串对象的特殊性,可能会造成不同位置的不同线程冲突.如果你能保证字符串的唯一性,例如 Guid 生成的字符串,也是可以作为锁的对

  • C#多线程系列之线程等待

    目录 前言 volatile 关键字 三种常用等待 再说自旋和阻塞 SpinWait 结构 属性和方法 自旋示例 新的实现 SpinLock 结构 属性和方法 示例 等待性能对比 前言 volatile 关键字 volatile 关键字指示一个字段可以由多个同时执行的线程修改. 我们继续使用<C#多线程(3):原子操作>中的示例: static void Main(string[] args) { for (int i = 0; i < 5; i++) { new Thread(AddO

随机推荐