Task

Task.Factory.StartNew和Task.Run区别之一就有Task.Run会自动执行Unwrap操作,但是Task.Factory.StartNew不会,Task.Run就是Task.Factory.StartNew的更人性化封装,而Task.Factory.StartNew则是原始的执行。

 

若希望延续任务和先导任务执行在同一个线程上,还需要指定TaskContinuationOptions.ExecuteSynchronously。否则的话,它就会去请求线程池。

 

死锁的根本原因是等待处理上下文的方式有问题。默认情况下,当一个没有完成的异步任务在被等待时,当前异步方法的上下文对象会被切换到等待状态让出CPU资源直到函数执行完成。这个上下文对象是当前异步方法的SynchronizationContext对象。图形界面程序和ASP.NET应用程序有一个SynchronizationContext对象,由于这个对象的存在,一次只允许运行一个代码块。等异步函数执行完成时,会返回异步函数自己的SynchronizationContext对象给调用端。但是,当前线程中已经有一个SynchronizationContext对象了,于是就形成了调用异步函数的线程在等待异步函数返回,而异步函数因为调用端线程中已经有一个SynchronizationContext对象了,在等待那个对象从线程中切换出去,于是形成了死锁。

大多数的库并不在意Context。 因此,这种自动封送经常是完全不必要的成本。

byte [] buffer = new byte[0x1000];
int numRead;
while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) {
  await source.WriteAsync(buffer, 0, numRead);
}

如果此复制操作是从 UI 线程调用,那么每一个等待的读、写操作都将强制完成回 UI 线程。 对于 1 MB 的源数据和异步完成读、写的流(大多数的流都是这样),这意味着从后台到 UI 线程有多达 500 个跃点。

为解决这一问题,Task 和 Task<TResult> 类型提供了 ConfigureAwait 方法。 ConfigureAwait 接受一个控制此封送行为的 Boolean continueOnCapturedContext 参数。 如果使用默认的 true,等待将在捕获的 SynchronizationContext 上自动完成。 但是如果使用 false,SynchronizationContext 将被忽略并且 Framework 将尝试在之前异步操作完成的位置继续执行。 

 

另一种创建任务的方法是使用TaskCompletionSource。
TaskCompletionSource可以创建一个任务,但是这种任务并非那种需要执行启动操作并在随后停止的任务;而是在操作结束或出错时手动创建的“附属”任务。这非常适用于I/O密集型的工作。它不但可以利用任务所有的优点(能够传递返回值、异常或延续)而且不需要在操作执行期间阻塞线程。
TaskCompletionSource的用法很简单,直接进行实例化即可。它包含一个Task属性,返回一个Task对象。我们可以等待这个对象,也可以和其他的所有任务一样,在其上附加延续。
TaskCompletionSource的真正作用是创建一个不绑定线程的任务。例如,假设一个任务需要等待5秒钟,之后返回数字42。
TaskCompletionSource不需要使用线程,意味着只有当延续启动的时候(5秒钟之后)才会创建线程。接下来,我们将同时启动10000个操作,但这并不会出错或者过多消耗资源:

 

有一个简单的方式可在不使用锁或者信号发送结构的前提下达到相同的效果。即缓存Task<string>而不是缓存string:

static Dictionary<string, Task<string> > _cache =
new Dictionary<string, Task<string> > ();

Task<string> GetWebPageAsync (string uri)
{
Task<string> downloadTask;
if (_cache.TryGetValue (uri, out downloadTask)) return downloadTask;
return _cache [uri] = new WebClient().DownloadStringTaskAsync (uri);
}

(注意,我们并没有将方法标记为async,这是因为我们直接返回了从WebClient的方法中得到的Task)。
现在,如果使用相同的URI重复调用GetWebPageAsync,就可以保证能够获得同一个Task<string>对象。(这样做还有一个好处即可以降低GC的负载。)如果任务完成,由于有前面介绍过的编译器优化,因此等待它的开销是很低的。

 

对于一个在循环中多次调用的方法,通过调用ConfigureAwait方法可以避免该方法重复回弹到UI消息循环中。它会阻止任务将延续提交到同步上下文中,将开销降低到了上下文切换的级别(远远小于等待同步方法完成的开销)。

async void A() { ... await B(); ... }

async Task B()
{
for (int i = 0; i < 1000; i++)
await C().ConfigureAwait (false);
}

async Task C() { ... }

这意味着方法B和方法C不会再应用UI应用程序的简单线程安全模型(代码会在UI线程上执行,只有在await时才可以抢占),而方法A则不受影响,在启动之后仍然会运行在UI线程上。
该优化尤其适合于编写程序库:因为此时不需要简化线程安全的支持,因为这些代码不会和调用者共享状态,也不会访问UI控件。

 

UI上运行continue

Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith
            (
                _ => Foo()
                // 如果 Foo 不需要在主线程,请注释下面一段代码
                , TaskScheduler.FromCurrentSynchronizationContext()
            );

 

使用 ValueTask 代替 Task

我们在遇到 Task<T> 时,大多数情况下只是需要简单的对其进行 await 而已,而并不需要将其保存下来以后再 await,那么 Task<T> 提供的很多的功能则并没有被使用,反而在高并发下,由于反复分配 Task 导致 GC 压力增加。

这种情况下,我们可以使用 ValueTask<T> 代替 Task<T>

async ValueTask<int> Foo()
{
    await Task.Delay(5000);
    return 5;
}

async ValueTask Caller()
{
    await Foo();
}

 

posted @ 2020-08-13 18:56  yetsen  阅读(586)  评论(0编辑  收藏  举报