【问题标题】:Keep track of Parallel Foreach threads跟踪并行 Foreach 线程
【发布时间】:2015-09-21 17:47:26
【问题描述】:

我目前正在编写一个简单的 WPF 文件复制应用程序,它可以并行复制文件。到目前为止效果很好!它做我想做的一切。操作的核心在以下代码块中:

Parallel.ForEach(Directory.GetFiles(dir).ToList(), file =>
{
    _destDetail.CurrOp = string.Format("Copying file: {0}", Path.GetFileName(file));
    File.Copy(file, file.Replace(_destDetail.Source, _destDetail.Dest), true);
    if (_destDetail.Progress < _destDetail.MaxProgress)
        _destDetail.Progress++;
});

我可以实现 ParallelOptions 并将最大线程数也限制为 4,但我想知道是否有办法准确跟踪每个线程在这种情况下会做什么?

例如,假设我有一部分 UI 专用于复制操作的当前“状态”。我想在Grid 中有 4 行,每行都有一个特定的线程以及它当前正在复制的文件。

我知道我可以使用Interlocked 来操作Parallel 循环之外的变量,但是我将如何从Parallel 循环的内部 跟踪特定于线程的变量并使用这些变量使 UI 在哪个线程正在处理哪个文件上保持最新?

【问题讨论】:

  • 仅供参考,File.CopyParallel.ForEach 内是个坏主意,您应该只在 CPU 绑定工作上使用 Parallel.ForEach,对于 IO 绑定工作,它的调度算法将尝试启动任务太多,并且很可能需要比您仅使用单个线程在普通foreach 循环中复制文件的时间更长。

标签: c# wpf multithreading parallel-processing


【解决方案1】:

不是直接跟踪线程,而是将 UI 绑定到每个代表进度的 ObserveableCollection&lt;ProgressDetail&gt;,然后在您的循环中让它在开始时向集合中添加一个项目,然后在结束时将其从集合中删除。

您必须注意的一件事是线程安全,ObseveableCollection 不是线程安全的,因此您必须仅以线程安全的方式与其交互,最简单的方法是添加和删除 @987654326 @UI 线程上的对象。这还有一个额外的好处,即在您创建 Progress 对象时捕获 UI 线程的 SynchronizationContext。

public ObserveableCollection<ProgressDetail> ProgressCollection {get; private set;}

public void CopyFiles(string dir)
{

    var dispatcher = Application.Current.Dispatcher;
    Parallel.ForEach(Directory.GetFiles(dir).ToList(), file =>
    {
        ProgressDetail progressDetail = null;
        dispatcher.Invoke(() => 
        {
           // We make the `Progress` object on the UI thread so it can capture the  
           // SynchronizationContext during its construction.
           progressDetail = new ProgressDetail(file);
           ProgressCollection.Add(progressDetail);
        }

        XCopy.Copy(file, file.Replace(_destDetail.Source, _destDetail.Dest), 
                   true, false, progressDetail.ProgressReporter);

        dispatcher.Invoke(() => ProgressCollection.Remove(progressDetail);
    });

}

public sealed class ProgressDetail : INotifyPropertyChanged
{
    private double _progressPercentage;

    public ProgressDetail(string fileName)
    {
        FileName = fileName;
        ProgressReporter = new Progress<double>(OnProgressReported);
    }

    public string FileName { get; private set; }
    public IProgress<double> ProgressReporter { get; private set; }
    public double ProgressPercentage
    {
        get { return _progressPercentage; }
        private set
        {
            if (value.Equals(_progressPercentage)) return;
            _progressPercentage = value;
            OnPropertyChanged();
        }
    }

    private void OnProgressReported(double progress)
    {
        ProgressPercentage = progress;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var temp = PropertyChanged;
        if(temp != null)
            temp(this, new PropertyChangedEventArgs(propertyName));
    }
}

请参阅 this answer 以获取将随进度复制的示例 XCopy 类。我假设Copy 的签名已更改为

public static void Copy(string source, string destination, bool overwrite, bool nobuffering, IProgress<double> handler)

但我将实际更改留给读者作为练习。

更新:我更新了上面的代码示例以公开一个公共属性ProgressPercentage,它可以绑定到并引发适当的事件。我还将Progress 事件的侦听移至ProgressDetail 类的内部。

【讨论】:

  • 你提到线程安全,然后你提到绑定。无论您在添加/删除集合时是否锁定集合,您都无法真正使绑定线程安全。
  • @Blindy 我没有使用任何锁定,我做了所有会导致绑定在 UI 线程上触发的修改,这就是我的代码通过使用 dispatcher.Invoke 进行添加和删除以及Progress&lt;T&gt; 用于报告进度(Progress&lt;T&gt; 将在其创建时使用SynchronizationContext 提升它的ProgressChanged,如果那是 UI 线程,那么它会在 UI 线程上引发事件)。跨度>
【解决方案2】:

关于 Parallel 库的关键在于您不了解线程 - 这非常适用于本示例。您的循环执行一些文件 IO,然后执行一些计算。当其中一个线程正在执行 IO 时,该线程未在使用中,并且可以重新用于执行与其他文件之一相关联的计算。这也是为什么最好将线程或并发任务的数量留给运行时的原因:它比你更清楚它可以使用多少。

同样写成_destDetail.Progress++; 应该真的使用Interlocked.Increment! (并且调用 .CurrOp 也对竞争条件开放。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-10-29
    • 2013-09-15
    • 2011-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-19
    相关资源
    最近更新 更多