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

目录
  • 判断任务状态
  • 再说父子任务
  • 组合任务/延续任务
  • 复杂的延续任务
  • 并行(异步)处理任务
  • 并行(同步)处理任务
  • 并行任务的 Task.WhenAny
  • 并行任务状态
  • 循环中值变化问题
  • 定时任务 TaskScheduler 类

判断任务状态

属性 说明
IsCanceled 获取此 Task 实例是否由于被取消的原因而已完成执行。
IsCompleted 获取一个值,它表示是否已完成任务。
IsCompletedSuccessfully 了解任务是否运行到完成。
IsFaulted 获取 Task是否由于未经处理异常的原因而完成。
Status 获取此任务的 TaskStatus。

要检测一个任务是否出错(指任务因未经处理的异常而导致工作终止),要使用 IsCanceled 和 IsFaulted 两个属性,只要任务抛出异常,IsFaulted 为 true。但是取消任务本质是抛出 OperationCancelExcetion 异常,不代表任务出错。

即使任务抛出了未经处理的异常,也算是完成了任务,因此 IsCompleted 属性,会为 true。

示例如下:

代码有点多,不易观察,请复制到程序中运行。

    class Program
    {
        static void Main()
        {
            // 正常任务
            Task task1 = new Task(() =>
            {
            });
            task1.Start();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            GetResult(task1.IsCanceled, task1.IsFaulted);
            Console.WriteLine("任务是否完成:" + task1.IsCompleted);
            Console.WriteLine("-------------------");

            // 异常任务
            Task task2 = new Task(() =>
            {
                throw new Exception();
            });
            task2.Start();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            GetResult(task2.IsCanceled, task2.IsFaulted);
            Console.WriteLine("任务是否完成:" + task2.IsCompleted);
            Console.WriteLine("-------------------");
            Thread.Sleep(TimeSpan.FromSeconds(1));

            CancellationTokenSource cts = new CancellationTokenSource();
            // 取消任务
            Task task3 = new Task(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(3));
            }, cts.Token);
            task3.Start();
            cts.Cancel();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            GetResult(task3.IsCanceled, task3.IsFaulted);
            Console.WriteLine("任务是否完成:" + task3.IsCompleted);
            Console.ReadKey();
        }

        public static void GetResult(bool isCancel, bool isFault)
        {
            if (isCancel == false && isFault == false)
                Console.WriteLine("没有异常发生");
            else if (isCancel == true)
                Console.WriteLine("任务被取消");
            else
                Console.WriteLine("任务引发了未经处理的异常");
        }
    }

再说父子任务

在上一篇文章中《C#多线程(13):任务基础①》,我们学习了父子任务,父任务需要等待子任务完成后才算完成任务。

上一章只是给出示例,没有明确说明场景和实验结果,这里重新写一个示例来补充。

非父子任务:

外层任务不会等待内嵌的任务完成,直接完成或返回结果。

        static void Main()
        {
            //两个任务没有从属关系,是独立的
            Task<int> task = new Task<int>(() =>
            {
                // 非子任务
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine("     内层任务1");
                        Thread.Sleep(TimeSpan.FromSeconds(0.5));
                    }
                });
                task1.Start();
                return 666;
            });
            task.Start();
            Console.WriteLine($"任务运算结果是:{task.Result}");
            Console.WriteLine("\n-------------------\n");
            Console.ReadKey();
            }

父子任务:

父任务等待子任务完成后,才能算完成任务,然后返回结果。

        static void Main()
        {
            // 父子任务
            Task<int> task = new Task<int>(() =>
            {
                // 子任务
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine("     内层任务1");
                        Thread.Sleep(TimeSpan.FromSeconds(0.5));
                    }
                }, TaskCreationOptions.AttachedToParent);
                task1.Start();

                Console.WriteLine("最外层任务");
                return 666;
            });

            task.Start();
            Console.WriteLine($"任务运算结果是:{task.Result}");
            Console.WriteLine("\n-------------------\n");

            Console.ReadKey();
        }

组合任务/延续任务

Task.ContinueWith() 方法创建一个在 任务(Task)实例 完成时异步执行的延续任务。

Task.ContinueWith() 的重载方法非常多,可以参考:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.continuewith?view=netcore-3.1#--

这里我们使用的构造函数定义如下:

public Task ContinueWith(Action<Task> continuationAction);

一个简单的示例:

            Task task = new Task(() =>
            {
                Console.WriteLine("     第一个任务");
                Thread.Sleep(TimeSpan.FromSeconds(2));
            });

            // 接下来第二个任务
            task.ContinueWith(t =>
            {
                Console.WriteLine($"     第二个任务}");
                Thread.Sleep(TimeSpan.FromSeconds(2));
            });
            task.Start();

