目录

前言

原文是Stephen Cleary的系列博客 https://blog.stephencleary.com/2014/04/a-tour-of-task-part-0-overview.html

我很容易迷失在Task,TPL,Async中,经常需要翻文章慢慢捋,索性做个合集争取一次性把Task的方方面面都涉及到。

1,Task的分类

Task分为两类,一类叫Delegate Task,一类叫Promise Task。

  • Delegate Task:包含要运行的代码的任务。在TPL(任务并行库)中,大多数任务都是Delegate Task(对Promise Task有一些支持)。进行并行处理时,各种Delegate Task分配给不同的线程,然后由这些线程实际执行任务中的代码。

  • Promise Task:表示某种事件或信号的任务,通常表示基于I/O事件或信号(比如“HTTP下载已完成”或者“10秒钟计时已到”)。在异步中,大多数任务都是Promise Task(对Delegate Task有一些支持)。注意Promise Task执行时,并没有线程的参与,代码只是在等待系统完成Promise Task的执行。

有时候把Delegate Task称为code-based Task,把Promise Task称为event-based Task,意思差不多。

2,Task的状态

2.1 TaskStatus枚举

如果将Task看作一个状态机,其Status属性则表示当前状态。Status属性的类型是TaskStatus枚举,它的枚举值如下:

枚举值 描述
Created
这是通过Task构造函数创建的任务的初始状态。处于此状态的任务会保持该状态,直到启动或者取消任务
WaitingForActivation 这是通过ContinueWith、ContinueWhenAll、ContinueWhenAny、 FromAsync等方法或者从TaskCompletionSource创建的任务的初始状态。 该任务尚未被分配(not scheduled),并且在相关操作完成之前都不会被分配
WaitingToRun 任务被分配到TaskScheduler,正在等待TaskScheduler的选取与执行。这是通过 TaskFactory.StartNew创建的任务的初始状态,当StartNew返回任务时,它已经被分配好了,因此状态至少是WaitingToRun(说至少是因为当StartNew返回任务时,任务可能已经处于Running甚至RanToCompletion)
Running 任务正在执行
WaitingForChildrenToComplete 当任务已完成其自身代码的执行,它就会离开Running状态。如果任务有子项,那么任务在其附加的子项完成之前不会被视为已完成,而是进入此状态
RanToCompletion 三个最终状态之一,任务已成功运行到代码结束
Canceled 三个最终状态之一,任务必须在开始执行之前或在执行期间响应取消请求,才能处于取消状态
Faulted 三个最终状态之一,任务执行自身代码时出现未处理的异常或者其子项处于Faulted状态

两种不同类型的任务具有不同的状态机路径。

  • 对于Delegate Task



    大多数情况下,Delegate Task是由Task.Run或者Task.Factory.StartNew创建,一上来就处于WaitingToRun状态了。 当Delegate Task实际开始执行时,任务就处于Running状态。Task完成时,如果有子项任务,则进入WaitingForChildrenToComplete状态等待子项任务。最后Task进入三个最终状态之一,RanToCompletion(成功运行), Faulted或者Canceled

    由于Delegate Task表示包含运行代码的任务,整个过程可能会很快,可能导致看不到其中的一个或多个状态。例如将一个简短的任务分配给线程池,当任务返回时它可能已经处于RanToCompletion状态了。

  • 对于Promise Task



    Promise Task的状态机要简单一些。Promise Task通常表示基于I/O事件或信号,这些基于I/O的操作正在执行时(比如“HTTP下载正在进行”或者“10秒钟计时正在进行”),实际上并没有执行CPU代码(而是交给了系统),因此永远不会进入WaitingToRun或者Running状态。没错,Promise Task可能直接就从WaitingForActivationRanToCompletion了,不经过Running。Promise Task创建时就开始执行了,让人困惑的是这种“执行中”的状态被居然称作WaitingForActivation,不知道微软怎么想的。

2.2 状态相关属性

Task有3个与状态相关属性

bool IsCompleted { get; }
bool IsCanceled { get; }
bool IsFaulted { get; }

IsCanceledIsFaulted很简单,直接判断当前状态是否Canceled或者FaultedIsCompleted表示当前状态是否是三个最终状态之一。

2.3 小结

尽管这些状态很有趣,但在实际编程中几乎用不到(除了调试代码时)。异步编程和并行编程都不怎么关心这些状态,通常都是等待任务完成并提取结果。

3,Task的等待

Task的等待会造成调用线程的阻塞,直到Task完成。因此Promise Task几乎不使用等待,等待Promise Task是造成死锁的常见原因。可见等待几乎是Delegate Task的专用(比如等待Task.Run返回的Task)。

3.1 Wait方法

