将多线程访问的ConcurrentBag转储到File的速度不够快

时间:2013-08-16 06:51:23

标签: c# task-parallel-library

我已经构建了这个代码来并行处理大量字符串之间的字符串比较以加快速度。

我使用了ConcurrentBag,因此所有线程(任务)都可以写入线程安全集合。然后我将此集合转储到一个文件中。

我遇到的问题是我转储到文件的ConcurrentBag<string> log填充速度比写入文件的速度快。因此,我的程序会持续消耗越来越多的内存,直到内存不足为止。

我的问题是我该怎么办?改进写日志?暂停任务直到转储ConcurrentBag然后恢复任务?什么是最快的选择?

以下是代码:

CsvWriter csv = new CsvWriter(@"C:\test.csv");

List<Bailleur> bailleurs = DataLoader.LoadBailleurs();
ConcurrentBag<string> log = new ConcurrentBag<string>();
int i = 0;

var taskWriteToLog = new Task(() =>
{
    // Consume the items in the bag
    string item;
    while (true)  //  (!log.IsEmpty)
    {
        if (!log.IsEmpty)
        {
            if (log.TryTake(out item))
            {
                csv.WriteLine(item);
            }
            else
                Console.WriteLine("Concurrent Bag busy");
        }
        else
        {
            System.Threading.Thread.Sleep(1000);
        }
    }
});

taskWriteToLog.Start();

Parallel.ForEach(bailleurs, s1 =>
{
    foreach (Bailleur s2 in bailleurs)
    {
        var lcs2 = LongestCommonSubsequenceExtensions.LongestCommonSubsequence(s1.Name, s2.Name);
        string line = String.Format("\"LCS\",\"{0}\",\"{1}\",\"{2}\"", s1.Name, s2.Name, lcs2.Item2);
        log.Add(line);
        // Console.WriteLine(line);

        var dic = DiceCoefficientExtensions.DiceCoefficient(s1.Name, s2.Name);
        line = String.Format("\"DICE\",\"{0}\",\"{1}\",\"{2}\"", s1.Name, s2.Name, dic);
        log.Add(line);
        // Console.WriteLine(line);
    }
    i++;
    Console.WriteLine(i);
});

public class CsvWriter
{
    public string FilePath { get; set; }
    private FileStream _fs { get; set; }
    private StreamWriter _sw { get; set; }

    public CsvWriter2(string filePath)
    {
        FilePath = filePath;
        _fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write);
        _sw = new StreamWriter(_fs);
    }

    public void WriteLine(string line)
    {
        _sw.WriteLine(line);
    }
}

3 个答案:

答案 0 :(得分:7)

不要直接使用并发包,使用具有并发包的BlockingCollection作为后备存储(默认情况下,它是并发队列)。

其中一个constructor overloads允许您设置集合大小的上限,如果行李变满,它将阻止插入线程,直到有空间插入。

它还为您提供了GetConsumingEnumerable(),使得从包中取出物品非常容易,您只需在foreach循环中使用它,它将继续提供您的消费者数据,直到CompleteAdding被调用。之后它会一直运行直到行李为空,然后像已经完成的任何其他正常IEnumerable一样退出。如果在调用CompleteAdding之前包“干”,它将阻塞线程并在更多数据放入包中时自动重启。

void ProcessLog()
{
    CsvWriter csv = new CsvWriter(@"C:\test.csv");

    List<Bailleur> bailleurs = DataLoader.LoadBailleurs();

    const int MAX_BAG_SIZE = 500;
    BlockingCollection<string> log = new BlockingCollection<string>(new ConcurrentBag<string>(), MAX_BAG_SIZE);

    int i = 0;

    var taskWriteToLog = new Task(() =>
    {
        // Consume the items in the bag, no need for sleeps or poleing, When items are available it runs, when the bag is empty but CompletedAdding has not been called it blocks.
        foreach(string item in log.GetConsumingEnumerable())
        {
            csv.WriteLine(item);
        }
    });

    taskWriteToLog.Start();

    Parallel.ForEach(bailleurs, s1 =>
    {
        //Snip... You can switch to BlockingCollection without any changes to this section of code.
    });

    log.CompleteAdding(); //lets anyone using GetConsumingEnumerable know that no new items are comming so they can leave the foreach loops when the bag becomes empty.
}

答案 1 :(得分:2)

使用BlockingCollection代替ConcurrentBag

BlockingCollection<string> log = new BlockingCollection<string>();
var item = log.Take();

在这种情况下,Take将被阻止,直到插入一个项目,您不必检查log.IsEmpty。也不需要Thread.Sleep

while (true)
{
    var item = log.Take();
    //Do something with item......
}

答案 2 :(得分:0)

首先,看起来您正在使用行作为块来写入文件?

如果您可以将所有数据放入对象并将其写为较大的块,则速度会更快。目前,您可能达到了要写入的设备的最大IOPS。你的线条很小。所以你的写模式看起来像4k Random IO ..或更糟。

使用不同的集合不会改变磁盘写入是你正在做的最慢的事实。

看看concurrentbag,它可能不是直接可行的,但是如果你可以从你的包中删除行并将它们连成一个接近1-5MB的大字符串/字节数组,你应该提高你的性能。 (您可能需要将CR LF插回到字符串中。)