一个任务(Task) 是可以设置多个延续任务的,这些任务是并行的,例如:

        static void Main()
        {
            Task task = new Task(() =>
            {
                Console.WriteLine("     第一个任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            });

            // 任务①
            task.ContinueWith(t =>
            {
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine($"    任务① ");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            });

            // 任务②
            task.ContinueWith(t =>
            {
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine($"     任务②");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            });

            // 任务① 和 任务② 属于同级并行任务

            task.Start();
            }

通过多次实现延续/组合任务,会实现强有力的任务流程。

复杂的延续任务

经过上一小节,我们学习了 ContinueWith() 来延续任务,现在我们来学习更多的重载方法,实现更加复杂的延续。

ContinueWith() 重载方法很多,它们的参数都含有下面几种参数之一或多个。

  • continuationAction

    类型:Action 或 Func

一个要执行的任务。

  • state

    类型:Object

给延续任务传递的参数。

  • cancellationToken

    类型:CancellationToken

取消标记。

  • continuationOptions

    类型:TaskContinuationOptions

控制延续任务的创建和特性。

  • scheduler

    类型:TaskScheduler

要与延续任务关联并用于其执行过程的 TaskScheduler。

前面四个参数(类型),在以往的文章中已经出现过,这里就不再赘述;TaskScheduler 类型,这里先讲解,后面再说。

注意 TaskCreationOptions 和 TaskContinuationOptions 的区别,在前一篇我们学习过 TaskCreationOptions。这里来学习 TaskContinuationOptions 。

TaskContinuationOptions 可以在以下重载上使用:

ContinueWith(Action, CancellationToken, TaskContinuationOptions, TaskScheduler)
ContinueWith(Action>, TaskContinuationOptions

在延续中,这样使用是无效的:

            Task task = new Task(() =>
            {
                Console.WriteLine("     第一个任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            });
            task.ContinueWith(t =>
            {
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine($"    任务① ");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            },TaskContinuationOptions.AttachedToParent);

因为 TaskContinuationOptions 需要有嵌套关系的父子任务,才能生效。

正确使用方法:

        static void Main()
        {
            // 父子任务
            Task<int> task = new Task<int>(() =>
            {
                // 子任务
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine("     内层任务1");
                    Thread.Sleep(TimeSpan.FromSeconds(0.5));
                }, TaskCreationOptions.AttachedToParent);

                task1.ContinueWith(t =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine("内层延续任务,也属于子任务");
                    Thread.Sleep(TimeSpan.FromSeconds(0.5));
                }, TaskContinuationOptions.AttachedToParent);

                task1.Start();

                Console.WriteLine("最外层任务");
                return 666;
            });

            task.Start();
            Console.WriteLine($"任务运算结果是:{task.Result}");
            Console.WriteLine("\n-------------------\n");

            Console.ReadKey();
        }

并行(异步)处理任务

这里我们来学习 Task.WhenAll() 方法的使用。

Task.WhenAll() :等待提供的所有 Task 对象完成执行过程

使用示例如下:

        static void Main()
        {
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < 5; i++)
                tasks.Add(Task.Run(() =>
                {
                    Console.WriteLine($"任务开始执行");
                }));

            // public static Task WhenAll(IEnumerable<Task> tasks);

            // 相当于多个任务,生成一个任务
            Task taskOne = Task.WhenAll(tasks);
            // 不需要等待的话就去除
            taskOne.Wait();

            Console.ReadKey();
        }

Task taskOne = Task.WhenAll(tasks); 可以写成 Task.WhenAll(tasks);,返回的 Task 对象可以用来判断任务执行情况。

要注意,下面这样是无效的:

你可以修改上面的代码进行测试。

                tasks.Add(new Task(() =>
                {
                    Console.WriteLine($"任务开始执行");
                }));

我也不知道为啥 new Task() 不行。。。

如果任务有返回值,则可以使用下面这种方法

        static void Main()
        {
            List<Task<int>> tasks = new List<Task<int>>();

            for (int i = 0; i < 5; i++)
                tasks.Add(Task.Run<int>(() =>
                {
                    Console.WriteLine($"任务开始执行");
                    return new Random().Next(0,10);
                }));

            Task<int[]> taskOne = Task.WhenAll(tasks);

            foreach (var item in taskOne.Result)
                Console.WriteLine(item);

            Console.ReadKey();
        }

并行(同步)处理任务

Task.WaitAll():等待提供的所有 Task 对象完成执行过程。

我们来看看 Task.WaitAll() 其中一个重载方法的定义:

public static bool WaitAll (Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
  • tasks 类型:Task[]

要执行的所有任务。

  • millisecondsTimeout 任务:Int32

等待的毫秒数,-1 表示无限期等待。

  • cancellationToken 类型:CancellationToken

等待任务完成期间要观察的 CancellationToken。

Task.WaitAll() 的示例如下:

        static void Main()
        {
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < 5; i++)
                tasks.Add(Task.Run(() =>
                {
                    Console.WriteLine($"任务开始执行");
                }));

            Task.WaitAll(tasks.ToArray());

            Console.ReadKey();
        }

Task.WaitAll() 会让当前线程等待所有任务执行完毕。并且 Task.WaitAll() 是没有泛型的,也么没有返回结果。

并行任务的 Task.WhenAny

Task.WhenAny() 和 Task.WhenAll() 使用上差不多,Task.WhenAll() 当所有任务都完成时,才算完成,而 Task.WhenAny() 只要其中一个任务完成,都算完成。

这一点可以参考上面的 父子任务

参考使用示例如下:

        static void Main()
        {
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < 5; i++)
                tasks.Add(Task.Run(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(0, 5)));
                    Console.WriteLine("     正在执行任务");
                }));
            Task taskOne = Task.WhenAny(tasks);
            taskOne.Wait(); // 任意一个任务完成,就可以解除等待

            Console.WriteLine("有任务已经完成了");

            Console.ReadKey();
        }

