如何等待并行任务完成

Posted

技术标签:

【中文标题】如何等待并行任务完成【英文标题】:how to await until parallel task done 【发布时间】:2021-10-21 20:03:49 【问题描述】:

我是 C# 新手。我正在编写窗口表单,用于根据其扩展名对文件夹内的文件进行排序。 使用一个文件夹执行任务时可行,但我将其修改为从列表框中的多个项目执行。

private async void startBtn_Click(object sender, EventArgs e)

    if (this.listBox1.Items.Count != 0)
    
        this.statusLabel.ForeColor = Color.DodgerBlue;
        this.statusLabel.Text = "Sorting";
        this.startBtn.Enabled = false;
        this.removeOtherCheck.Enabled = false;
        this.workerCounter.Enabled = false;
        foreach (var item in this.listBox1.Items)
        
            if (Directory.Exists( (string)item ))
            
               await Task.Run(() => startTask((string)item, this.removeOtherCheck.Checked, this.workerCounter.TabIndex));
            
        
        FinishedTask();
    
    private async void startTask(string path, bool removeOtherFlag, int worker)
    
        await Task.Run(() => doJob(path, removeOtherFlag, worker));
    
    private void FinishedTask()
    
        this.statusLabel.ForeColor = Color.LimeGreen;
        this.statusLabel.Text = "Finished";
        this.startBtn.Enabled = true;
        this.removeOtherCheck.Enabled = true;
        this.workerCounter.Enabled = true;
    
//this method are seperate but I put it here so you guy can see it
        public static void doJob(string directory,bool removeOtherFlag, int worker)
        
            // loop in directory
            createFolders(directory); // create extension folder
            string[] all_files = Directory.GetFiles(directory);
            Parallel.ForEach(all_files, new ParallelOptions  MaxDegreeOfParallelism = worker , (item) => multiprocessingFiles(item));

            
        if(removeOtherFlag == true) deleteOtherFolder(Path.Combine(directory,"other"));
        removeEmptyFolder(directory); // remove empty extension folder
    

我将解释我的任务。 首先我在进程启动时单击开始按钮,它将禁用所有按钮,然后循环列表框中的每个项目并对文件夹中的所有文件进行排序。 一切完成后,它会显示已完成的标签并启用所有按钮。

问题是,它会在 removeEmptyFolder() 执行此操作之前显示已完成的标签并重新启用所有按钮。

我尝试将 Parallel.forEach 更改为 Parallel.For 但它没有做我的事。

编辑 谢谢大家的回答。 感谢 Harald Coppoolse 的结论。 Paulo Morgado 是对的。

我删除 startTask 并更改

await Task.Run(() => startTask((string)item, this.removeOtherCheck.Checked, this.workerCounter.TabIndex));

await Task.Run(() => doJob(item, this.removeOtherCheck.Checked, this.workerCounter.TabIndex));

现在一切都如我所愿。 谢谢大家。

【问题讨论】:

multiprocessingFilesdeleteOtherFolderremoveEmptyFolder的实现是什么?它们是异步的吗? 使用Parallel.ForEach 确实不太可能提高代码运行的速度。访问磁盘比对一堆字符串进行排序要慢几个数量级。除非执行的工作比排序多。 deleteOtherFolder,removeEmptyFolder 不是异步的,需要等到 parallel.ForEach 完成 您需要向我们展示multiprocessingFiles 方法。附带说明一下,C# 中的方法名称遵循 PascalCase 模式。 MultiprocessingFiles 是正确的。 没关系,这是您的问题:private async void startTask。 Avoid async void. 【参考方案1】:

您是否正在寻找类似Task.WaitAll(params Task[] tasks) 的东西?

然后您可以将doJob 转换为异步, 将Parallel.ForEach 替换为:

var tasks = all_files.Select(f => Task.Run(multiprocessingFiles(f)));
await Task.WaitAll(tasks);

或者如果你想限制最大并行任务: 通过Task t = new Task(() => doSometing()); 创建非运行任务 并以t.Start();await Task.WaitAll(batch); 批量启动它们

但是,正如 cmets 中指出的那样,我认为这不会提高性能。

【讨论】:

在此之前,它像python线程池一样同时对目录中的5个文件进行排序。但该版本仅适用于输入文本中的一个目录。我将输入文本更改为列表框,并对列表框中的所有项目进行排序。也许不使用 Parallel 会解决它。【参考方案2】:

在 winforms 中,当您使用 async-await 时,仅当您需要启动一个额外的线程来执行一项需要相当长的时间的工作时才使用Task.Run,比您希望程序冻结的时间长。您不会为异步方法调用 Task.Run,​​因为正确设计的异步方法不会冻结您的 UI。

一旦执行异步方法的线程看到await,它就不会等待过程完成,而是向上调用堆栈执行代码,直到看到等待。再次向上调用堆栈,并执行代码,直到看到等待等。

结果:如果异步事件处理程序仅使用异步方法,您的 UI 将不会冻结。除非其中一种异步方法进行了一些繁重的计算(= 使用长的非异步功能)

如果您希望在这些繁重的计算过程中保持 UI 响应,您应该创建一个异步方法,使用 Task.Run 调用具有繁重计算的过程。

但我做到了!

