使用TPL并行阻塞IO操作

时间:2017-02-25 07:12:30

标签: c# multithreading task-parallel-library

前言:我知道使用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数组的大小),几乎立即返回了第一个结果。某些计算机名称适用于已关闭的计算机,因此调用GetProcessesPingTime需要很长时间才能超时(我的实际代码会捕获异常)。这可能是因为脱机计算机正在阻止Tasks正在运行,而TPL自然会将其限制为我的CPU硬件线程数(8)。

有没有办法告诉TPL不让内部任务(例如GetProcesses)阻止外部任务(GetComputerInfo)?

(我已经尝试了#34;父/子&#34;任务附件/阻止,但它并不适用于我的情况,因为我从未明确地将子任务附加到父任务,并且无论如何,父任务自然会等待Task.WaitAll

1 个答案:

答案 0 :(得分:3)

我假设您在某个事件处理程序中有foreach循环,因此首先应该将其标记为async,以便您可以以异步方式调用其他人。之后,您应该引入GetComputerInfoasync 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;
}