在.NET 4.0中使用Parallel.Foreach和Task

4
我开始尝试为Windows表单添加进度条,以更新在Parallel.Foreach循环中运行的代码的进度。 为此,UI线程必须可用于更新进度条。 我使用了一个Task来运行Parallel.Foreach循环,以允许UI线程更新进度条。
在Parallel.Foreach循环中执行的工作相当密集。 在使用Task运行程序的可执行文件后(而非在Visual Studio中进行调试),程序变得无响应。 如果我不使用Task运行程序,则情况并非如此。 我注意到两种情况之间的关键区别是:在没有Task的情况下运行程序时,程序占用大约80%的CPU,而在有Task的情况下只占用大约5%。
private void btnGenerate_Click(object sender, EventArgs e)
    {
        var list = GenerateList();
        int value = 0;
        var progressLock = new object ();

        progressBar1.Maximum = list.Count();

        Task t = new Task(() => Parallel.ForEach (list, item =>
        {
                DoWork ();
                lock (progressLock)
                {
                    value += 1;
                }
        }));

        t.Start();

        while (!t.IsCompleted)
        {
            progressBar1.Value = value;
            Thread.Sleep (100);
        }
    }

附注:我知道

这里是关于IT技术的内容。
 Interlocked.Increment(ref int___);

锁的替代品是有效的吗?这与IT技术有关。

我的问题有三个方面:

1.) 当负载较小时,为什么带有任务的程序会变得无响应?

2.) 使用Task来运行Parallel.Foreach是否会将Parallel.Foreach的线程池限制在仅运行任务的线程上?

3.) 是否有一种方法可以使UI线程响应而不需要使用取消标记并在0.1秒的持续时间内休眠?

非常感谢您的任何帮助或想法,我已经花费了很多时间研究此问题。如果我违反了任何发布格式或规则,我也向大家道歉。我尽力遵守规定,但可能会漏掉某些内容。


在UI线程中循环等待任务完成就像在UI线程上执行它一样(甚至更糟)。请查看BackgroundWorkerasync - SimpleVar
如果DoWork()访问了任何UI元素,将会导致死锁发生。 - Johnathon Sullinger
JohnathonSullinger - DoWork() 不访问任何 UI 元素。@Yorye Nathan - 你能详细说明或举个例子吗?我今天之前从未做过任何与多线程相关的事情。 - EN Speer
也许与这个有关?https://dev59.com/7WQm5IYBdhLWcg3w6CWr - dbc
显示剩余2条评论
1个回答

6
您可以通过使用内置的Invoke方法来极大地简化代码,该方法在所属的Windows同步上下文中调用委托。
来自MSDN
执行指定的委托,该委托在控件的基础窗口句柄所在的线程上运行。如果当前控件的基础窗口句柄尚不存在,则Invoke方法会沿着控件的父级链搜索,直到找到具有窗口句柄的控件或窗体为止。
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    string[] GenerateList() => new string[500];
    void DoWork()
    {
        Thread.Sleep(50);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var list = GenerateList();
        progressBar1.Maximum = list.Length;

        Task.Run(() => Parallel.ForEach(list, item =>
        {
            DoWork();

            // Update the progress bar on the Synchronization Context that owns this Form.
            this.Invoke(new Action(() => this.progressBar1.Value++));
        }));
    }
}

这将在同一UI线程上调用Action委托,从任务中调用。现在尝试回答你的问题:
1.) 当负载较小时,为什么带有任务的程序会失去响应?
我不是100%确定,但这可能与您在UI线程上锁定成员有关。如果负载较小,则锁定将更频繁地发生,可能导致UI线程在增加进度条时“挂起”。
您还在运行一个while循环,它每100毫秒使UI线程睡眠。由于该while循环,您会看到UI挂起。
2.) 使用Task运行Parallel.ForEach是否将Parallel.ForEach的线程池限制为仅运行任务的线程?
不会。在Parallel.ForEach调用内部将创建几个任务。底层的ForEach使用partitioner来分配工作,并且不会创建多余的任务。它创建批次的任务并处理批次。

3.) 是否有一种方法可以使UI线程响应而不使用取消令牌来代替休眠0.1秒的时间?

我通过删除while循环并使用Invoke方法直接在UI线程上执行lambda表达式来处理这个问题。


这是一个很好的 .Net 4.5 解决方案,但问题标记为 .net-4.0。OP 可以使用 BeginInvoke() 来更新进度条吗? - dbc
感谢您的回复!不幸的是,我正在处理的项目使用的是.NET 4.0,据我所知,它不支持Progress(T)类。 编辑:没有看到已经有人发现了这个问题。 - EN Speer
1
@ENSpeer 在4.0中,Microsoft提供了Progres<T>,您只需要安装NuGet包Microsoft.Bcl即可。 - Scott Chamberlain
很抱歉,但我必须对建议“在UI线程的while循环中使用DoEvents()总是好的”做出-1的评价,几乎从不在循环中使用DoEvents(),如果您发现需要在循环中使用DoEvents(),正确的建议是“您应该重写代码,以避免在UI线程上执行while循环”。您在答案中提到了正确的方法(使用Invoke),但不应该推荐使用DoEvents() - Scott Chamberlain
Hans Passant的回答(我认为他是C# UI主题方面这个网站上最好的专家之一)很好地解决了它。 - Scott Chamberlain
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接