当然,Task.WhenAny() 也有泛型方法,可以返回结果。

并行任务状态

Task.Status 属性可以获取任务的状态。其属性类型是一个 TaskStatus 枚举,其定义如下:

枚举 说明
Canceled 6 已经通过 CancellationToken 取消任务。
Created 0 该任务已初始化,但尚未被计划。
Faulted 7 由于未处理异常的原因而完成的任务。
RanToCompletion 5 已成功完成执行的任务。
Running 3 该任务正在运行,但尚未完成。
WaitingForActivation 1 该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。
WaitingForChildrenToComplete 4 该任务已完成执行,正在隐式等待附加的子任务完成。
WaitingToRun 2 该任务已被计划执行,但尚未开始执行。

在使用并行任务时,Task.Status 的值,有一定规律:

  • 如果有其中一个任务出现未经处理的异常,那么返回TaskStatus.Faulted
  • 如果所有任务都出现未经处理的异常,会返回 TaskStatus. RanToCompletion 
  • 如果其中一个任务被取消(即使出现未经处理的异常),会返回 TaskStaus.Canceled

循环中值变化问题

请运行测试下面两个示例:

        static void Main()
        {
            for (int i = 0; i < 5; i++)
                new Thread(() =>
                {
                    Console.WriteLine($"i = {i}");
                }).Start();

            Console.ReadKey();
        }
        static void Main()
        {
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < 5; i++)
                tasks.Add(Task.Run(() =>
                {
                    Console.WriteLine($"i = {i}");
                }));
            Task taskOne = Task.WhenAll(tasks);
            taskOne.Wait();

            Console.ReadKey();
        }

你会发现,两个示例的结果并不是 1,2,3,4,5,而是 5,5,5,5,5

这个问题称为 Race condition(竞争条件),可以参考维基百科:

https://en.wikipedia.org/wiki/Race_condition

微软文档里面也有关于此问题的说明,请参考:

https://docs.microsoft.com/zh-cn/archive/blogs/ericlippert/closing-over-the-loop-variable-considered-harmful

由于 i 在整个生命周期,内存都是在同一个位置,每个线程或任务对其值得使用,都是指向相同位置的。

这样就行了:

        static void Main()
        {
            for (int i = 0; i < 5; i++)
            {
                int tmp = i;
                new Thread(() =>
                {
                    Console.WriteLine($"i = {tmp}");
                }).Start();
            }

            Console.ReadKey();
        }

这样是无效的:

            for (int i = 0; i < 5; i++)
                new Thread(() =>
                {
                    int tmp = i;
                    Console.WriteLine($"i = {tmp}");
                }).Start();

定时任务 TaskScheduler 类

TaskScheduler 类:表示一个处理将任务排队到线程中的低级工作的对象。

网上大多数示例是 Wpf 、WinForm的,微软文档也是很复杂的样子: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskscheduler?view=netcore-3.1#properties

貌似 TaskScheduler 主要对 SynchronizationContext 进行控制,也就是说是对 UI 起作用。

到此这篇关于C#多线程系列之任务基础(二)的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

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

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

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

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

  • 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

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

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

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

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

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

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

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

    目录 TaskAwaiter 延续的另一种方法 另一种创建任务的方法 实现一个支持同步和异步任务的类型 Task.FromCanceled() 如何在内部取消任务 Yield 关键字 补充知识点 TaskAwaiter 先说一下 TaskAwaiter,TaskAwaiter 表示等待异步任务完成的对象并为结果提供参数. Task 有个 GetAwaiter() 方法,会返回TaskAwaiter 或TaskAwaiter<TResult>,TaskAwaiter 类型在 System.Run

  • 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#多线程系列之线程池

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

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

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

  • 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#多线程系列之原子操作

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

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

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

随机推荐