【问题标题】:C# Scan tree recursively with multiple threadsC# 使用多线程递归扫描树
【发布时间】:2019-04-27 13:07:20
【问题描述】:

我正在扫描一些目录中的项目。我刚刚阅读了Multithreaded Directory Looping in C# 的问题,但我仍然想让它成为多威胁的。尽管每个人都说驱动器将成为瓶颈,但我有一些观点:

  • 驱动器可能大多是“单线程”的,但您怎么知道它们将来会带来什么?
  • 您如何知道您正在扫描的不同子路径是同一个物理驱动器?
  • 我在System.IO 上使用了一个抽象层(甚至两个),以便以后可以在不同的场景中重用代码。

所以,我的第一个想法是使用 Task,第一个虚拟实现是这样的:

public async Task Scan(bool recursive = false) {
    var t = new Task(() => {
        foreach (var p in path.scan) Add(p);
        if (!recursive) return;
        var tks = new Task[subs.Count]; var i = 0;
        foreach (var s in subs) tks[i++] = s.Scan(true);
        Task.WaitAll(tks);
    }); t.Start();
    await t;
}

我不喜欢为每个项目创建一个Task 的想法,通常这似乎并不理想,但这只是为了测试,因为任务被宣传为自动管理线程...

此方法有效,但速度很慢。它需要 5s 才能完成,而下面的单个受威胁版本大约需要 0.5s 才能在同一数据集上完成整个程序:

public void Scan2(bool recursive = false) {
    foreach (var p in path.scan) Add(p);
    if (!recursive) return;
    foreach (var s in subs) s.Scan2(true);
}

我徘徊在拳头方法真正出了什么问题。机器未加载,CUP 使用量微不足道,驱动器很好...我尝试使用 NProfiler 对其进行分析,除了程序一直位于 Task.WaitAll(tks) 之外,它并没有告诉我太多。

我还编写了一个线程锁定计数机制,在添加每个项目期间调用该机制。也许是它的问题?

#region SubCouting
public Dictionary<Type, int> counters = new Dictionary<Type, int>(); 
private object cLock = new object();
private int _sc = 0;
public int subCount => _sc;
private void inCounter(Type t) {
    lock (cLock) {
        if (!counters.ContainsKey(t)) counters.Add(t, 1);
        counters[t]++;
        _sc++;
    }
    if (parent) parent.inCounter(t);
}
#endregion

但是即使线程在这里等待,执行时间会不会类似于单线程版本而不是慢 10 倍?

我不确定如何处理这个问题。如果我不想使用任务,是否需要手动管理线程,或者是否已经有一些库非常适合这项工作?

【问题讨论】:

  • 我怀疑启动这么多任务(you should be using Task.Run,不创建任务实例,.Starting 他们)正在创建线程池作业的积压,导致线程池饥饿(线程池有有限数量的线程和一个队列...只有当很明显队列没有缩小时,它才会启动额外的线程...这种测量需要时间,导致您的应用程序出现延迟)。确实没有理由为 IO 手动生成额外的线程......您的应用程序将能够跟上。考虑专用的async...
  • ... 您正在使用的文件 IO 命令的版本。并停止滥用线程。
  • 顺便说一句,我投票结束,因为虽然这个问题是关于文件 IO,但在你的问题中没有任何与文件 IO 相关的代码。
  • 滥用线程非常切中要害。线程用于分散工作负载,这样做有其成本。这里没有你可以分散的工作量,所以你得到它的成本。
  • 这个问题不是关于文件IO,而是关于使用多线程的递归算法。我以为我说得很清楚。现在我在文件系统上使用算法,但这是一个独立于IO 的抽象层,我可以将底层系统切换到其他系统。

标签: c# multithreading recursion tree


【解决方案1】:

我想你几乎明白了。 Task.WaitAll(tks) 是问题所在。您为此阻塞一个线程,因为这是同步操作。您很快就会退出线程,所有线程都在等待一些没有线程可以运行的任务。你可以用异步解决这个问题,用await Task.WhenAll(...)代替等待。它会在等待时释放线程。对于一些工作负载,多线程版本明显更快。当只是 IO 绑定时,它大致相等。

ConcurrentBag<string> result = new ConcurrentBag<string>();
List<string> result2 = new List<string>();

public async Task Scan(string path)
{
    await Task.Run(async () =>
    {
        var subs = Directory.GetDirectories(path);
        await Task.WhenAll(subs.Select(s => Scan(s)));

        result.Add(Enumerable.Range(0, 1000000).Sum(i => path[i % path.Length]).ToString());
    });
}

public void Scan2(string path)
{
    result2.Add(Enumerable.Range(0, 1000000).Sum(i => path[i % path.Length]).ToString());

    var subs = Directory.GetDirectories(path);
    foreach (var s in subs) Scan2(s);
}

private async void button4_Click(object sender, EventArgs e)
{
    string dir = @"d:\tmp";

    System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch();
    st.Start();
    await Scan(dir);
    st.Stop();
    MessageBox.Show(st.ElapsedMilliseconds.ToString());

    st = new System.Diagnostics.Stopwatch();
    st.Start();
    Scan2(dir);            
    st.Stop();
    MessageBox.Show(st.ElapsedMilliseconds.ToString());

    MessageBox.Show(result.OrderBy(x => x).SequenceEqual(result2.OrderBy(x => x)) ? "OK" : "ERROR");
}

【讨论】:

  • 我觉得没什么好说的了,你刚刚恢复了我对SO的信心,你是英雄! :) 好吧,select 方法对我不起作用,但我将其更改为以前的 Task[] 方法,它就像一个魅力。明天我会接受你的回答,因为如果你愿意,我想增加一些额外的赏金:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-02
相关资源
最近更新 更多