目录

前言

原文是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. Fake权限验证小例子

    前言 关于本地测试如何进行Fake权限验证 正文 在我们使用swagger调试本地接口的时候,我们常常因为每次需要填写token而耽误工作,不可能每次调试的时候都去本地测试环境请求一个token进行验 ...

  2. 嵌入式知识分享——GDB程序调试方法说明

    前  言 本指导文档适用开发环境: Windows开发环境:Windows 7 64bit.Windows 10 64bit Linux开发环境:Ubuntu 18.04.4 64bit 虚拟机:VM ...

  3. Python性能测试框架:Locust实战教程

    01认识Locust Locust是一个比较容易上手的分布式用户负载测试工具.它旨在对网站(或其他系统)进行负载测试,并确定系统可以处理多少个并发用户,Locust 在英文中是 蝗虫 的意思:作者的想 ...

  4. Nginx负载配置

    目录 Nginx 负载均衡笔记 1. 概述 1.1 Nginx 简介 1.2 负载均衡概述 2. 四层负载均衡(传输层) 2.1 工作原理 2.2 特点 2.3 优缺点 优点 缺点 2.4 示例场景 ...

  5. NKCTF 2023 Misc

    NKCTF 2023 Misc hard-misc base32 --> N0wayBack公众号回复:NKCTF2023我来了! 得到flag:NKCTF{wtk2023Oo0oImcoM1N ...

  6. IDEA+Maven+Spring5.X项目创建

    创建maven 添加依赖 pom.xml <dependencies> <dependency> <groupId>org.springframework</ ...

  7. SpringBoot配置Jackson处理字段

    常用框架 阿里fastjson,谷歌gson等 JavaBean序列化为json 性能:Jackson>FastJson>Gson>lib 同个结构 Jackson.Fastjson ...

  8. [oeasy]python0100_wintel联盟_intel_微软_microsoft_msDOS_基尔代尔

    wintel联盟 回忆上次内容 上次 了解了IBM的 背水一战 IBM 已经不在乎 软硬一体全自主的设计 了 而采用了 开放的架构 任何 硬件厂商和软件厂商 都可以来合作 以丧失 自主控制力的方式 获 ...

  9. 靶机: EvilBox---One

    靶机: EvilBox---One 准备工作 靶机地址: https://download.vulnhub.com/evilbox/EvilBox---One.ova MD5 校验:c3a65197b ...

  10. holiday week 1

    本周进度总结: JAVA javafx以安装完毕并完成了环境配置 因处于小学期中java暂时搁置学习 自学了部分链表.多线程以及一些C/C++的知识,对部分C++库有了更进一步了解 因多线程的问题将平 ...