c# TaskFactory ContinueWhenAll 在所有任务完成之前意外运行

Posted

技术标签:

【中文标题】c# TaskFactory ContinueWhenAll 在所有任务完成之前意外运行【英文标题】:c# TaskFactory ContinueWhenAll unexpectedly running before all tasks complete 【发布时间】:2020-08-31 15:03:17 【问题描述】:

我有一个 C# 数据处理程序(.NET 4.6.2;用于 UI 的 WinForms)。我遇到了一种奇怪的情况,计算机速度似乎导致 Task.Factory.ContinueWhenAll 比预期更早地运行,或者某些任务在实际运行之前报告完成。正如你在下面看到的,我有一个最多 390 个任务的队列,一次队列中不超过 4 个。当所有任务都完成时,状态标签会更新为完成。 ScoreManager 涉及从数据库中检索信息、执行多个客户端计算以及保存到 Excel 文件。

在我的笔记本电脑上运行程序时,一切正常;从功能更强大的工作站运行时,我遇到了这个问题。不幸的是,由于组织限制,我可能无法在工作站上直接使用 Visual Studio 进行调试。有谁知道是什么原因导致我对此进行调查?

private void button1_Click(object sender, EventArgs e)

    int startingIndex = cbStarting.SelectedIndex;
    int endingIndex = cbEnding.SelectedIndex;
    lblStatus.Text = "Running";
    if (endingIndex < startingIndex)
    
        MessageBox.Show("Ending must be further down the list than starting.");
        return;
    
    List<string> lItems = new List<string>();
    for (int i = startingIndex; i <= endingIndex; i++)
    
        lItems.Add(cbStarting.Items[i].ToString());
    

    System.IO.Directory.CreateDirectory(cbMonth.SelectedItem.ToString());

    ThreadPool.SetMaxThreads(4, 4);
    List<Task<ScoreResult>> tasks = new List<Task<ScoreResult>>();
    for (int i = startingIndex; i <= endingIndex; i++)
    
        ScoreManager sm = new ScoreManager(cbStarting.Items[i].ToString(),
            cbMonth.SelectedItem.ToString());
        Task<ScoreResult> task = Task.Factory.StartNew<ScoreResult>((manager) =>
            ((ScoreManager)manager).Execute(), sm);
        sm = null;
        Action<Task<ScoreResult>> itemcomplete = ((_task) =>
        
            if (_task.Result.errors.Count > 0)
            
                txtLog.Invoke((MethodInvoker)delegate
                
                    txtLog.AppendText("Item " + _task.Result.itemdetail +
                        " had errors/warnings:" + Environment.NewLine);
                );

                foreach (ErrorMessage error in _task.Result.errors)
                
                    txtLog.Invoke((MethodInvoker)delegate
                    
                        txtLog.AppendText("\t" + error.ErrorText +
                            Environment.NewLine);
                    );
                
            
            else
            
                txtLog.Invoke((MethodInvoker)delegate
                
                    txtLog.AppendText("Item " + _task.Result.itemdetail +
                     " succeeded." + Environment.NewLine);
                );

            
        );
        task.ContinueWith(itemcomplete);
        tasks.Add(task);
    
    Action<Task[]> allComplete = ((_tasks) =>
    
        lblStatus.Invoke((MethodInvoker)delegate
        
            lblStatus.Text = "Complete";
        );
    );
    Task.Factory.ContinueWhenAll<ScoreResult>(tasks.ToArray(), allComplete);

【问题讨论】:

究竟是什么让你这么想? 程序停止处理(通过任务管理器使用确认为 0,没有更多的日志条目,也没有创建更多的输出文件),并在处理约 1/3 的条目后将状态更新为完成。它似乎总是与在处理莫名其妙停止之前处理的最后一个条目相同并且显示“完成” 当然,我刚刚说过,并且在我通过向日志中添加一些任务状态信息来至少获取一些信息的新尝试中,它已经成功地超过了它停止的项目之前。 它似乎在另一个项目之后不久就停止了;有了这个,我可以看到许多任务显示为“故障” 可以加minimal reproducible example吗? 【参考方案1】:

在这里,您正在创建无需等待或观察的即发即弃的任务:

task.ContinueWith(itemcomplete);
tasks.Add(task);
Task.Factory.ContinueWhenAll<ScoreResult>(tasks.ToArray(), allComplete);

ContinueWith 方法返回一个Task。您可能需要将 allComplete 延续附加到这些任务,而不是它们的前身:

List<Task> continuations = new List<Task>();
Task continuation = task.ContinueWith(itemcomplete);
continuations.Add(continuation);
Task.Factory.ContinueWhenAll<ScoreResult>(continuations.ToArray(), allComplete);

附带说明一下,如果您使用 async/await 而不是老式的 ContinueWithInvoke((MethodInvoker) 技术,您可以使您的代码大小减半并显着提高可读性。


另外:为了控制并行度而设置ThreadPool线程数的上限是非常不可取的:

ThreadPool.SetMaxThreads(4, 4); // Don't do this!

您可以改用Parallel 类。它可以很容易地控制MaxDegreeOfParallelism

【讨论】:

感谢您的建议!我现在对 async/await 还不够熟悉,无法在我需要它的时间范围内以我需要的方式实际得到一些可行的东西——我花了一些时间(不成功)更早地尝试并在它工作时解决了这个问题.老实说,我的 .NET 背景真的多 2.0 天,所以即使是任务的整个想法也与我习惯的有点不同:D 将研究将 ContinueWith 分配给 Task 而不是使用先行词。 (另外,回复 +1,但 b/c 我的代表太低了,我猜它不会显示) @ww2406 是的,我不建议在不了解其机制的情况下跳上 async-await 马车,因为尽管它看起来很简单,但它可能非常棘手。但绝对是当今需要了解的一项非常重要的技术。【参考方案2】:

在发现状态为 IsFaulted 后,我添加了一些代码以将一些异常信息添加到日志中(https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library)。似乎问题是一个底层数据库问题,其中连接池中没有足够的连接(超时已过期。在从池中获取连接之前已经过了超时时间。这可能是因为所有池连接都在使用中并且最大池已达到大小。)-- 额外的速度允许查询更快/更频繁地触发。不完全确定为什么,因为我确实将 SqlConnection 包含在 using 子句中,但在这方面调查了一些事情。无论如何,这个问题显然和我上面想的有点不同,所以标记这个准答案。

【讨论】:

以上是关于c# TaskFactory ContinueWhenAll 在所有任务完成之前意外运行的主要内容,如果未能解决你的问题,请参考以下文章

Task和TaskFactory

taskFactory

我可以使用哪些技术来模拟与TaskFactory.FromAsync()的交互?

Thread(线程)三

转:async异步thread多线程

TaskCompletionSource的使用场景