【问题标题】:Why does this Parallel.ForEach code freeze the program up?为什么这个 Parallel.ForEach 代码会冻结程序?
【发布时间】:2012-01-12 00:21:12
【问题描述】:

更多新手问题:

这段代码从主窗口的列表中抓取了一些代理(我不知道如何使变量在不同的函数之间可用)并检查每个代理(简单的httpwebrequest)然后添加将它们添加到一个名为 finishedProxies 的列表中。

由于某种原因,当我按下开始按钮时,整个程序挂断了。我的印象是 Parallel 会为每个操作创建单独的线程,让 UI 线程保持独立以便响应?

private void start_Click(object sender, RoutedEventArgs e)
        {
            // Populate a list of proxies
            List<string> proxies = new List<string>();
            List<string> finishedProxies = new List<string>();

            foreach (string proxy in proxiesList.Items)
            {
                proxies.Add(proxy);
            }

            Parallel.ForEach<string>(proxies, (i) =>
            {
                string checkResult;
                checkResult = checkProxy(i);

                finishedProxies.Add(checkResult);
                // update ui
                /*
                 status.Dispatcher.Invoke(
                  System.Windows.Threading.DispatcherPriority.Normal,
                  new Action(
                    delegate()
                    {
                        status.Content = "hello" + checkResult;
                    }
                )); */
                // update ui finished

                
                //Console.WriteLine("[{0}] F({1}) = {2}", Thread.CurrentThread.Name, i, CalculateFibonacciNumber(i));
            });

            
        }

我尝试使用被注释掉的代码来更改Parallel.Foreach 内的 UI,它会在按下开始按钮后使程序冻结。它以前对我有用,但我使用了 Thread 类。

如何从 Parallel.Foreach 内部更新 UI 以及如何使 Parallel.Foreach 工作,以免 UI 在工作时冻结?

Here's the whole code.

【问题讨论】:

  • 你正在用调用请求来攻击 UI 线程,它不再有时间去做它的常规职责了。就像重绘 UI 一样。至少降低背景的优先级。
  • @dsp_099б 你在“它之前对我有用,但我使用了 Thread 类”下是什么意思?
  • 任何其他人来这里:我怀疑这个例子甚至不应该使用 Parallel.ForEach。除非使用它有明显的好处,否则只需创建一个后台线程,然后在其中按顺序处理项目。然后任何返回 UI 线程的调用也将按顺序完成,避免同时请求使 UI 线程过载。 (或者使用更现代的解决方案,例如 async/await,它们也一次执行一个操作。)

标签: c# multithreading visual-studio-2010 user-interface task-parallel-library


【解决方案1】:

您不得在 UI 线程中启动并行处理。请参阅this page 中“避免在 UI 线程上执行并行循环”标题下的示例。

更新: 或者,您可以简单地手动创建一个新线程并在其中开始处理,正如我看到的那样。这也没什么不好。

此外,正如 Jim Mischel 所指出的,您同时从多个线程访问列表,因此存在竞争条件。要么用 ConcurrentBag 替换 List,要么在每次访问列表时将列表包装在 lock 语句中。

【讨论】:

  • “用ConcurrentBag 代替List”是什么意思? ...您的意思是“将List 替换为ConcurrentBag”吗?
  • @Fulproof:我认为正确的英文是“substitute A for B” == “substitute B with A”。
  • "substitute one thing (A) for another (B)" == "用 B 代替 A" == (A) "发生或执行另一个的功能" (B)。你能参考一下你的用法吗?
  • Jon,你是对的,但由于这是一个国际论坛,如果使用更直接的表达方式会更好。我认为“要么使用 ConcurrentBag 而不是 List 要么......”对每个人来说都更容易阅读。吸取教训,思考。 :)
【解决方案2】:

避免使用Parallel语句时无法写入UI线程的问题的一个好方法是使用Task Factory和delegate,参见下面的代码,我用它来迭代一个系列中的一系列文件目录,并在Parallel.ForEach 循环中处理它们,在处理完每个文件后,UI 线程会发出信号并更新:

var files = GetFiles(directoryToScan);

tokenSource = new CancellationTokenSource();
CancellationToken ct = tokenSource.Token;

Task task = Task.Factory.StartNew(delegate
{
    // Were we already canceled?
    ct.ThrowIfCancellationRequested();

    Parallel.ForEach(files, currentFile =>
    {
        // Poll on this property if you have to do 
        // other cleanup before throwing. 
        if (ct.IsCancellationRequested)
        {
            // Clean up here, then...
            ct.ThrowIfCancellationRequested();
        }

        ProcessFile(directoryToScan, currentFile, directoryToOutput);

        // Update calling thread's UI
        BeginInvoke((Action)(() =>
        {
            WriteProgress(currentFile);
        }));
    });
}, tokenSource.Token); // Pass same token to StartNew.

task.ContinueWith((t) =>
        BeginInvoke((Action)(() =>
        {
            SignalCompletion(sw);
        }))
);

以及执行实际 UI 更改的方法:

void WriteProgress(string fileName)
{
    progressBar.Visible = true;
    lblResizeProgressAmount.Visible = true;
    lblResizeProgress.Visible = true;

    progressBar.Value += 1;
    Interlocked.Increment(ref counter);
    lblResizeProgressAmount.Text = counter.ToString();
        
    ListViewItem lvi = new ListViewItem(fileName);
    listView1.Items.Add(lvi);
    listView1.FullRowSelect = true;
}