下面列举几种常见的方法重载

bool Wait(int timeout, CancellationToken token);  //等待一个任务
bool WaitAll(params Task[], int timeout, CancellationToken token); //等待所有任务
int WaitAny(params Task[], int timeout, CancellationToken token); //等待任一任务 //其他的等待,比如void Wait(),void WaitAll(params Task[]),int WaitAny(params Task[])最终也是调用上述方法,不再赘述

等待其实相当简单,阻塞调用线程直到Task,直到等待发生超时、等待被取消或任务完成。

如果等待发生超时,则返回false或-1。

如果等待被取消,则引发OperationCanceledException

如果任务在FaultedCanceled状态下完成,则会将任何异常包装到AggregateException中。

需要注意的是,任务取消和等待取消都会引发OperationCanceledException,区别在于任务取消的OperationCanceledException被封装在AggregateException中,而等待取消的OperationCanceledException是直接抛出的。

大多数时候,Task.Wait是危险的,它可能会造成死锁。只在少数情况下我们会使用Task.Wait,比如一个控制台应用的Main方法有异步工作要做,但希望主线程同步阻塞,直到完成该工作时。

3.2 死锁

3.2.1 死锁形成

下面是一个Winform应用的死锁案例

public static async Task<JObject> GetJsonAsync(Uri uri)
{
// real-world code shouldn't use HttpClient in a using block; this is just example code
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return JObject.Parse(jsonString);
}
} public void Button1_Click(...)
{
var jsonTask = GetJsonAsync(...);
textBox1.Text = jsonTask.Result; //效果相当于jsonTask.Wait();
}

点击Button1后代码就死锁了。死锁是如何发生的呢?

  1. Button1_Click方法在UI上下文调用GetJsonAsync方法。
  2. GetJsonAsync方法在UI上下文调用GetStringAsync方法,GetStringAsync返回一个未完成任务(任务1)。
  3. GetJsonAsync开始等待GetStringAsync返回的未完成任务(任务1)。在等待之前GetJsonAsync捕获了UI上下文,将用于任务完成后的继续运行。同时GetJsonAsync也返回一个未完成任务(任务2)给Button1_Click
  4. Button1_Click执行到jsonTask.Result,阻塞UI上下文所在线程等待任务2的完成。
  5. 过了一会,GetStringAsync执行完成了,GetJsonAsync方法需要恢复到之前捕获的UI上下文中继续运行。但此时UI上下文已经被阻塞,无法让GetJsonAsync方法继续,死锁。

3.3.2 死锁避免

有2个办法

  1. 在被调用的方法中,使用ConfigureAwait(false)
public static async Task<JObject> GetJsonAsync(Uri uri)
{
// real-world code shouldn't use HttpClient in a using block; this is just example code
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
return JObject.Parse(jsonString);
}
}

await关键字有切换线程的功能,ConfigureAwait(false)的意思是不要切换线程,避免了上下文的延续。

在此案例中避免了GetJsonAsync方法在先前捕获的UI上下文中继续执行,而是在线程池线程中继续执行,这样就和Button1_Click不冲突了。

但是使用ConfigureAwait(false)并不是最好的办法,因为如果Button1_Click调用了很多异步方法,岂不是要把这些方法都修改一遍?最好的办法还是在调用端不要阻止异步方法。

  1. 不要等待Task,使用async/await
public async void Button1_Click(...)
{
var json = await GetJsonAsync(...);
textBox1.Text = json;
}

感觉刚开始学的人都知道要这么写,标准做法。

4,Task的结果

4.1 Result

Task<T>类才有成员变量ResultTask类没有

T Result { get; }

Wait一样,Result将同步阻塞调用线程,直到任务完成。这通常不是一个好主意,原因同上:容易导致死锁。

此外,Result会将任何任务异常包装在AggregateException中,这通常会使异常处理变得复杂。

4.2 GetAwaiter().GetResult()

Task<T> task = ...;
T result = task.GetAwaiter().GetResult();

效果和Result是类似的,和Result也存在同样的问题:容易导致死锁。与Result的区别在于发生异常时不会将任务异常包装在AggregateException中,而是直接抛出。

4.3 await关键字

从Promise Task获取结果的最佳方式就是使用await关键字。await以最良性的方式检索任务结果,异步等待结果(不会阻塞),返回成功任务的结果(如果有的话),任务失败时直接抛出异常而不是封装在AggregateException

绝大多数情况下,应该使用await,而不是Wait, Result, 或者GetAwaiter().GetResult()

5,Task的继续

继续即Continuation,Continuation是一个附加到任务的委托,当任务完成时,就会分配资源来执行附加的委托。被附加的任务被称为“先行任务”(Antecedent Task)。

