如何将其转换为异步任务?

鉴于以下代码……

static void DoSomething(int id) { Thread.Sleep(50); Console.WriteLine(@"DidSomething({0})", id); } 

我知道我可以将其转换为异步任务,如下所示……

 static async Task DoSomethingAsync(int id) { await Task.Delay(50); Console.WriteLine(@"DidSomethingAsync({0})", id); } 

通过这样做,如果我多次调用(Task.WhenAll),一切都会比使用Parallel.Foreach或甚至在循环内调用更快更高效。

但是有一分钟,让我们假装Task.Delay()不存在,我实际上必须使用Thread.Sleep(); 我知道实际情况并非如此,但这是概念代码,延迟/睡眠通常是IO操作,其中没有异步选项(例如早期EF)。

我试过以下……

 static async Task DoSomethingAsync2(int id) { await Task.Run(() => { Thread.Sleep(50); Console.WriteLine(@"DidSomethingAsync({0})", id); }); } 

但是,虽然它运行没有错误,但Lucien Wischik认为这实际上是不好的做法,因为它只是从池中启动线程来完成每个任务(使用以下控制台应用程序也会更慢 – 如果你在DoSomethingAsync和DoSomethingAsync2之间交换打电话你可以看到完成所需的时间有显着差异)…

 static void Main(string[] args) { MainAsync(args).Wait(); } static async Task MainAsync(String[] args) { List tasks = new List(); for (int i = 1; i <= 1000; i++) tasks.Add(DoSomethingAsync2(i)); // Can replace with any version await Task.WhenAll(tasks); } 

然后我尝试了以下……

 static async Task DoSomethingAsync3(int id) { await new Task(() => { Thread.Sleep(50); Console.WriteLine(@"DidSomethingAsync({0})", id); }); } 

移植它代替原始的DoSomethingAsync,测试永远不会完成,屏幕上不会显示任何内容!

我还尝试了其他多种不能编译或不完整的变体!

因此,考虑到您无法调用任何现有异步方法的约束并且必须在异步任务中完成Thread.Sleep和Console.WriteLine,您如何以与原始代码一样高效的方式执行此操作?

对于那些感兴趣的人来说,这里的目标是让我更好地理解如何创建我自己的异步方法,而不是任何人。 尽管进行了许多搜索,但这似乎是缺少示例的一个领域 – 虽然有成千上万的调用异步方法的示例依次调用其他异步方法我找不到任何将现有void方法转换为异步任务的方法除了那些使用Task.Run(()=> {})方法之外,没有调用进一步的异步任务。

有两种任务:执行代码的任务(例如, Task.Run和朋友),以及响应某些外部事件的任务(例如, TaskCompletionSource和朋友)。

您正在寻找的是TaskCompletionSource 。 常见情况有各种“速记”forms,因此您不必总是直接使用TaskCompletionSource 。 例如, Task.FromResultTaskFactory.FromAsync 。 如果您有I / O的现有*Begin / *End实现,则最常使用FromAsync ; 否则,您可以直接使用TaskCompletionSource

有关更多信息,请参阅实现基于任务的异步模式的“I / O绑定任务”部分。

Task构造函数(不幸的是)是基于任务的并行性的保留,不应该在异步代码中使用。 它只能用于创建基于代码的任务,而不能用于创建外部事件任务。

因此,考虑到您无法调用任何现有异步方法的约束并且必须在异步任务中完成Thread.Sleep和Console.WriteLine,您如何以与原始代码一样高效的方式执行此操作?

我会使用某种计时器,并在计时器触发时完成TaskCompletionSource 。 我几乎肯定的是,无论如何,实际的Task.Delay实现都是如此。

因此,考虑到您无法调用任何现有异步方法的约束并且必须在异步任务中完成Thread.Sleep和Console.WriteLine,您如何以与原始代码一样高效的方式执行此操作?

IMO,这是一个非常合成的约束,你真的需要坚持使用Thread.Sleep 。 在此约束下,您仍然可以略微改进基于Thread.Sleep的代码。 而不是这个:

 static async Task DoSomethingAsync2(int id) { await Task.Run(() => { Thread.Sleep(50); Console.WriteLine(@"DidSomethingAsync({0})", id); }); } 

你可以这样做:

 static Task DoSomethingAsync2(int id) { return Task.Run(() => { Thread.Sleep(50); Console.WriteLine(@"DidSomethingAsync({0})", id); }); } 

这样,您就可以避免编译器生成的状态机类的开销。 这两个代码片段之间存在细微差别, 如何传播exception 。

无论如何,这不是放缓的瓶颈所在。

(使用以下控制台应用程序也会变慢 – 如果在DoSomethingAsync和DoSomethingAsync2调用之间切换,您可以看到完成所需时间的显着差异)

让我们再看一下你的主循环代码:

 static async Task MainAsync(String[] args) { List tasks = new List(); for (int i = 1; i <= 1000; i++) tasks.Add(DoSomethingAsync2(i)); // Can replace with any version await Task.WhenAll(tasks); } 

从技术上讲,它要求并行运行1000个任务,每个任务都应该在自己的线程上运行。 在理想的宇宙中,你希望并行执行Thread.Sleep(50) 1000次并在大约50ms内完成整个事情。

但是,TPL的默认任务调度程序永远不会满足此请求 ,原因很简单:线程是一种宝贵且昂贵的资源。 而且,并发操作的实际数量限于CPU /核心数。 所以实际上,使用ThreadPool的默认大小,我得到21个并行服务此操作的池线程(峰值) 。 这就是DoSomethingAsync2 / Thread.SleepDoSomethingAsync / Task.Delay花费更长时间的Task.DelayDoSomethingAsync不会阻塞池线程,它只在超时完成时请求一个。 因此, DoSomethingAsync任务实际上可以并行运行,而不是DoSomethingAsync2那些。

测试 (控制台应用程序):

 // https://stackoverflow.com/q/21800450/1768303 using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace Console_21800450 { public class Program { static async Task DoSomethingAsync(int id) { await Task.Delay(50); UpdateMaxThreads(); Console.WriteLine(@"DidSomethingAsync({0})", id); } static async Task DoSomethingAsync2(int id) { await Task.Run(() => { Thread.Sleep(50); UpdateMaxThreads(); Console.WriteLine(@"DidSomethingAsync2({0})", id); }); } static async Task MainAsync(Func tester) { List tasks = new List(); for (int i = 1; i <= 1000; i++) tasks.Add(tester(i)); // Can replace with any version await Task.WhenAll(tasks); } volatile static int s_maxThreads = 0; static void UpdateMaxThreads() { var threads = Process.GetCurrentProcess().Threads.Count; // not using locks for simplicity if (s_maxThreads < threads) s_maxThreads = threads; } static void TestAsync(Func tester) { s_maxThreads = 0; var stopwatch = new Stopwatch(); stopwatch.Start(); MainAsync(tester).Wait(); Console.WriteLine( "time, ms: " + stopwatch.ElapsedMilliseconds + ", threads at peak: " + s_maxThreads); } static void Main() { Console.WriteLine("Press enter to test with Task.Delay ..."); Console.ReadLine(); TestAsync(DoSomethingAsync); Console.ReadLine(); Console.WriteLine("Press enter to test with Thread.Sleep ..."); Console.ReadLine(); TestAsync(DoSomethingAsync2); Console.ReadLine(); } } } 

输出:

按Enter键以使用Task.Delay进行测试...
 ...
时间,ms:1077,峰值线程:13

按Enter键以使用Thread.Sleep进行测试...
 ...
时间,ms:8684,高峰期线程:21

是否有可能改善基于Thread.SleepDoSomethingAsync2的时序数字 ? 我能想到的唯一方法是使用TaskCreationOptions.LongRunningTask.Factory.StartNew

在任何实际应用程序中执行此操作之前,您应该三思而后行

 static async Task DoSomethingAsync2(int id) { await Task.Factory.StartNew(() => { Thread.Sleep(50); UpdateMaxThreads(); Console.WriteLine(@"DidSomethingAsync2({0})", id); }, TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness); } // ... static void Main() { Console.WriteLine("Press enter to test with Task.Delay ..."); Console.ReadLine(); TestAsync(DoSomethingAsync); Console.ReadLine(); Console.WriteLine("Press enter to test with Thread.Sleep ..."); Console.ReadLine(); TestAsync(DoSomethingAsync2); Console.ReadLine(); } 

输出:

按Enter键以使用Thread.Sleep进行测试...
 ...
时间,ms:3600,峰值线程:163

时间越来越好,但价格却很高。 此代码要求任务计划程序为每个新任务创建一个新线程。 不要指望这个线程来自池:

 Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("Thread pool: " + Thread.CurrentThread.IsThreadPoolThread); // false! }, TaskCreationOptions.LongRunning).Wait();