【问题标题】:c# multi threading process large file lines in batches of 100 [closed]c#多线程处理100个批次的大文件行[关闭]
【发布时间】:2017-04-10 08:02:36
【问题描述】:

我有一个包含 500.000.000 行的文件。

这些行是最多 10 个字符的字符串。

如何使用多线程和 100 个批量处理此文件?

【问题讨论】:

  • 编写代码然后比较它们......或者更好地阅读 SO 文档如何提出一个好问题......
  • 多线程对您没有帮助,因为 I/O 是瓶颈,而且您只有一个总线连接到硬盘驱动器。 I/O 在硬件级别进行序列化。即使您有 RAID,也不会有任何区别,因为 I/O 比 CPU 慢得多。我想如果处理如此复杂以至于可以与 I/O 相媲美,这可能很重要,但我怀疑情况是否如此。见stackoverflow.com/questions/902425/…
  • @sashoalm,这取决于对读取行进行的处理。如果处理是 CPU 密集型的,那么多线程仍然有用。在这种情况下,线程数等于CPU核数,这种方法可以达到并行处理的效果。

标签: c# multithreading asynchronous async-await batch-processing


【解决方案1】:

使用 MoreLinq 的 Batch 方法,这将创建一个 IEnumerable<string> 集合,其中包含 100 行的批量大小,它将为每 100 行旋转一个新任务。

这是一个基本实现,明智的做法是使用Semaphore 在任何给定时间只运行一定数量的任务,并查看File.ReadAllLines 对 500,000,000 行性能的开销。

public class FileProcessor
{
    public async Task ProcessFile()
    {
        List<Task> tasks = new List<Task>();
        var lines = File.ReadAllLines("File.txt").Batch(100);
        foreach (IEnumerable<string> linesBatch in lines)
        {
            IEnumerable<string> localLinesBatch = linesBatch;
            Task task = Task.Factory.StartNew(() =>
            {
                // Perform operation on localLinesBatch
            });
            tasks.Add(task);
        }

        await Task.WhenAll(tasks);
    }
}

public static class LinqExtensions
{
    public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
              this IEnumerable<TSource> source, int size)
    {
        TSource[] bucket = null;
        var count = 0;

        foreach (var item in source)
        {
            if (bucket == null)
                bucket = new TSource[size];

            bucket[count++] = item;
            if (count != size)
                continue;

            yield return bucket;

            bucket = null;
            count = 0;
        }

        if (bucket != null && count > 0)
            yield return bucket.Take(count);
    }
}

【讨论】:

  • 我的理解是 File.ReadAllLines("File.txt") 会将所有内容加载到内存中,然后您将其批处理为..线程*的大小,这不是预期的效果,这将一次加载 500,000,000 个。 docs.microsoft.com/en-us/dotnet/api/… -> Opens a text file, reads all lines of the file into a string array, and then closes the file.
【解决方案2】:

如果您使用内置 TPL 中的 Parallel.ForEach 并编写几个枚举器(如下所列),则不需要使用其他库。您的代码可能如下所示:

using (var input = new StreamReader(File.OpenRead(@"c:\path\to\my\file.txt")))
{
    Parallel.ForEach(
        input.ReadLines().TakeChunks(100),
        new ParallelOptions() { MaxDegreeOfParallelism = 8 /* better be number of CPU cores */ },
        batchOfLines => {
            DoMyProcessing(batchOfLines);
        });
}

为此,您需要IEnumerable&lt;T&gt; 上的几个扩展方法和几个枚举器,定义如下:

public static class EnumerableExtensions
{
    public static IEnumerable<string> ReadLines(this StreamReader input)
    {
        return new LineReadingEnumerable(input);
    }

    public static IEnumerable<IReadOnlyList<T>> TakeChunks<T>(this IEnumerable<T> source, int length)
    {
        return new ChunkingEnumerable<T>(source, length);
    }

    public class LineReadingEnumerable : IEnumerable<string>
    {
        private readonly StreamReader _input;