Continuation非常重要,它不会阻塞任何线程,它其实就是异步的本质,抛开事实不谈,await关键字在某种程度上可以理解为封装了Continuation的语法糖。

5.1 ContinueWith方法

附加Continuation到Task最底层的方式就是ContinueWith方法,下面列举几种常见的方法重载

//Task类的ContinueWith方法
//先行任务和附加委托都没有返回值
Task ContinueWith(Action<Task>, CancellationToken, TaskContinuationOptions, TaskScheduler);
//先行任务没有返回值,附加委托有返回值
Task<TResult> ContinueWith<TResult>(Func<Task, TResult>, CancellationToken, TaskContinuationOptions, TaskScheduler); //Task<TResult>类的ContinueWith方法
//先行任务有返回值,附加委托没有返回值
Task ContinueWith(Action<Task<TResult>>, CancellationToken, TaskContinuationOptions, TaskScheduler);
//先行任务和附加委托都有返回值
Task<TContinuationResult> ContinueWith<TContinuationResult>(Func<Task<TResult>, TContinuationResult>, CancellationToken, TaskContinuationOptions, TaskScheduler); //其他的继续,比如Task ContinueWith(Action<Task>),Task<TResult> ContinueWith<TResult>(Func<Task, TResult>)最终也是调用上述方法,不再赘述

下面是一个调用ContinueWith方法的例子

public void ContinueWithOperation()
{
Task<string> t = Task.Run(() =>
{
Thread.Sleep(1000); //模拟耗时操作
return "hello world";
});
//先行任务有返回值,附加委托没有返回值,对应Task ContinueWith(Action<Task<TResult>>
Task t2 = t.ContinueWith((t1) =>
{
Thread.Sleep(1000); //模拟耗时操作
Console.WriteLine(t1.Result);
});
}

tt1就是先行任务,同一个东西。t2就是继续任务。

ContinueWith(Action<Task<TResult>>)方法最终调用的是Task ContinueWith(Action<Task<TResult>>, CancellationToken, TaskContinuationOptions, TaskScheduler),在此说明一下方法的几个参数。

  • Action<Task<TResult>>:即附加委托
  • CancellationToken:如果在执行附加委托之前响应取消,那么附加委托将永远不会执行,但是如果附加委托已经开始执行,取消就没用了,这可能有一些误导性,换句话说,取消只是取消了附加委托的分配(scheduling),而不是附加委托本身。可以参考另一篇专门写取消的文章C#基础 - Cancellation
  • TaskContinuationOptions:选项集合,这些选项与Continuation的条件、分配和附加有关。
  • TaskScheduler:负责Continuation分配的任务分配器。遗憾的是,此参数的默认值不是TaskScheduler.Default,而是 TaskScheduler.Current,这个设定多年来引起了非常多的混乱,不知道微软怎么想的。因为绝大多数时候,开发者是按照TaskScheduler.Default来做的开发,因此建议调用ContinueWith方法时指定你期望的TaskScheduler。(这里插一句,Task.Factory.StartNew也存在参数默认值是TaskScheduler.Current的问题,后面再详细讲)

总之ContinueWith是个很底层的方法,除非你需要实现动态任务并行性(dynamic task parallelism),否则都应该用await关键字,而不是ContinueWith方法。

5.2 其他方法

  • TaskFactory.ContinueWhenAny:效果和ContinueWith差不多,不过是一组先行任务中的任何一个完成时开启Continuation。
  • TaskFactory.ContinueWhenAll:效果和ContinueWith差不多,不过是所有先行任务中都完成时开启Continuation。

同样也应该使用await关键字,比如await Task.WhenAny(...)await Task.WhenAll(...),而不是TaskFactory.ContinueWhenAnyTaskFactory.ContinueWhenAll方法。

var client = new HttpClient();
string[] results = await Task.WhenAll(
client.GetStringAsync("http://example.com"),
client.GetStringAsync("http://microsoft.com"));
// results[0] has the HTML of example.com
// results[1] has the HTML of microsoft.com
var client = new HttpClient();
Task<string> downloadFastTask = client.GetStringAsync("http://fast.com");
Task<string> downloadSlowTask = client.GetStringAsync("http://slow.com");
Task completedTask = await Task.WhenAny(downloadFastTask, downloadSlowTask);
Debug.Assert(completedTask == downloadFastTask);

6,Task的启动

使用Task构造函数创建出任务时,任务处于Created状态,处于此状态的任务会保持该状态,直到启动或者取消任务。

注意:做开发时基本上不会用到Task构造函数,如果不是出于学习目的,这一章可以直接跳过。

6.1 Start方法

有两个方法重载

void Start();
void Start(TaskScheduler);

