在后台运行长时间运行的并行任务,同时允许小异步任务更新前台

时间:2017-02-07 15:42:04

标签: c# wpf task-parallel-library

我有大约10 000 000个任务,每个任务需要1-10秒才能完成。我在一个功能强大的服务器上运行这些任务,使用50个不同的线程,每个线程选择第一个未完成的任务,运行它并重复。

的伪代码:

for i = 0 to 50:
    run a new thread:
        while True:
            task = first available task
            if no available tasks: exit thread
            run task

使用此代码,我可以在任意给定数量的线程上以并行方式运行所有任务。

实际上,代码使用C#的Task.WhenAll,看起来像这样:

ServicePointManager.DefaultConnectionLimit = threadCount; //Allow more HTTP request simultaneously
var currentIndex = -1;
var threads = new List<Task>(); //List of threads
for (int i = 0; i < threadCount; i++) //Generate the threads
{
    var wc = CreateWebClient();
    threads.Add(Task.Run(() =>
    {
        while (true) //Each thread should loop, picking the first available task, and executing it.
        {
            var index = Interlocked.Increment(ref currentIndex);
            if (index >= tasks.Count) break;
            var task = tasks[index];
            RunTask(conn, wc, task, port);
        }
    }));
}

await Task.WhenAll(threads);

这就像我想要的那样,但我有一个问题:因为这段代码需要花费很多时间才能运行,所以我希望用户看到一些进展。进度显示在彩色位图(表示矩阵)中,并且还需要一些时间来生成(几秒钟)。

因此,我想在后台线程上生成此可视化。但是这个其他后台线程永远不会执行。我怀疑它是使用与并行代码相同的线程池,因此排队,并且在并行代码实际完成之前不会执行。 (而且有点太晚了。)

以下是我如何生成进度可视化的示例:

private async void Refresh_Button_Clicked(object sender, RoutedEventArgs e)
{
    var bitmap = await Task.Run(() => // <<< This task is never executed!
    {
        //bla, bla, various database calls, and generating a relatively large bitmap
    });

    //Convert the bitmap into a WPF image, and update the GUI
    VisualizationImage = BitmapToImageSource(bitmap);
}

那么,我怎样才能最好地解决这个问题呢?我可以创建一个Task的列表,其中每个Task代表我的一个任务,并使用Parallel.Invoke运行它们,并选择另一个线程池(我认为)。但是后来我必须生成1000万个Task个对象,而不仅仅是50个Task个对象,这些对象贯穿我的一系列要做的事情。听起来它使用的RAM比必要的多得多。任何聪明的解决方案吗?

编辑: 正如Panagiotis Kanavos在他的一条评论中所说,我尝试用ActionBlock替换我的一些循环逻辑,如下所示:

// Create an ActionBlock<int> that performs some work. 
var workerBlock = new ActionBlock<ZoneTask>(
t =>
{
    var wc = CreateWebClient(); //This probably generates some unnecessary overhead, but that's a problem I can solve later.
    RunTask(conn, wc, t, port);
},
// Specify a maximum degree of parallelism. 
new ExecutionDataflowBlockOptions
{
    MaxDegreeOfParallelism = threadCount
});

foreach (var t in tasks) //Note: the objects in the tasks array are not Task objects
    workerBlock.Post(t);
workerBlock.Complete();

await workerBlock.Completion;

注意:RunTask只使用WebClient执行Web请求,并解析结果。它没有任何东西可以造成死锁。

这似乎是旧的并行代码,除了它需要一两分钟来完成初始foreach循环来发布任务。这种延迟真的值得吗?

尽管如此,我的进度任务似乎仍被阻止。忽略进度&lt; T>建议现在,因为这个减少的代码仍然遇到同样的问题:

private async void Refresh_Button_Clicked(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("This happens");
    var bitmap = await Task.Run(() =>
    {
        Debug.WriteLine("This does not!");
        //Still doing some work here, so it's not optimized away.
    };

    VisualizationImage = BitmapToImageSource(bitmap);
}

因此,只要并行任务正在运行,它看起来仍然不会执行新任务。我甚至将“MaxDegreeOfParallelism”从50减少到5(在24核服务器上),看看Peter Ritchie的建议是否正确,但没有变化。还有其他建议吗?

另一个编辑:

问题似乎是我用所有同时阻塞的I / O调用重载了线程池。我用HttpClient及其异步函数替换了WebClient,现在一切似乎都运行良好。

感谢大家提出的好建议!虽然并非所有这些都直接解决了问题,但我确信它们都改进了我的代码。 :)

1 个答案:

答案 0 :(得分:1)

.NET已经提供了一种机制来报告IProgress< T>Progress< T>实施的进度。

IProgress接口允许客户端使用Report(T)类发布消息,而不必担心线程。该实现确保在适当的线程中处理消息,例如UI线程。通过使用简单的IProgress< T>接口,后台方法与处理消息的任何人分离。

您可以在Async in 4.5: Enabling Progress and Cancellation in Async APIs文章中找到更多信息。取消和进度API不是特定于TPL。即使是原始线程,它们也可用于简化取消和报告。

进度和LT; T&GT;处理创建它的线程上的消息。这可以通过在实例化类时传递处理委托,或者通过订阅事件来完成。复制文章:

private async void Start_Button_Click(object sender, RoutedEventArgs e)
{
    //construct Progress<T>, passing ReportProgress as the Action<T> 
    var progressIndicator = new Progress<int>(ReportProgress);
    //call async method
    int uploads=await UploadPicturesAsync(GenerateTestImages(), progressIndicator);
}

其中ReportProgress是接受int参数的方法。它还可以接受报告已完成工作,消息等的复杂类。

异步方法只需要使用IProgress.Report,例如:

async Task<int> UploadPicturesAsync(List<Image> imageList, IProgress<int> progress)
{
        int totalCount = imageList.Count;
        int processCount = await Task.Run<int>(() =>
        {
            int tempCount = 0;
            foreach (var image in imageList)
            {
                //await the processing and uploading logic here
                int processed = await UploadAndProcessAsync(image);
                if (progress != null)
                {
                    progress.Report((tempCount * 100 / totalCount));
                }
                tempCount++;
            }

            return tempCount;
        });
        return processCount;
}

将背景方法与接收和处理进度消息的人分开。