        public LineReadingEnumerable(StreamReader input)
        {
            _input = input;
        }
        public IEnumerator<string> GetEnumerator()
        {
            return new LineReadingEnumerator(_input);
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    public class LineReadingEnumerator : IEnumerator<string>
    {
        private readonly StreamReader _input;
        private string _current;

        public LineReadingEnumerator(StreamReader input)
        {
            _input = input;
        }
        public void Dispose()
        {
            _input.Dispose();
        }
        public bool MoveNext()
        {
            _current = _input.ReadLine();
            return (_current != null);
        }
        public void Reset()
        {
            throw new NotSupportedException();
        }
        public string Current
        {
            get { return _current; }
        }
        object IEnumerator.Current
        {
            get { return _current; }
        }
    }

    public class ChunkingEnumerable<T> : IEnumerable<IReadOnlyList<T>>
    {
        private readonly IEnumerable<T> _inner;
        private readonly int _length;

        public ChunkingEnumerable(IEnumerable<T> inner, int length)
        {
            _inner = inner;
            _length = length;
        }
        public IEnumerator<IReadOnlyList<T>> GetEnumerator()
        {
            return new ChunkingEnumerator<T>(_inner.GetEnumerator(), _length);
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    }

    public class ChunkingEnumerator<T> : IEnumerator<IReadOnlyList<T>>
    {
        private readonly IEnumerator<T> _inner;
        private readonly int _length;
        private IReadOnlyList<T> _current;
        private bool _endOfInner;

        public ChunkingEnumerator(IEnumerator<T> inner, int length)
        {
            _inner = inner;
            _length = length;
        }
        public void Dispose()
        {
            _inner.Dispose();
            _current = null;
        }
        public bool MoveNext()
        {
            var currentBuffer = new List<T>();

            while (currentBuffer.Count < _length && !_endOfInner)
            {
                if (!_inner.MoveNext())
                {
                    _endOfInner = true;
                    break;
                }

                currentBuffer.Add(_inner.Current);
            }

            if (currentBuffer.Count > 0)
            {
                _current = currentBuffer;
                return true;
            }

            _current = null;
            return false;
        }
        public void Reset()
        {
            _inner.Reset();
            _current = null;
            _endOfInner = false;
        }
        public IReadOnlyList<T> Current
        {
            get
            {
                if (_current != null)
                {
                    return _current;
                }

                throw new InvalidOperationException();
            }
        }
        object IEnumerator.Current
        {
            get
            {
                return this.Current;
            }
        }
    }
}

【讨论】:

  • 这里要提到的重要一点是Parallel.ForEach 将在循环完成时返回,在长时间运行的操作中,这可能会阻塞 UI 线程。解决这个问题的一种方法是在初始调用方法上分拆一个新任务。
  • 除非我遗漏了什么,否则这种分块实现不是线程安全的。
  • @Eric J 不是线程安全的,也不是必须的。 Parallel.ForEach 负责线程安全,同时在并行线程之间划分可枚举(查看 System.Collections.Concurrent.Partitioner 及其嵌套类 InternalPartitionEnumerable 和 InternalPartitionEnumerator)。
  • input.ReadLines().TakeChunks(100) 并行执行 100 还是全部内容?,如何跟踪输入的内容以确保两个线程不会同时执行相同的数据或每个 MoveNext .
  • 我想写一个你所拥有的版本,但想确保它符合我想要实现的目标,我的关键是它必须是线程安全的,并且记录需要同步跨线程,即假设项目有数字,1-1000,这将如何确保线程不重复范围......假设你想使用跳过和采取......假设两个线程可以并且将点击 MoveNext,我可以看到它在不引入锁以确保范围的情况下工作的唯一方法是预先计算范围,这是正确的吗?
猜你喜欢
  • 2013-09-11
  • 2015-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-18
  • 1970-01-01
  • 1970-01-01
  • 2013-01-11
相关资源
最近更新 更多