Start方法只能由Task构造函数创建出的任务调用,且只有Delegate Task才能使用构造函数创建出来。一旦调用了Start方法,任务进入WaitingToRun状态(永远不会返回Created状态),所以Start方法只能调用一次。做开发时创建任务用Task.Run就好,别用Task构造函数。

6.2 RunSynchronously方法

RunSynchronouslyStart非常相似,有两个方法重载。比Start还冷门,更加不会用到。。

void RunSynchronously();
void RunSynchronously(TaskScheduler);

7,Delegate Task

看看开发中创建Delegate Task的主流方式。

7.1 TaskFactory.StartNew

首先介绍的就是被过度使用的TaskFactory.StartNew方法,下面列举几种常见的方法重载

Task StartNew(Action, CancellationToken, TaskCreationOptions, TaskScheduler);
Task<TResult> StartNew<TResult>(Func<TResult>, CancellationToken, TaskCreationOptions, TaskScheduler); //其他的StartNew,比如Task StartNew(Action),Task<TResult> StartNew<TResult>(Func<TResult>);最终也是调用上述方法,不再赘述

StartNew方法传入一个委托(Action或者Func),返回一个对应的任务。注意传入的委托不能是异步感知委托(async-aware delegates),因为使用StartNew启动异步任务会导致复杂性(TaskFactory.StartNew不支持异步感知委托,但是Task.Run支持哦)。

StartNew方法的参数默认值均来自TaskFactory实例。比如使用Task StartNew(Action)到最终调用Task StartNew(Action, CancellationToken, TaskCreationOptions, TaskScheduler)时,CancellationToken参数的实参是TaskFactory.CancellationToken

TaskCreationOptions参数的实参是TaskFactory.CreationOptionTaskScheduler参数的实参是TaskFactory.Scheduler。下面讲一下这几个参数。

7.1.1 CancellationToken

传递给StartNewCancellationToken仅在委托开始执行之前有效。换句话说,它用于取消委托的启动,而不是委托本身。一旦该委托开始执行,就不能用它来取消该委托。

如果想要取消委托本身,那么需要在委托中显式使用CancellationToken(比如调用CancelToken.ThrowIfCancelRequest)。

总之,StarNewCancellationToken参数几乎毫无用处。它的行为让许多开发者感到困惑。我自己从不使用它。

7.1.2 TaskCreationOptions

TaskCreationOptions是枚举类型

  • TaskCreationOptions.PreferFairness:以FIFO方式执行任务(尽量让先分配的任务先执行,后分配的后执行)。
  • TaskCreationOptions.LongRunning:长时间运行的任务(不使用线程池线程,而是新开一个独立的线程来执行任务)。
  • TaskCreationOptions.DenyChildAttach:禁止当前任务添加Continuation(Task.Run的默认行为)。
  • TaskCreationOptions.HideScheduler:执行任务时假装没有TaskScheduler。
  • TaskCreationOptions.RunContinuationsAsynchronously:强制任务的Continuation异步执行。
  • TaskCreationOptions.None:TaskFactory.StarNew的默认行为

7.1.3 TaskScheduler

TaskScheduler参数指定任务的分配者。TaskFactory有自己默认的TaskScheduler。但要注意TaskFactory默认的TaskScheduler不是TaskScheduler.Default,而是TaskScheduler.Current(重要的事情反复说)。

下面在winform里演示一下TaskScheduler.Current的效果。

private void Button_Click(object sender, EventArgs e)
{
TaskFactory factory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext()); //指定UI上下文的TaskScheduler
factory.StartNew(() =>
{
Debug.WriteLine("UI work on thread " + Environment.CurrentManagedThreadId);
Task.Factory.StartNew(() =>
{
Debug.WriteLine("Background work on thread " + Environment.CurrentManagedThreadId);
});
});
}
//输出:
//UI work on thread 1(UI线程)
//Background work on thread 1(UI线程)
private void Button_Click(object sender, EventArgs e)
{
TaskFactory factory = new TaskFactory(); //默认是线程池的TaskScheduler
factory.StartNew(() =>
{
Debug.WriteLine("UI work on thread " + Environment.CurrentManagedThreadId);
Task.Factory.StartNew(() =>
{
Debug.WriteLine("Background work on thread " + Environment.CurrentManagedThreadId);
});
});
}
//输出:
//UI work on thread 3(线程池线程)
//Background work on thread 4(线程池线程)

7.2 Task.Run

Task.Run是将委托排队到线程池的首选方法,提供了比Task.Factory.StartNew更简单的API,并且支持异步感知。Task.Run默认的TaskSchedulerTaskScheduler.Default,这一点很棒,但是如果你想使用自定义的TaskScheduler,就只能用TaskFactory了。下面列举几种常见的方法重载