是的,你做到了,但你也有一个异步函数,它使用 Task.Run 调用另一个异步函数!这不是必需的。

顺便说一句,坚持编码约定可能是个好主意,例如使用驼峰式大小写和在异步方法中添加 Async。这将有助于未来的读者理解您的代码。

void DoJob(...)...     // your time consuming method with heavy calculations

DoJob 的异步版本。显然在 DoJob 中我们不能做任何异步操作,所以这个版本必须调用 DoJob:

async Task DoJobAsync(...)

    // call the non-async version on a separate thread:
    await Task.Run( () => DoJob(...)).ConfigureAwait(false);

我将过程命名为 DoJobAsync,因为前置条件和后置条件与 DoJob 中的相同。这匹配所有其他非异步/异步对:Stream.Write 和 Stream.WriteAsync、File.Read 和 File.ReadAsync、Queryable.ToList 和 Queryable.ToListAsync。

如果在未来的版本中 DoJobAsync 可以使用一些异步方法,例如因为有人发明了一个过程 MultiprocessingFilesAsync,那么只有 DoJobAsync 需要更改,其他人不会知道。

对于 ConfigureAwait(false),请参阅 Stephen Cleary 的 [异步编程最佳实践][1]

顺便说一句:您确定在 Parallel.Foreach 中进行磁盘处理是明智的吗?您是否测量过它比标准 foreach 更有效?

无论如何,显然您有一些用户界面元素将启动“执行工作”的过程 在处理此工作时,您希望向操作员提供一些视觉信息,即工作正忙,并且您希望告诉操作员工作已经完成。你已经发明了FinishedTask,为什么不创建一个StartTask(并使用更好的描述性名称:方法的动词)

void ShowTaskStarted() ...
void ShowTaskCompleted() ...    // was: FinishedTask

显然 listBox1 中的项目是字符串。

async Task ProcessItems(IEnumerable<string> itemsToProcess,
           bool removeOtherFlag, int worker)  // TODO: invent proper name

    foreach (string itemToProcess in itemsToProcess)
    
        await DoJobAsync(itemToProcess, removeOtherFlag, worker);
    

如果您认为可以在第一个作业完成之前开始第二个 DoJobAsync,那么在前一个作业完成之前开始下一个作业:

List<Task> jobs = new List<Task>();
foreach (string itemToProcess in itemsToProcess)

    Task jobTask = DoJobAsync(itemToProcess, removeOtherFlag, worker);
    jobs.Add(jobTask);

await Task.WhenAll(jobs);

因为您的工作是一些磁盘处理,所以我不确定这样做是否明智,但请记住这一点,例如,如果您正在启动可以在前一个尚未完成的情况下启动的任务。

直到现在,程序还不知道数据来自列表框,也不知道来自 CheckBox 或 TabControl。我这样做是因为如果以后您决定更改数据源,例如 ComboBox 或 DataGridView,这些过程不必更改。以下是了解您的表单的第一个过程。

这是对模型(= 数据以及数据的处理方式)和视图(= 数据的显示方式)的严格分离。

将模型与视图分离的其他优点:

您可以将模型放在表单之外(表单具有模型 = 聚合或组合)。这样一来,这个模型可以被多个表单使用。 可以在没有表单的情况下对单独的模型进行单元测试 如果你给模型一个接口,你可以改变模型,而不必改变表单。 使用该界面,您可以在开发界面时模拟模型。

因此请考虑始终将模型与视图分开。

视图

我不知道你的列表框中有什么,所以我不能给下一个过程一个正确的名字。

async Task ProcessListBox()          // TODO: proper name

    ShowTaskStarted();

    var itemsToProcess = this.ListBox1.Items.ToList();
    var removeOtherCheck = this.removeOtherCheck.Checked;
    var worker = this.workerCounter.TabIndex;
    await ProcessItems(itemsToProcess, removeOtherCheck, worker);

    ShowTaskCompleted();

最后:您想在操作员单击开始按钮时处理 ListBox:

async void StartButton_Clicked(object sender, ...)

    await ProcessLisbBox().ConfigureAwait(false);

因为我将动作(必须做什么)与触发器(什么时候必须做)分开,所以下面将是一个单一的方法:

async void MenuItem_EditStart_Clicked(object sender, ...)

    await ProcessLisbBox().ConfigureAwait(false);

或者也许你想在加载表单时这样做:添加一行就足够了

结论

使用 async-await 时:一路异步。

异步方法总是调用其他方法的异步版本

如果耗时的方法没有异步替代方案,请创建一个,并让它Task.Run 非异步版本。在此过程之外,没有人知道启动了一个单独的线程。

约定:异步方法后缀为 Async

不要制定一个可以做所有事情的程序。较小的程序更容易理解、更容易重用、更容易更改和单元测试

将模型与视图分离,最好使用界面

CamelCase 你的方法,使用动词。 [1]:https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

【讨论】:

以上是关于如何等待并行任务完成的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程基础

一个巧妙的解决并行计算里两个任务等待问题的方法

异步等待和并行

CMD - 等待并行命令行完成?

背水一战 Windows 10 (118) - 后台任务: 后台下载任务(任务分组,并行或串行执行,组完成后通知)

真正的并行处理[关闭]