走进Task(1):什么是Task
前言
本系列会拆分为以下几篇分次进行叙述:
- 什么是 Task(本文)
- Task 的回调执行与 await(TODO)
- async 到底干了什么(TODO)
- 总结与常见误区(TODO)
在 2 中,会和大家分享死锁相关的问题。2 和 3 中会穿插自定义 Awaitable 的话题。
本系列会直接引用前一篇博客概述 .NET 6 ThreadPool 实现 里的结论,所有请没看过的同学先麻烦看下。
文中所有例子均出于解释目的,并非具有实际意义的代码。有返回值的 Task 和无返回值的 Task 实际区别不是很大,下文大多数举例不做特别区分。不纠结 api 的使用细节,只讲 Task 的整体设计思路。
代码运行截图是在 .NET 6 中的,其他版本的设计没有大的改动,不影响学习。
笔者解读并非权威解读,只是希望能给大家一个理解 Task 的方法。
从表象讲起
Task 从何而来
以下仅做典型举例,并非全部
- new Task
new Task(_ =>
{
Console.WriteLine("Hello World!");
}, null).Start();
- TaskFactory.StartNew
new TaskFactory().StartNew(() =>
{
Console.WriteLine("Hello World!");
});
- Task.Run
Task.Run(() =>
{
Console.WriteLine("Hello World!");
});
- Task.FromResult 等直接创建一个已完成的 Task
Task.FromResult("Hello World!");
var task = Task.CompletedTask;
- 某个不知道其内部实现的 async 方法
async Task<Bar> FooAsync();
Task 常见用法
- 注册一个回调,等待 Task 执行完成时获取结果并执行回调
var task = Task.Run<string>(() => "Hello World!");
task.ContinueWith(t => Console.WriteLine(t.Result));
- await 一个 Task 并得到结果
var task = Task.Run<string>(() => "Hello World!");
var result = await task;
Console.WriteLine(result);
- 直接 GetResult
var task = Task.Run<string>(() => "Hello World!");
// 等效于 task.Result
var result = task.GetAwaiter().GetResult();
Console.WriteLine(result);
Task 的分类
按是否包含 Result 分,也就是是否是泛型 Task
Task
Task<T>
按得到 Task 的方式,可以分为
- 我知道这个 Task 是怎么来的,这种情况下,我们自己参与了 Task 的创建过程,知道这个 Task 是在干啥。比如:
Task task = Task.Run<int>(() => 1 + 2);
计算 1 + 2,并将结果作为 Task 的结果。
- 不知道这个 Task 是怎么来的。比如:
Task task = new HttpClient().GetStringAsync("http://localhost:5000/api/values");
而这两种获取方式的不同对应的是两种完全不同的侧重点:
- Task 是一个白盒,关注 Task 里干了什么,在哪执行里面这些代码。
- Task 是一个黑盒,关注 Task 能给到我什么,Task 完成执行之后,我该干什么。
对 Task 进行分解
按功能点可以将 Task 分为三个部分
- 任务执行:通过 Task.Run 等方式执行一段我们自定义的逻辑。
- 回调通知及回调执行:注册一个回调,等待 Task 完成时执行。
- await 语法支持:脱离了 await,task 的上述两个功能依旧可以完整执行。但却会丧失代码的简洁性。
Task 在哪执行?
线程池
Task 可以作为 ThreadPool 队列系统的基本单元被 ThreadPool 调度执行。
下面这些常见的创建 Task 的方式,默认情况都是在 ThreadPool 中被调度执行的,这几个本质上是一样的,只是使用方式上和可支持传入的自定义选项上的区别。
- new Task
new Task(_ =>
{
Console.WriteLine("Hello World!");
}, null).Start();
- TaskFactory.StartNew
new TaskFactory().StartNew(() =>
{
Console.WriteLine("Hello World!");
});
- Task.Run
// 可以看做简化版的 TaskFactory.StartNew
Task.Run(() =>
{
Console.WriteLine("Hello World!");
});
以 Task.Run
为例来看下里面到底做了些什么。
在 PortableThreadPool.TryCreateWorkerThread
和实际要要执行的 lambda 表达式中打上断点,我们便可以清晰的看到整个执行过程。
整理一下的话,主要就是这个样子,为简化理解,ThreadPool 中的调用细节已省略。
Task 关键代码摘录:
class Task
{
// 任务的主体,我们要执行的实际逻辑
// 可能有返回值,可能没有
internal Delegate m_action;
// 任务的状态
internal volatile int m_stateFlags;
// ThreadPool 调用入口,由于 JIT 的内联优化,调用栈里只能看到 ExecuteEntryUnsafe,看不到这个方法
internal virtual void ExecuteFromThreadPool(Thread threadPoolThread) => ExecuteEntryUnsafe(threadPoolThread);
internal void ExecuteEntryUnsafe(Thread? threadPoolThread)
{
// 设置 Task 状态为已经执行
m_stateFlags |= (int)TaskStateFlags.DelegateInvoked;
if (!IsCancellationRequested & !IsCanceled)
{
ExecuteWithThreadLocal(ref t_currentTask, threadPoolThread);
}
else
{
ExecuteEntryCancellationRequestedOrCanceled();
}
}
// 创建 Task 的时候可传入的数据,用于执行时使用
// new Task(state => Console.WriteLine(state), "Hello World").Start();
internal object? m_stateObject;
private void ExecuteWithThreadLocal(ref Task currentTaskSlot, Thread threadPoolThread = null)
{
// 执行上下文维护着代码执行逻辑上下文的一些数据,如 AsyncLocal
// 具体请看我的 AsyncLocal 博客 https://www.cnblogs.com/eventhorizon/p/12240767.html
ExecutionContext? ec = CapturedContext;
if (ec == null)
{
// 没有执行上下文,直接执行
InnerInvoke();
}
else
{
// 是否是在 ThreadPool 线程上执行
if (threadPoolThread is null)
{
ExecutionContext.RunInternal(ec, s_ecCallback, this);
}
else
{
ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, ec, s_ecCallback, this);
}
}
}
// 不管 ExecuteWithThreadLocal 分支如何,最后会走到 InnerInvoke
internal virtual void InnerInvoke()
{
if (m_action is Action action)
{
action();
return;
}
if (m_action is Action<object?> actionWithState)
{
actionWithState(m_stateObject);
}
}
}
可以看到 Task 以 ThreadPoolTaskScheduler 为媒介,进入了 ThreadPool。ThreadPool 调用 Task.ExecuteFromThreadPool 方法最终触发 Task 所封装的 action 的执行。
与 ThreadPool 中另一种基本单元 IThreadPoolWorkItem 一样,Task 在进入 ThreadPoolWorkQueue 时会有两种可能,进入全局队列或者本地队列。
理解这个问题,我们需要看一下 ThreadPoolTaskScheduler.QueueTask 里做了些什么。
internal sealed class ThreadPoolTaskScheduler : TaskScheduler
{
protected internal override void QueueTask(Task task)
{
TaskCreationOptions options = task.Options;
if (Thread.IsThreadStartSupported && (options & TaskCreationOptions.LongRunning) != 0)
{
// 创建独立线程,和线程池无关
new Thread(s_longRunningThreadWork)
{
IsBackground = true,
Name = ".NET Long Running Task"
}.UnsafeStart(task);
}
else
{
// 第二个参数是 preferLocal
// options & TaskCreationOptions.PreferFairness 这个位标志的枚举用法可查看官方资料
// https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/enum#enumeration-types-as-bit-flags
ThreadPool.UnsafeQueueUserWorkItemInternal(task, (options & TaskCreationOptions.PreferFairness) == 0);
}
}
}
上面代码里的 TaskCreationOptions 是我们在创建 Task 的时候可以指定的一个选项,默认是 None。
Task.Run 不支持传入该选项,可使用 TaskFactory.StartNew
的重载进行指定:
new TaskFactory().StartNew(() =>
{
Console.WriteLine("Hello World!");
}, TaskCreationOptions.PreferFairness);
根据 TaskCreationOptions 的不同,出现了三个分支
- LongRunning:独立线程,和线程池无关
- 包含 PreferFairness时:preferLocal=false,进入全局队列
- 不包含 PreferFairness时:preferLocal=ture,进入本地队列
进入全局队列的任务能够公平地被各个线程池中的线程领取执行,也是就是 prefer fairness
这个词组的字面意思了。
下图中 Task666 先进入全局队列,随后被 Thread1 领走。Thread3 通过 WorkStealing 机制窃取了 Thread2 中的 Task2。
一个独立的后台线程中
也就是上文提到的创建 Task 时使用 TaskCreationOptions.LongRunning
,如果你需要一个执行一个长时间的任务,比如一段耗时很久的同步代码,就可以使用这个。执行异步代码(指 await xxx)时不推荐使用,后面会讲原因。
new TaskFactory().StartNew(() =>
{
// 耗时较长的同步代码
}, TaskCreationOptions.LongRunning);
ThreadPool 管理的线程是出于可复用的目的设计的,不停地从队列系统中领取任务执行。如果一个 WorkThread 阻塞在一个耗时较长的任务上,它就没办法处理其他任务,ThreadPool 的吞吐率会受影响。
当然并不意味着 ThreadPool 不能处理这样的任务。举个极端的例子,如果线程池目前的 WorkThread 全在处理 LongRunning Task。在 Starvation Avoidance 机制(每隔500ms)创建新的 WorkThread 之前,ThreadPool 没法执行新的任务。
LongRunning 的 Task 生命周期与 ThreadPool 设计目的不符合,因此需独立开来。
自定义的TaskScheduler里
除了 ThreadPoolTaskScheduler
外,我们还可以定义自己的 TaskScheduler
。
首先需要继承 TaskScheduler
这个抽象类,有三个抽象方法需要我们实现。
public abstract class TaskScheduler
{
// 入口,待调度执行的 Task 会通过该方法传入
protected internal abstract void QueueTask(Task task);
// 这个是在执行 Task 回调的时候才会被执行到的方法,放到后面再讲
protected abstract bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued);
// 获取所有调度到该 TaskScheduler 的 Task
protected abstract IEnumerable<Task>? GetScheduledTasks();
}
在我们自定义的 TaskScheduler 里,在 QueueTask 被执行时会拿到 Task,但是 Task 要怎么去触发里面的 action 呢。
Task 针对 ThreadPool 的调用场景暴露了一个 ExecuteFromThreadPool 的 internal 方法,同时也提供了一个 ExecuteEntry 方法供其他场景调用,但是这个方法也是 internal 的。只能通过 TaskScheduler 的 protect 方法进行间接调用。
public abstract class TaskScheduler
{
protected bool TryExecuteTask(Task task)
{
if (task.ExecutingTaskScheduler != this)
{
throw new InvalidOperationException(SR.TaskScheduler_ExecuteTask_WrongTaskScheduler);
}
return task.ExecuteEntry();
}
}
下面是一个自定义的 TaskScheduler,在一个固定的线程上顺序执行 Task。
```C#
class CustomTaskScheduler : TaskScheduler
{
private readonly BlockingCollection<Task> _queue = new();
public CustomTaskScheduler()
{
new Thread(() =>
{
while (true)
{
var task = _queue.Take();
Console.WriteLine($"task {task.Id} is going to be executed");
TryExecuteTask(task);
Console.WriteLine($"task {task.Id} has been executed");
}
})
{
IsBackground = true
}.Start();
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return _queue.ToArray();
}
protected override void QueueTask(Task task)
{
_queue.Add(task);
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
}
在 TaskFactory 的构造函数中可以传入我们自定义的 TaskScheduler
var taskFactory = new TaskFactory(new CustomTaskScheduler());
taskFactory.StartNew(() =>
Console.WriteLine($"task {Task.CurrentId}" +
$" threadId: {Thread.CurrentThread.ManagedThreadId}"));
taskFactory.StartNew(() =>
Console.WriteLine($"task {Task.CurrentId}" +
$" threadId: {Thread.CurrentThread.ManagedThreadId}"));
Console.ReadLine();
输出结果如下:
var taskFactory = new TaskFactory(new CustomTaskScheduler());
taskFactory.StartNew(() =>
Console.WriteLine($"task {Task.CurrentId}" +
$" threadId: {Thread.CurrentThread.ManagedThreadId}"));
taskFactory.StartNew(() =>
Console.WriteLine($"task {Task.CurrentId}" +
$" threadId: {Thread.CurrentThread.ManagedThreadId}"));
Console.ReadLine();
task 1 is going to be executed
task 1 threadId: 10
task 1 has been executed
task 2 is going to be executed
task 2 threadId: 10
task 2 has been executed
所有的 Task 都会在一个线程里被调度执行。
Task 可以封装任何类型的别的任务
上面两种情况,Task 都存在明确的执行实体,但有时候,可能是没有的。看下面这样的例子。
var task = FooAsync();
var action = typeof(Task).GetField("m_action", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(task);
Console.WriteLine($"Task action is null: {action == null}");
task.ContinueWith(t => Console.WriteLine(t.Result));
// 回调可以注册多个
task.ContinueWith(t => Console.WriteLine(t.Result));
Task<string> FooAsync()
{
var tsc = new TaskCompletionSource<string>();
new Thread(() =>
{
Thread.Sleep(1000);
tsc.SetResult("Hello World");
})
{
IsBackground = true
}.Start();
return tsc.Task;
}
输出:
Task action is null: True
Hello World
Hello World
从 FooAsync 外部和内部两个角度来看这个问题
- FooAsync 外:拿到了一个 Task 并注册了回调
- FooAsync 内:相当于间接的持有了这个回调,并通过 tsc.SetResult 间接地调用了这个回调。
下面是关键代码的摘录
class Task<T>
{
// 保存一个或一组回调
private volatile object? m_continuationObject;
internal bool TrySetResult(TResult result)
{
// ...
this.m_result = result;
FinishContinuations();
// ...
}
internal void FinishContinuations()
{
// 处理回调的执行
}
}
public class TaskCompletionSource<TResult>
{
public TaskCompletionSource() => _task = new Task<TResult>();
public Task<TResult> Task => _task;
public void SetResult(TResult result)
{
TrySetResult(result);
}
public bool TrySetResult(TResult result)
{
_task.TrySetResult(result);
// ...
}
}
有时候 Task.TrySetResult() 的触发源可能是一个异步IO完成事件导致的,也就是我们常说的异步IO,硬件有自己的处理芯片,在异步IO完成通知CPU(硬件中断 hardware interrupt)之前,CPU并不需要参与,这也是异步IO的价值所在。
小结
Task 是个已经完成或者将在未来某个时间点完成的任务,可以向其注册一个回调等待任务完成时被执行。
走进Task(1):什么是Task的更多相关文章
- Task.Factory.StartNew和Task.Run
在系统中单开线程进行操作,经常用到Task,发现Task主要有以下两种方法 Task.Factory.StartNew(() => { }); Task.Run(() => { }); 初 ...
- 【转载】hadoop之failed task任务和killed task任务
failed task可理解为自杀,也就是task本身出了问题而自杀:killed task可理解为是他杀,也就是jobtracker认为这个任务的执行是多余的,所以把任务直接杀掉.起初用hadoop ...
- 捕获Task.WhenALl返回的Task的Exception
如果有一个任务抛出异常,则Task.WhenAll 会出错,并把这个异常放在返回的Task 中.如果多个任务抛出异常,则这些异常都会放在返回的Task 中.但是,如果这个Task 在被await 调用 ...
- .NET 中 如果一个Task A正在await另一个Task B,那么Task A是什么状态
新建一个.NET Core控制台程序,输入如下代码: using System; using System.Threading; using System.Threading.Tasks; class ...
- await Task.Yield()和await Task.CompletedTask有什么不同
有时候我们在代码中要执行一些非常耗时的操作,我们不希望这些操作阻塞调用线程(主线程)的执行,因为调用线程(主线程)可能还有更重要的工作要做,我们希望将这些非常耗时的操作由另外一个线程去执行,这个时候就 ...
- C#异步编程のTask模型返回值Task<TResult>应用
文中所有Task<TResult>的返回值都是直接用task.result获取,这样如果后台任务没有执行完毕的话,主线程会等待其执行完毕,这样的话就和同步一样了(看上去一样,但其实awai ...
- 大数据学习day22------spark05------1. 学科最受欢迎老师解法补充 2. 自定义排序 3. spark任务执行过程 4. SparkTask的分类 5. Task的序列化 6. Task的多线程问题
1. 学科最受欢迎老师解法补充 day21中该案例的解法四还有一个问题,就是当各个老师受欢迎度是一样的时候,其排序规则就处理不了,以下是对其优化的解法 实现方式五 FavoriteTeacher5 p ...
- 走进Task(2):Task 的回调执行与 await
目录 前言 Task.ContinueWith ContinueWith 的产物:ContinuationTask 额外的参数 回调的容器:TaskContinuation Task.Continue ...
- .Net多线程编程—任务Task
1 System.Threading.Tasks.Task简介 一个Task表示一个异步操作,Task的创建和执行是独立的. 只读属性: 返回值 名称 说明 object AsyncState 表示在 ...
随机推荐
- LuoguP4263 [Code+#3]投票统计 题解
Content 有 \(t\) 组询问,每组询问给定一个长度为 \(n\) 的数列,请将出现次数最多的数按照从小到大的顺序输出,或者这些数在数列中出现的次数都相等. 数据范围:\(t\) 未知,\(n ...
- Python 的元类设计起源自哪里?
一个元老级的 Python 核心开发者曾建议我们( 点击阅读),应该广泛学习其它编程语言的优秀特性,从而提升 Python 在相关领域的能力.在关于元编程方面,他的建议是学习 Hy 和 Ruby.但是 ...
- 『学了就忘』Linux日志管理 — 93、日志轮替补充
目录 1.把自己的日志加入日志轮替 (1)操作方式 (2)示例 2.logrotate命令 1.把自己的日志加入日志轮替 使用RPM包方式安装服务的日志会自动的加入logrotate轮替,一般不需要你 ...
- AcWing429. 奖学金
题目: 某小学最近得到了一笔赞助,打算拿出其中一部分为学习成绩优秀的前5名学生发奖学金. 期末,每个学生都有3门课的成绩:语文.数学.英语. 先按总分从高到低排序,如果两个同学总分相同,再按语文成绩从 ...
- JAVA获取指定日期是星期几
/** * 获取指定日期是星期几<br> * * @param date * @return 指定日期是星期几 */ public static String getWeekOfDate( ...
- springboot目录结构、重要配置文件、重要注解的详解
前面2篇博客已经带着大家搭建了springboot项目,并编写了持久化接口部署到tomcat下访问.这里我们一起补充下springboot的基本信息 一.springboot简单介绍 springbo ...
- 网络编程之UDP(3)丢包总结
读书笔记 from here UDP socket缓冲区满造成的UDP丢包 如果socket缓冲区满了,应用程序没来得及处理在缓冲区中的UDP包,那么后续来的UDP包会被内核丢弃,造成丢包.在sock ...
- 使用.NET 6开发TodoList应用(8)——实现全局异常处理
系列导航 使用.NET 6开发TodoList应用文章索引 需求 因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在Controller里就需要进行完整的try-catch捕获,并根据是否 ...
- 【LeetCode】1079. Letter Tile Possibilities 解题报告 (C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 回溯 日期 题目地址:https://leetcode ...
- 【LeetCode】438. Find All Anagrams in a String 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 滑动窗口 双指针 日期 题目地址:https://l ...