Task Run(Action);
Task Run(Action, CancellationToken);
Task Run(Func<Task>);
Task Run(Func<Task>, CancellationToken);
Task<TResult> Run<TResult>(Func<TResult>);
Task<TResult> Run<TResult>(Func<TResult>, CancellationToken);
Task<TResult> Run<TResult>(Func<Task<TResult>>);
Task<TResult> Run<TResult>(Func<Task<TResult>>, CancellationToken);

对于TaskFactory.StartNew,委托参数是Action / Func<TResult>时结果具有合理的预期,而委托参数是Func<Task> / Func<Task<TResult>>时结果却变得复杂,这就是所谓的不支持异步感知。

对于Task.Run,不论委托参数是Action / Func<TResult>还是Func<Task> / Func<Task<TResult>>,结果都具有合理的预期,这就是所谓的支持异步感知。(关于异步感知,后面再详细讲)

CancellationToken参数和在StarNew存在一样的问题,几乎毫无用处。

8,Promise Task

Promise Task是表示系统事件或信号的任务,它没有需要执行的用户代码。看看开发中创建Promise Task的方式。

8.1 Task.Delay

几种常见的方法重载

Task Delay(int);
Task Delay(int, CancellationToken);

Delay方法本质上是一个计时器,当计时器时间到时会让返回的Task进入RanToCompletion状态。CancellationToken参数与Task.Run不同,此参数时可以取消Delay本身的。因此响应取消时,返回的Task进入Canceled状态。

8.2 Task.Yield

Task.Yield有点奇怪。它不返回Task,因此它并不是正宗的创建Promise Task方法,但是它使用起来很像Promise Task。

YieldAwaitable Yield();

Task.Yield就像执行一个已经完成的任务,或者说就像Task.Delay(0)

private async void button_Click(object sender, EventArgs e)
{
await Task.Yield(); // Make us async right away
var data = DoSomethingOnUIThread(); // This will run on the UI thread at some point later
await UseDataAsync(data);
}

如果没有Task.Yield()DoSomethingOnUIThread方法将会立刻在UI线程上同步执行。Task.Yield()配合await关键字让后续代码成为Task的Continuation,需要TaskScheduler来重新分配。但是这有什么用呢??没想明白。。

8.3 Task.FromResult

Task.FromResult返回一个带返回值的已经完成的任务

Task<TResult> FromResult<TResult>(TResult);

有点像在Task.Yield的基础上加了一个返回值,除了用于直接返回一个带返回值的已经完成的任务,在一些其他情况下也是有用的。

比如一个接口中有一个异步方法,如果方法的实现是同步的,就可以用Task.FromResult包装这个同步结果。

interface IMyInterface
{
// Implementations might need to be asynchronous, so we define an asynchronous API.
Task<int> DoSomethingAsync();
}
class MyClass : IMyInterface
{
// This particular implementation is not asynchronous.
public Task<int> DoSomethingAsync()
{
int result = 42; // Do synchronous work.
return Task.FromResult(result);
}
}

还有一种情况就是使用缓存时,如果缓存中检索到了数据则用Task.FromResult包装同步结果,否则执行真正的异步操作。

public Task<string> GetValueAsync(int key)
{
string result;
if (cache.TryGetValue(key, out result))
{
return Task.FromResult(result);
}
return DoGetValueAsync(key);
}
private async Task<string> DoGetValueAsync(int key)
{
string result = await GetValueAsync();
cache.TrySetValue(key, result);
return result;
}

是否还有其他的方法返回已经完成的任务呢?有的,类似Task.FromResult返回状态为RanToCompletion的任务,还有Task.FromCanceledTask.FromException分别返回状态为CanceledFaulted的任务。

8.4 TaskCompletionSource

TaskCompletionSource用于创建一个任务,并且可以手动设置任务的最终状态。有点像Task.FromResultTask.FromCanceledTask.FromException三者的合集。

举个例子,在不使用Task.RunStartNew的前提下,如何实现异步执行Func<T>并且用Task<T>来表示这个操作呢?用TaskCompletionSource<T>就可以做到。

public static Task<T> RunAsync<T>(Func<T> function)
{
if (function == null)
{
throw new ArgumentNullException(“function”);
}
var tcs = new TaskCompletionSource<T>();
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
T result = function();
tcs.SetResult(result);
}
catch(Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}

SetResult方法将任务状态设为RanToCompletionSetException方法将任务状态设为Faulted,还有SetCanceled方法将任务状态设为Canceled

9,补充

9.1 Task.Run vs Task.Factory.StartNew

9.1.1 简单理解

Task.Run可以看作是Task.Factory.StartNew的一种简单快捷方式。