private void SignalCompletion(Stopwatch sw)
{
    sw.Stop();
        
    if (tokenSource.IsCancellationRequested)
    {
        InitializeFields();
        lblFinished.Visible = true;
        lblFinished.Text = String.Format("Processing was cancelled after {0}", sw.Elapsed.ToString());
    }
    else
    {
        lblFinished.Visible = true;
        if (counter > 0)
        {
            lblFinished.Text = String.Format("Resized {0} images in {1}", counter, sw.Elapsed.ToString());
        }
        else
        {
            lblFinished.Text = "Nothing to resize";
        }
    }
}

希望这会有所帮助!

【讨论】:

  • 在 UI 线程上使用 BeginInvoke 可能会进一步提高性能,然后您不必等待它更新,就像您当前使用 Invoke 时所做的那样。当然,这可能需要在WriteProgress...
  • UI 线程当前没有被锁定,它保持完全响应,所以我不确定这有什么帮助?但我会尝试看看有什么不同,在我的场景中锁定对象本身不是问题。
  • 忽略我关于锁的评论 - 我在考虑运行BeginInvoke 的多个线程,但由于它们都在 UI 线程上被调用,所以不能有任何重新进入。我的意思是Parallel.ForEach 中的每个线程都必须等待Invoke 完成,这会减慢速度。使用BeginInvoke,UI 更新将排队到 UI 线程并异步运行。
  • 现在正在设置一个测试用例进行比较,等我得到结果再回来,提前谢谢!
  • 刚刚完成我的测试,每次调用都使用相同数量的文件来调整大小。 Invoke 给了我从 9.6 秒到 8.2 最快的结果(只有一次),而 BeginInvoke 更快,从 8.1 最快几倍到 8.5 最慢。感谢@SimonMcKenzie 的提示!
【解决方案3】:

如果有人好奇,我想通了,但我不确定这是否是好的编程或任何解决问题的方法。

我像这样创建了一个新线程:

Thread t = new Thread(do_checks);
t.Start();

并把所有并行的东西放在 do_checks() 中。

看起来还不错。

【讨论】:

  • 这个很有帮助,我试过这个,简单有效,谢谢!!
【解决方案4】:

您的代码的一个问题是您同时从多个线程调用FinishedProxies.Add。这会导致问题,因为List&lt;T&gt; 不是线程安全的。您需要使用锁或其他同步原语来保护它,或者使用并发集合。

这是否会导致 UI 锁定,我不知道。没有更多信息,很难说。如果proxies 列表很长并且checkProxy 执行时间不长,那么您的任务将全部排在Invoke 调用后面。这将导致一大堆待处理的 UI 更新。这将锁定 UI,因为 UI 线程正忙于为那些排队的请求提供服务。

【讨论】:

  • 另外,我每次加载大约 10 个代理来运行测试,所以不会太多;我在 parallel.foreach 之后添加了一行,将标签框更改为“检查器已完成!”当我按下复选按钮时,程序会等待,直到所有进程都在更新框之前完成,所以就像是的,它们都同时运行,但如果这有任何意义,就好像它们在同一个线程中同时运行,因为只是做来自同一个 ui 线程的 httpwebrequest 会导致它以完全相同的方式挂断。
【解决方案5】:

这就是我认为您的代码库中可能发生的事情。

正常场景:您点击按钮。不要使用Parallel.Foreach 循环。使用 Dispatcher 类并推送代码以在后台的单独线程上运行。一旦后台线程完成处理,它将调用主 UI 线程来更新 UI。在这种情况下,后台线程(通过 Dispatcher 调用)知道主 UI 线程,它需要回调。或者简单地说主 UI 线程有自己的身份。

使用 Parallel.Foreach 循环:一旦调用 Paralle.Foreach 循环,框架就会使用 threadpool 线程。 ThreadPool 线程是随机选择的,执行代码不应该对所选线程的身份做出任何假设。在原始代码中,通过Parallel.Foreach 循环调用的调度程序线程很可能无法找出与之关联的线程。当您使用显式线程时,它可以正常工作,因为显式线程有自己的身份,可以被执行代码依赖。

理想情况下,如果您主要关心的是保持 UI 响应,那么您应该首先使用 Dispatcher 类在后台线程中推送代码,然后在其中使用您想要加速整体执行的任何逻辑。

【讨论】:

    【解决方案6】:

    如果您想在 GUI 控件中使用 parallel foreach,例如按钮单击等 然后将并行 foreach 放入 Task.Factory.StartNew 喜欢

    private void start_Click(object sender, EventArgs e)
            {
                    await Task.Factory.StartNew(() =>
                         Parallel.ForEach(YourArrayList, (ArraySingleValue) =>
                         {
    
                    Console.WriteLine("your background process code goes here for:"+ArraySingleValue);
                         })
                        );
        }//func end
    

    它将解决冻结/卡住或挂起问题

    【讨论】:

    猜你喜欢
    • 2011-11-13
    • 2012-07-13
    • 2020-02-23
    • 2022-01-23
    • 1970-01-01
    • 1970-01-01
    • 2013-11-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多