为什么这个Parallel.ForEach代码会冻结程序?

时间:2011-12-03 03:46:03

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

更多新手问题:

这段代码从主窗口的列表中抓取了许多代理(我无法弄清楚如何在不同的函数之间使变量可用)并检查每个代理(简单的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.

6 个答案:

答案 0 :(得分:18)

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

更新:或者,你可以简单地创建一个新的线程手册并开始内部处理,就像我看到的那样。这也没有错。

另外,正如Jim Mischel指出的那样,您正在同时访问多个线程的列表,因此存在竞争条件。将ConcurrentBag替换为List,或者在每次访问时将列表包装在lock语句中。

答案 1 :(得分:9)

避免在使用Parallel语句时无法写入UI线程的问题的好方法是使用Task Factory和委托,请参阅以下代码,我使用它来迭代一系列文件目录,并在并行的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);
        }))
);

执行实际用户界面的方法发生了变化:

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";
        }
    }
}

希望这有帮助!

答案 2 :(得分:3)

如果有人好奇,我有点想通了,但我不确定这是不是很好的编程或任何方式来处理这个问题。

我创建了一个新的线程:

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

并收起do_checks()中的所有并行内容。

似乎没事。

答案 3 :(得分:2)

如果你想在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

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:1)

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

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

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

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