//下面两段代码是等价的
Task.Run(someAction); Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

9.1.2 异步感知

上文提到Task.Run支持异步感知(async-aware),而Task.Factory.StartNew不支持。考虑如下代码

Task<int> t = await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
return 42;
});

按初学者的思路,尝试用Task.Run实现上面代码的功能

int result = await Task.Run(async () =>
{
await Task.Delay(1000);
return 42;
});

发现问题了吧,await Task.Factory.StartNew返回类型是Task<int>,而await Task.Run(async)返回类型是int

StartNew的参数类型为Func<Task<int>>,那么StartNew的返回类型是Task<Task<int>>await Task<Task<int>>就会得到Task<int>,没毛病啊。

那问题肯定就出在Task.Run,还有一层Task到哪去了呢?实际上将上面使用Task.Run的代码片段改用StartNew,会变成下面这样

int result = await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();

Unwrap方法解封装了Func<Task<int>>委托返回的内部任务,更明确地讲Unwrap方法使得Task<Task<int>>变成了Task<Task<int>>(带删除线的就是被解封装的Task)。

Task.Run的委托参数是异步委托时,它能自动识别并且在内部调用Unwrap方法进行解封装。这就是异步感知的本质,微软偷偷摸摸做的好事。

9.1.3 TaskScheduler.Current的问题

Task.Run的默认TaskSchedulerTaskScheduler.DefaultStartNew的默认TaskSchedulerTaskScheduler.Current。上文推荐TaskScheduler.Default,并反复吐槽了TaskScheduler.Current

Task.Factory.StartNew(A);

请问方法A会在哪个线程上执行?回答不上来?那我们再补充上下文

private void Form1_Load(object sender, EventArgs e)
{
Task.Factory.StartNew(A);
}

再次请问方法A会在哪个线程上执行?A会在线程池线程上执行。

为什么?Task.Factory.StartNew首先检查当前的TaskScheduler。结果当前没有,所以它使用了线程池的TaskScheduler。对于简单的情况来说已经足够了,让我们考虑一个更实际的例子。

private void Form1_Load(object sender, EventArgs e)
{
Compute(3);
}
private void Compute(int counter)
{
if (counter == 0) // If we're done computing, just return.
{
return;
}
TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() => A(counter))
.ContinueWith(t =>
{
this.Text = t.Result.ToString(); // Update UI with results.
Compute(counter - 1); // Continue working.
}, ui);
}
private int A(int value)
{
return value; // CPU-intensive work.
}

还是同样的问题,方法A会在哪个线程上执行?上文其实还有一个类似的例子,如果看懂了应该能答出这个问题。

方法A一共执行了3次,第1次在线程池线程上执行,后2次在UI线程上执行。

第1次执行A时,TaskFactory首先检查当前的TaskScheduler。结果当前没有,所以它使用了线程池的TaskScheduler。第1次执行ContinueWith时,指定了UI的TaskScheduler,当第2次执行A时,TaskScheduler.Current指导TaskFactory获取到了UI的TaskScheduler,因此第2次在UI线程上执行,第3次情况一样。

TaskScheduler.Current经常会导致不可预知的行为,因此很多开发团队要求在使用StartNew时必须显式地指定TaskScheduler参数。遗憾的是具有TaskScheduler参数的唯一重载方法也具有CancellationToken参数和 TaskCreationOptions参数。为了使Task.Factory.StartNew可靠地、可预测地将任务安排到线程池,就应该这么写

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

你可能发现了,这不就是Task.Run(A)

9.2 Promise Task的执行

考虑一个常见的Promise Task,比如写入操作(写入硬盘文件, 网络流, 内存流等)

private async void Button_Click(object sender, EventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
}

await期间UI线程没有阻塞,那么是谁在执行写入操作从而解放了UI线程呢?

首先,假设WriteAsync是使用.NET的标准P/Invoke异步I/O系统(standard P/Invoke asynchronous I/O system)实现的,那么它会在设备的底层HANDLE上启动一个Win32异步I/O操作(overlapped I/O operation)。

然后,操作系统要求设备驱动开始写入操作,它首先构造了一个表示写入请求的对象,称作I/O请求包(I/O Request Packet, IRP)。设备驱动收到IRP并向对应的设备发出写入数据的命令。如果设备支持直接内存访问(Direct Memory Access, DMA),写入操作就会像把缓存地址写入设备寄存器一样简单。这就是设备驱动做的事:将IRP标记为挂起(pending)并返回给操作系统。



在处理IRP时不允许阻止设备驱动。这意味着,如果无法立即完成IRP,则必须异步处理它,即使对于同步API也是如此。在设备驱动的级别,所有的请求都是异步的。

