返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C#多线程系列之任务基础(三)
  • 710
分享到

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

2024-04-02 19:04:59 710人浏览 八月长安
摘要

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

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#多线程系列之任务基础(三)的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持编程网。

--结束END--

本文标题: C#多线程系列之任务基础(三)

本文链接: https://lsjlt.com/news/139059.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • C#多线程系列之任务基础(三)
    目录TaskAwaiter延续的另一种方法另一种创建任务的方法实现一个支持同步和异步任务的类型Task.FromCanceled()如何在内部取消任务Yield 关键字补充知识点Ta...
    99+
    2024-04-02
  • C#多线程系列之任务基础(一)
    目录多线程编程多线程编程模式探究优点任务操作两种创建任务的方式Task.Run() 创建任务取消任务父子任务任务返回结果以及异步获取返回结果捕获任务异常全局捕获任务异常多线程编程 多...
    99+
    2024-04-02
  • C#多线程系列之任务基础(二)
    目录判断任务状态再说父子任务组合任务/延续任务复杂的延续任务并行(异步)处理任务并行(同步)处理任务并行任务的 Task.WhenAny并行任务状态循环中值变化问题定时任务 Task...
    99+
    2024-04-02
  • c#多线程之线程基础
    目录一、简介二、创建线程三、暂停线程四、线程等待五、终止线程六、检测线程状态七、线程优先级八、前台线程和后台线程九、向线程传递参数十、使用C# Lock 关键字十一、使用Monito...
    99+
    2024-04-02
  • C#多线程系列之线程池
    目录线程池ThreadPool 常用属性和方法线程池说明和示例线程池线程数线程池线程数说明不支持的线程池异步委托任务取消功能计时器线程池 线程池全称为托管线程池,线程池受 .NET ...
    99+
    2024-04-02
  • MySQL系列之三 基础篇
    目录系列教程一、MySQL简介二、MySQL的发展历史三、MariaDB的基本使用1、基本安装与配置2、客户端命令:mysql3、其他客户端工具4、安全加强脚本 mysql_secu...
    99+
    2024-04-02
  • C#多线程系列之线程通知
    AutoRestEvent 类用于从一个线程向另一个线程发送通知。 微软文档是这样介绍的:表示线程同步事件在一个等待线程释放后收到信号时自动重置。 其构造函数只有一个: 构造函数里面...
    99+
    2024-04-02
  • C#多线程系列之线程等待
    目录前言volatile 关键字三种常用等待再说自旋和阻塞SpinWait 结构属性和方法自旋示例新的实现SpinLock 结构属性和方法示例等待性能对比前言 volatile 关键...
    99+
    2024-04-02
  • C#多线程系列之线程完成数
    解决一个问题 假如,程序需要向一个 Web 发送 5 次请求,受网路波动影响,有一定几率请求失败。如果失败了,就需要重试。 示例代码如下: class Program ...
    99+
    2024-04-02
  • C#多线程学习之基础入门
    目录同步方式异步多线程方式异步多线程优化异步回调异步信号量异步多线程返回值异步多线程返回值回调线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进...
    99+
    2024-04-02
  • C#多线程系列之多线程锁lock和Monitor
    目录1,Locklock 原型lock 编写实例2,Monitor怎么用呢解释一下示例设置获取锁的时效1,Lock lock 用于读一个引用类型进行加锁,同一时刻内只有一个线程能够访...
    99+
    2024-04-02
  • C#多线程系列之多阶段并行线程
    前言 这一篇,我们将学习用于实现并行任务、使得多个线程有序同步完成多个阶段的任务。 应用场景主要是控制 N 个线程(可随时增加或减少执行的线程),使得多线程在能够在 M 个阶段中保持...
    99+
    2024-04-02
  • C#多线程系列之读写锁
    本篇的内容主要是介绍 ReaderWriterLockSlim 类,来实现多线程下的读写分离。 ReaderWriterLockSlim ReaderWriterLock 类:定义支...
    99+
    2024-04-02
  • C#多线程系列之手动线程通知
    区别与示例 AutoResetEvent 和 ManualResetEvent 十分相似。两者之间的区别,在于前者是自动(Auto),后者是手动(Manua)。 你可以先运行下面的示...
    99+
    2024-04-02
  • C#多线程开发实战记录之线程基础
    目录前言线程基础 1、创建线程2、暂停线程3、线程等待4、线程终止C#中的lock关键字总结前言 最近由于工作的需要,一直在使用C#的多线程进行开发,其中也遇到了很多问题,但也都解决...
    99+
    2024-04-02
  • Java基础之多线程的三种实现方式
    目录一、前言二、继承Thread类实现多线程三、Runnable接口方式实现多线程四、Thread和Runnable的关系五、使用ExecutorService、Callable、F...
    99+
    2024-04-02
  • C#多线程系列之原子操作
    目录知识点竞争条件线程同步CPU时间片和上下文切换阻塞内核模式和用户模式Interlocked类1,出现问题2,Interlocked.Increment()3,Interlocke...
    99+
    2024-04-02
  • C#多线程系列之进程同步Mutex类
    Mutex 中文为互斥,Mutex 类叫做互斥锁。它还可用于进程间同步的同步基元。 Mutex 跟 lock 相似,但是 Mutex 支持多个进程。Mutex 大约比 lock 慢 ...
    99+
    2024-04-02
  • C#多线程系列之工作流实现
    目录前言节点ThenParallelScheduleDelay试用一下顺序节点并行任务编写工作流接口构建器工作流构建器依赖注入实现工作流解析前言 前面学习了很多多线程和任务的基础知识...
    99+
    2024-04-02
  • C#多线程系列之资源池限制
    Semaphore、SemaphoreSlim 类 两者都可以限制同时访问某一资源或资源池的线程数。 这里先不扯理论,我们从案例入手,通过示例代码,慢慢深入了解。 Semaphore...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作