前言:我知道使用ThreadPool(通过TPL
或直接通过)进行IO操作is generally frowned upon,因为IO必然是顺序的,但我的问题与" parallel有关IO"阻止不会公开Async
方法的电话。
我正在编写一个GUI工具来获取有关网络上执行此操作的计算机的信息(简化代码):
String[] computerNames = { "foo", "bar", "baz" };
foreach(String computerName in computerNames) {
Task.Factory
.StartNew( GetComputerInfo, computerName )
.ContinueWith( ShowOutputInGui, RunOnGuiThread );
}
private ComputerInfo GetComputerInfo(String machineName) {
Task<Int64> pingTime = Task.Factory.StartNew( () => GetPingTime( machineName ) );
Task<Process[]> processes = Task.Factory.StartNew( () => System.Diagnostics.Process.GetProcesses( machineName ) );
// and loads more
Task.WaitAll( pingtime, processes, etc );
return new ComputerInfo( pingTime.Result, processes.Result, etc );
}
当我运行此代码时,我发现与我使用的旧顺序代码相比,运行需要相当长的时间。
请注意GetComputerInfo
方法中的每个任务完全独立于其周围的其他任务(例如,Ping时间可以与GetProcesses
分开计算),但是当我插入一些Stopwatch
时间调用时,我发现个别子任务(如GetProcesses
调用仅在3000ms
被调用后才启动到GetComputerInfo
- 存在一些大的延迟。
我注意到当我将外部并行调用的数量减少到GetComputerInfo
时(通过减小computerNames
数组的大小),几乎立即返回了第一个结果。某些计算机名称适用于已关闭的计算机,因此调用GetProcesses
和PingTime
需要很长时间才能超时(我的实际代码会捕获异常)。这可能是因为脱机计算机正在阻止Tasks
正在运行,而TPL自然会将其限制为我的CPU硬件线程数(8)。
有没有办法告诉TPL不让内部任务(例如GetProcesses
)阻止外部任务(GetComputerInfo
)?
(我已经尝试了#34;父/子&#34;任务附件/阻止,但它并不适用于我的情况,因为我从未明确地将子任务附加到父任务,并且无论如何,父任务自然会等待Task.WaitAll
。
答案 0 :(得分:3)
我假设您在某个事件处理程序中有foreach
循环,因此首先应该将其标记为async
,以便您可以以异步方式调用其他人。之后,您应该引入GetComputerInfo
来async
all the way down。
您的代码还有其他陷阱:StartNew
is dangerous,因为它使用Current
调度程序执行任务,而不是Default
(因此您需要其他重载)。不幸的是,那个重载需要更多的参数,所以代码不会那么简单。好消息是你仍然需要重载告诉线程池您的任务是否已经运行所以它应该为它们使用专用线程:
TaskCreationOptions.LongRunning
指定任务将是一个长时间运行的粗粒度操作,涉及比细粒度系统更少,更大的组件。它提供了
TaskScheduler
的提示,即可能需要超额认购。Oversubscription允许您创建比可用硬件线程数更多的线程。它还向任务调度程序提供任务可能需要额外线程的提示这样它就不会阻止本地线程池队列中其他线程或工作项的前进。
此外,您应该避免使用WaitAll
方法,因为它是一个阻塞操作,因此您可以使用1
线程来完成实际工作。您可能想要使用WhenAll
。
最后,要返回ComputerInfo
结果,您可以使用TaskCompletionSource
用法继续,因此您的代码可能是这样的(取消逻辑也已添加):
using System.Diagnostics;
// handle event in fire-and-forget manner
async void btn_Click(object sender, EventArgs e)
{
var computerNames = { "foo", "bar", "baz" };
foreach(String computerName in computerNames)
{
var compCancelSource = new CancellationTokenSource();
// asynchronically wait for next computer info
var compInfo = await GetComputerInfo(computerName, compCancelSource. Token);
// We are in UI context here
ShowOutputInGui(compInfo);
RunOnGuiThread(compInfo);
}
}
private Task<ComputerInfo> GetComputerInfo(String machineName, CancellationToken token)
{
var pingTime = Task.Factory.StartNew(
// action to run
() => GetPingTime(machineName),
//token to cancel
token,
// notify the thread pool that this task could take a long time to run,
// so the new thread probably will be used for it
TaskCreationOptions.LongRunning,
// execute all the job in a thread pool
TaskScheduler.Default);
var processes = Task.Run(() => Process.GetProcesses(machineName), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
// and loads more
await Task.WhenAll(pingtime, processes, etc);
return new ComputerInfo(pingTime.Result, processes.Result, etc);
//var tcs = new TaskCompletionSource<ComputerInfo>();
//Task.WhenAll(pingtime, processes, etc)
// .ContinueWith(aggregateTask =>
// if (aggregateTask.IsCompleted)
// {
// tcs.SetResult(new ComputerInfo(
// aggregateTask.Result[0],
// aggregateTask.Result[1],
// etc));
// }
// else
// {
// // cancel or error handling
// });
// return the awaitable task
//return tcs.Task;
}