操作系统收到挂起的IRP并返回给函数库,函数库再将IRP作为一个未完成的Task返回给Button_Click方法,Button_Click方法收到未完成的Task便继续执行UI线程。

纵观整个过程,没有线程参与写入操作;驱动程序线程、操作系统线程、BCL线程或线程池线程都没有,没有任何线程

后面设备完成写入操作,也没有线程的参与,想知道更多细节可以看Stephen Cleary的博客。

9.3 Task.Run使用建议

Task.Run用于以异步的方式执行CPU密集型代码(CPU-bound code),That is all。

9.3.1 简单例子

考虑如下CPU密集型的简单例子

class MyService
{
public int CalculateMandelbrot()
{
for (int i = 0; i != 10000000; ++i) // Tons of work to do in here
{
// heavy calculation
}
return 42;
}
}
private void MyButton_Click(object sender, EventArgs e)
{
myService.CalculateMandelbrot(); // UI线程阻塞了
}

我们不希望阻塞UI线程,下面尝试用Task.Run来执行这些CPU密集型代码避免阻塞。

class MyService
{
public int CalculateMandelbrot()
{
for (int i = 0; i != 10000000; ++i) // Tons of work to do in here
{
// heavy calculation
}
return 42;
}
}
private async void MyButton_Click(object sender, EventArgs e)
{
await Task.Run(() => myService.CalculateMandelbrot()); // Use Task.Run here
}

不要在实现方法时使用Task.Run,应该在调用方法时使用Task.Run。既然UI层需要异步API,那么就让UI层使用Task.Run来解决问题,保持服务MyService的干净整洁。

9.3.2 复杂例子

再考虑一个CPU密集型和IO密集型的复杂例子

// Bad code
class MyService
{
public int PredictStockMarket()
{
Thread.Sleep(1000); // Do some I/O first
for (int i = 0; i != 10000000; ++i) // Tons of work to do in here
{
// heavy calculation
}
Thread.Sleep(1000); // Possibly some more I/O here
for (int i = 0; i != 10000000; ++i) // More work
{
// heavy calculation
}
return 42;
}
}

对于CPU密集型部分,使用异步代码将阻塞I/O替换为异步I/O。但是我们如何处理CPU密集型部分呢?先看一个常见的错误做法

// Bad code
class MyService
{
public async Task<int> PredictStockMarketAsync()
{
await Task.Delay(1000); // Do some I/O first
await Task.Run(() => // Bad
{
for (int i = 0; i != 10000000; ++i) // Tons of work to do in here
{
// heavy calculation
}
});
await Task.Delay(1000); // Possibly some more I/O here
await Task.Run(() => // Bad
{
for (int i = 0; i != 10000000; ++i) // More work
{
// heavy calculation
}
});
return 42;
}
}

API不能是异步的(因为它有CPU密集型部分),也不能是同步的(因为我们想要使用异步I/O)。因此,这里并没有理想的解决方案。经过讨论,最好的办法还是使用异步签名,同时记录此方法包含CPU密集型部分。

// Acceptable code
class MyService
{
// This method is CPU-bound!
public async Task<int> PredictStockMarketAsync()
{
await Task.Delay(1000); // Do some I/O first
for (int i = 0; i != 10000000; ++i) // Tons of work to do in here
{
// heavy calculation
}
await Task.Delay(1000); // Possibly some more I/O here
for (int i = 0; i != 10000000; ++i) // More work
{
// heavy calculation
}
return 42;
}
}

桌面应用使用Task.Run调用此方法,ASP.NET应用则直接调用此方法。

private async void MyButton_Click(object sender, EventArgs e)
{
await Task.Run(() => myService.PredictStockMarketAsync());
} public class StockMarketController: Controller
{
public async Task<ActionResult> IndexAsync()
{
var result = await myService.PredictStockMarketAsync();
return View(result);
}
}

即便在复杂情况下,也不应该在实现方法时使用Task.Run,而是调用方法时使用Task.Run

作者:tossorrow

出处:C#基础 - Task

转载:欢迎转载,请保留此段声明,请在文章中给出原文链接;

