鉴于以下代码......
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<Task> tasks = new List<Task>();
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(()=&gt; {})方法之外,没有其他异步任务的调用。
答案 0 :(得分:7)
有两种任务:执行代码的任务(例如Task.Run
和朋友),以及响应某些外部事件的任务(例如TaskCompletionSource<T>
和朋友)。
您正在寻找的是TaskCompletionSource<T>
。常见情况有各种“速记”形式,因此您不必总是直接使用TaskCompletionSource<T>
。例如,Task.FromResult
或TaskFactory.FromAsync
。如果您的I / O已经实现FromAsync
/ *Begin
,那么*End
最常用;否则,您可以直接使用TaskCompletionSource<T>
。
有关详细信息,请参阅Implementing the Task-based Asynchronous Pattern的“I / O绑定任务”部分。
Task
构造函数(不幸的是)是基于任务的并行性的保留,不应该在异步代码中使用。它只能用于创建基于代码的任务,而不能用于创建外部事件任务。
因此,鉴于您无法调用任何现有异步方法的约束并且必须在异步任务中完成Thread.Sleep和Console.WriteLine,您如何以与原始代码一样高效的方式执行此操作?
我会使用某种计时器,并在计时器触发时完成TaskCompletionSource<T>
。我几乎肯定这是实际的Task.Delay
实现所做的事情。
答案 1 :(得分:5)
所以,考虑到你不能调用任何现有的约束 异步方法,必须完成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);
});
}
这样,您就可以避免编译器生成的状态机类的开销。在how exceptions are propagated中,这两个代码片段之间存在细微差别。
无论如何,这是不,这是减速瓶颈所在。
(使用以下控制台应用程序也会更慢 - 如果你 在DoSomethingAsync和DoSomethingAsync2调用之间交换你可以看到一个 完成所需时间的显着差异)
让我们再看一下你的主循环代码:
static async Task MainAsync(String[] args) {
List<Task> tasks = new List<Task>();
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.Sleep
比DoSomethingAsync
/ Task.Delay
花费更长时间的原因。 DoSomethingAsync
不阻止池线程,它只在超时完成时请求一个。因此,更多DoSomethingAsync
个任务实际上可以并行运行,而不是DoSomethingAsync2
个。{/ p>
测试(控制台应用):
// 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<int, Task> tester)
{
List<Task> tasks = new List<Task>();
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<int, Task> 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();
}
}
}
<强>输出:强>
Press enter to test with Task.Delay ... ... time, ms: 1077, threads at peak: 13 Press enter to test with Thread.Sleep ... ... time, ms: 8684, threads at peak: 21
是否可以改善基于Thread.Sleep
的{{1}} 的时序数字?我能想到的唯一方法是将DoSomethingAsync2
与TaskCreationOptions.LongRunning
:
在任何实际应用程序中执行此操作之前,您应该三思而后:
Task.Factory.StartNew
<强>输出:强>
Press enter to test with Thread.Sleep ... ... time, ms: 3600, threads at peak: 163
时间越来越好,但价格却很高。此代码要求任务计划程序为每个新任务创建一个新线程。 不期望此线程来自池:
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();
}