这可能并非专门针对SemaphoreSlim,但基本上我的问题是,以下两种限制长时间运行的任务的方法之间是否存在差异;如果有,差异是什么?可以使用)。
在下面的示例中,假设每个跟踪的任务都涉及从Url加载数据(完全构成示例,但我在SemaphoreSlim示例中发现了一个常见的示例)。
主要区别在于将各个任务添加到跟踪任务列表中的方式。在第一个示例中,我们用lambda调用Task.Run()
,而在第二个示例中,我们用lambda调用Func(<Task<Result>>())
,然后立即调用该func并将结果添加到跟踪的任务列表中。 / p>
SemaphoreSlim ss = new SemaphoreSlim(_concurrentTasks);
List<string> urls = ImportUrlsFromSource();
List<Task<Result>> trackedTasks = new List<Task<Result>>();
foreach (var item in urls)
{
await ss.WaitAsync().ConfigureAwait(false);
trackedTasks.Add(Task.Run(async () =>
{
try
{
return await ProcessUrl(item);
}
catch (Exception e)
{
_log.Error($"logging some stuff");
throw;
}
finally
{
ss.Release();
}
}));
}
var results = await Task.WhenAll(trackedTasks);
SemaphoreSlim ss = new SemaphoreSlim(_concurrentTasks);
List<string> urls = ImportUrlsFromSource();
List<Task<Result>> trackedTasks = new List<Task<Result>>();
foreach (var item in urls)
{
trackedTasks.Add(new Func<Task<Result>>(async () =>
{
await ss.WaitAsync().ConfigureAwait(false);
try
{
return await ProcessUrl(item);
}
catch (Exception e)
{
_log.Error($"logging some stuff");
throw;
}
finally
{
ss.Release();
}
})());
}
var results = await Task.WhenAll(trackedTasks);
答案 0 :(得分:2)
有两个区别:
首先,当您调用lambda时,它将运行。另一方面,Task.Run
会调用它。这很重要,因为Task.Run
在幕后做了一些工作。它所做的主要工作是处理错误的任务...
如果您调用lambda,并且lambda抛出,它将在您将Task
添加到列表之前抛出……
但是,在您的情况下,由于您的lambda是异步的,因此编译器会为其创建Task
(您不是手工创建的),并且它将正确处理该异常并通过返回Task
。因此,这一点尚无定论。
Task.Run
设置DenyChildAttach
。这意味着在Task.Run
内部创建的任务独立于返回的Task
运行(不同步)。
例如,此代码:
List<Task<int>> trackedTasks = new List<Task<int>>();
var numbers = new int[]{0, 1, 2, 3, 4};
foreach (var item in numbers)
{
trackedTasks.Add(Task.Run(async () =>
{
var x = 0;
(new Func<Task<int>>(async () =>{x = item; return x;}))().Wait();
Console.WriteLine(x);
return x;
}));
}
var results = await Task.WhenAll(trackedTasks);
将以未知顺序输出0到4之间的数字。但是下面的代码:
List<Task<int>> trackedTasks = new List<Task<int>>();
var numbers = new int[]{0, 1, 2, 3, 4};
foreach (var item in numbers)
{
trackedTasks.Add(new Func<Task<int>>(async () =>
{
var x = 0;
(new Func<Task<int>>(async () =>{x = item; return x;}))().Wait();
Console.WriteLine(x);
return x;
})());
}
var results = await Task.WhenAll(trackedTasks);
每次将按顺序输出从0到4的数字。这很奇怪,对吧?发生的情况是内部任务附加到外部任务,并立即在同一线程中执行。但是,如果您使用Task.Run
,则内部任务不会附加和独立安排。
即使您使用await
,只要您await
的任务不会进入外部系统,这仍然适用...
外部系统会怎样?好吧,例如,如果您的任务是从URL读取-如您的示例-系统将创建一个TaskCompletionSource
,从中获取Task
,设置一个响应处理程序,将结果写入到TaskCompletionSource
,发出请求,然后返回Task
。 Task
没有被调度,它在与父任务相同的线程上运行是没有意义的。因此,它可能会破坏顺序。
由于您正在使用await
在外部系统上等待,因此这一点也没有意义。
我必须得出结论,这些是等效的。
如果您想确保安全,并确保它能按预期工作,即使在将来的版本中,以上几点不再无聊,也请保留Task.Run
。另一方面,如果您确实要优化,请使用lambda并避免Task.Run
(很小)的开销。但是,这可能不会成为瓶颈。
当我谈论到外部系统的任务时,我指的是在.NET外部运行的内容。可以在.NET中运行一些代码来与外部系统交互,但是大部分代码将不在.NET中运行,因此根本就不会在托管线程中。
API的使用者对此没有指定任何内容。该任务将是一个许诺任务,但这并不是暴露的,因为对于消费者而言,它没有什么特别的。
实际上,去往外部系统的任务可能根本无法在CPU中运行。此外,它可能只是在等待计算机外部的某个东西(可能是网络或用户输入)。
模式如下:
该库创建一个TaskCompletionSource
。
该库设置了一种接收通知的方法。它可以是回调,事件,消息循环,钩子,侦听套接字,管道,等待全局互斥锁……等等。
该库设置了代码,以对将调用SetResult
上的SetException
或TaskCompletionSource
上的TaskCompletionSource.Task
的通知做出响应。
该库对外部系统进行实际调用。
该库返回CancellationToken
。
注意:要特别注意优化,不要在不应该进行的地方重新排序,还要注意在设置阶段处理错误。另外,如果涉及到SetCancelled
,则必须将其考虑在内(并在适当时调用TaskCompletionSource
上的Task
)。另外,对通知的响应(或取消通知)可能会被删除。 啊,别忘了验证您的参数。
然后,外部系统运行并执行其所做的任何事情。然后,当它完成或出现问题时,向库发出通知,并且您的Task
突然完成,出现了错误……(或者如果发生了取消,则您的Task.Delay
现在被取消了)和.NET将根据需要安排任务的继续。
注意:异步/等待在幕后使用连续性,即恢复执行的方式。
顺便说一句,如果您想自己实现SempahoreSlim,则必须做与我上面描述的非常相似的事情。您可以在我的backport of SemaphoreSlim中看到它。
让我们看看一些诺言任务的例子...
Task.Delay
:当我们在等待FileStream.ReadSync
时,CPU没有旋转。这不是在线程中运行。在这种情况下,通知机制将是OS计时器。当操作系统看到计时器的时间已过时,它将调用CLR,然后CLR将标记任务为完成。什么线程在等待?没有。
FileStream.ReadSync
:当我们使用{{1}}从存储中读取数据时,实际工作是由设备完成的。 CRL必须声明一个自定义事件,然后将该事件,文件句柄和缓冲区传递给OS ... OS调用设备驱动程序,设备驱动程序与设备连接。随着存储设备恢复信息,它将通过DMA技术写入内存(直接在指定的缓冲区上)。完成后,它将设置一个中断,由驱动程序处理,通知操作系统,该操作系统调用自定义事件,将该任务标记为已完成。哪个线程从存储中读取了数据?没有。
将使用类似的模式从网页下载,除了这次设备进入网络。如何发出HTTP请求以及系统如何等待响应超出了此答案的范围。
外部系统也可能是另一个程序,在这种情况下,它将在线程上运行。但这不是您进程中的托管线程。
您的收获是这些任务不在您的任何线程上运行。它们的时机可能取决于外部因素。因此,将它们视为在同一线程中运行,或者我们可以预测它们的计时是没有道理的(当然,就计时器而言,当然除外)。
答案 1 :(得分:1)
两者都不是很好,因为它们会立即创建任务。 func版本的开销较少,因为它在线程池上保存了Task.Run
路由,只是为了立即结束线程池的工作并在信号量上挂起。您不需要异步Func
,可以通过使用异步方法(可能是本地函数)来简化此操作。
但是您完全不应该这样做。而是使用helper method that implements a parallel async foreach。
public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
{
return Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(dop)
select Task.Run(async delegate {
using (partition)
while (partition.MoveNext())
await body(partition.Current);
}));
}
那你就去urls.ForEachAsync(myDop, async input => await ProcessAsync(input));
在这里,任务是按需创建的。您甚至可以使输入流变得懒惰。