C#基础 - Task的更多相关文章

  1. Task C# 多线程和异步模型 TPL模型 【C#】43. TPL基础——Task初步 22 C# 第十八章 TPL 并行编程 TPL 和传统 .NET 异步编程一 Task.Delay() 和 Thread.Sleep() 区别

    Task C# 多线程和异步模型 TPL模型   Task,异步,多线程简单总结 1,如何把一个异步封装为Task异步 Task.Factory.FromAsync 对老的一些异步模型封装为Task ...

  2. ansible基础-task控制

    1. 前言 很多情况下,一个play是否执行会依赖于某个(些)变量的值,这个变量可以来自自定义变量.facts,甚至是另一个task的执行结果. ansible通过变量判定task是否执行,我们称之为 ...

  3. C#并发编程-2 异步编程基础-Task

    一 异步延迟 在异步方法中,如果需要让程序延迟等待一会后,继续往下执行,应使用Task.Delay()方法. //创建一个在指定的毫秒数后完成的任务. public static Task Delay ...

  4. ansible基础-理解篇

    1. 介绍 要说现在的部署工具,ansible可以说家喻户晓了. ansible是一个开源软件,用于软件供应.配置管理.应用部署.ansible可以通过SSH.remote PowerShell.其他 ...

  5. celery (二) task调用

    调用 TASK 基础 task 的调用方式有三种: 类似普通函数的调用方式, 通过 __calling__ 调用 ,类似 function() 通过 apply_async() 调用,能接受较多的参数 ...

  6. C# 异步编程3 TPL Task 异步程序开发

    .Net在Framework4.0中增加了任务并行库,对开发人员来说利用多核多线程CPU环境变得更加简单,TPL正符合我们本系列的技术需求.因TPL涉及内容较多,且本系列文章为异步程序开发,所以本文并 ...

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

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

  8. 【温故而知新-万花筒】C# 异步编程 逆变 协变 委托 事件 事件参数 迭代 线程、多线程、线程池、后台线程

    额基本脱离了2.0 3.5的时代了.在.net 4.0+ 时代.一切都是辣么简单! 参考文档: http://www.cnblogs.com/linzheng/archive/2012/04/11/2 ...

  9. 【C#】C#线程_I/O限制的异步操作

    目录结构: contents structure [+] 为什么需要异步IO操作 C#的异步函数 async和await的使用 async和Task的区别 异步函数的状态机 异步函数如何转化为状态机 ...

  10. Celery-4.1 用户指南: Application(应用)

    Application Celery 库在使用之前必须初始化,一个celery实例被称为一个应用(或者缩写 app). Celery 应用是线程安全的,所以多个不同配置.不同组件.不同任务的 应用可以 ...

随机推荐

  1. bs4解析-优美图库

    import requests from bs4 import BeautifulSoup url = 'http://www.umeituku.com/bizhitupian/meinvbizhi/ ...

  2. python基础-集合set { }

    集合的定义和操作 集合的特性: 元素数量 支持多个 元素类型 任意 下标索引 支持 重复元素 不支持 可修改性 支持 数据有序 否 使用场景 不可重复的数据记录场景 # 定义集合 my_set = { ...

  3. 说说你对 SPA 单页面的理解,它的优缺点分别是什么?

    SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML.JavaScript 和 CSS. 一旦页> 面加载完成,SPA 不会因为用户的操作而 ...

  4. application.properties数据库连接字符串

    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://loca ...

  5. [oeasy]python0112_扩展ascii_Extended_ascii_法文字符

    法文字符 回忆上次内容 上次回顾了 字型编码的进化过程 从 7-seg 到 点阵字库 终于让字母.数字.标点 明确了字型 小写字符 占据了位置 法文字符 没有地方放了     ​   添加图片注释,不 ...

  6. [oeasy]python0072_自定义小动物变色_cowsay_color_boxes_asciiart

    修改颜色 回忆上次内容 上次搞的是 颜色 前景颜色 总共有 7 种基本色 还有什么 好玩的 么? 可以 给小动物 上色 吗? 配合 先将cowsay结果 输出重定向 sudo apt install ...

  7. 2023 CSP 游记

    目录 \(\text{CSP-J}\) 游记 \(\text{CSP-S}\) 游记 \(\text{CSP-J}\) 游记 省流:\(\text{B}\) 题挂了 \(100\text{ pts}\ ...

  8. wails实现腾讯元器bot

    简单记录工具的一个模块 后端 Api调用 登录 腾讯元器 后创建智能体,按自己的需求来创建,发布后要等等审核. ​​ 等发布完成后点击调用api即可,这里可以看到user_id​, assistant ...

  9. 毕业设计&毕业项目:基于springboot+vue实现的在线音乐平台

    一.前言 在当今数字化时代,音乐已经成为人们生活中不可或缺的一部分.随着技术的飞速发展,构建一个用户友好.功能丰富的在线音乐平台成为了许多开发者和创业者的目标.本文将介绍如何使用SpringBoot作 ...

  10. 从DDPM到DDIM (一) 极大似然估计与证据下界

    从DDPM到DDIM (一) 极大似然估计与证据下界   现在网络上关于DDPM和DDIM的讲解有很多,但无论什么样的讲解,都不如自己推到一遍来的痛快.笔者希望就这篇文章,从头到尾对扩散模型做一次完整 ...