Потокобезопасный StreamWriter C# как это сделать? Два

Итак, это продолжение моего последнего вопроса - Так что вопрос был "Каков наилучший способ создания программы, которая является потокобезопасной с точки зрения необходимости записи двойных значений в файл. Если функция, которая сохраняет значения через streamwriter вызывается несколькими потоками? Как лучше всего это сделать?"

и я изменил некоторый код, найденный в MSDN, как насчет следующего? Этот правильно записывает все в файл.

namespace SafeThread
{
    class Program
    {
        static void Main()
        {
            Threading threader = new Threading();

            AutoResetEvent autoEvent = new AutoResetEvent(false);

            Thread regularThread =
                new Thread(new ThreadStart(threader.ThreadMethod));
            regularThread.Start();

            ThreadPool.QueueUserWorkItem(new WaitCallback(threader.WorkMethod),
                autoEvent);

            // Wait for foreground thread to end.
            regularThread.Join();

            // Wait for background thread to end.
            autoEvent.WaitOne();
        }
    }


    class Threading
    {
        List<double> Values = new List<double>();
        static readonly Object locker = new Object();
        StreamWriter writer = new StreamWriter("file");
        static int bulkCount = 0;
        static int bulkSize = 100000;

        public void ThreadMethod()
        {
            lock (locker)
            {
                while (bulkCount < bulkSize)
                    Values.Add(bulkCount++);
            }
            bulkCount = 0;
        }

        public void WorkMethod(object stateInfo)
        {
            lock (locker)
            {
                foreach (double V in Values)
                {
                    writer.WriteLine(V);
                    writer.Flush();
                }
            }
            // Signal that this thread is finished.
            ((AutoResetEvent)stateInfo).Set();
        }
    }
}

4 ответов


Thread и QueueUserWorkItem это самые низкие доступные API для резьбы. Я бы не стал их использовать, если бы у меня не было абсолютно другого выбора. Попробуйте Task класс для гораздо более высокого уровня абстракции. Для деталей, см. мой недавний пост в блоге на эту тему.

вы также можете использовать BlockingCollection<double> как правильный очередь производителя/потребителя вместо того, чтобы пытаться построить один вручную с самые низкие доступные API для синхронизация.

правильно изобрести эти колеса удивительно сложно. Я настоятельно рекомендую использовать классы, предназначенные для этого типа потребностей (Task и BlockingCollection, если быть точным). Они встроены в .NET 4.0 framework и доступны в качестве дополнения для .NET 3.5.


  • код имеет запись в качестве экземпляра var, но с использованием статического шкафчика. Если у вас было несколько экземпляров, записывающих в разные файлы, нет причин, по которым им нужно было бы использовать одну и ту же блокировку
  • в соответствующем примечании, поскольку у вас уже есть writer (как частный экземпляр var), вы можете использовать его для блокировки вместо использования отдельного объекта locker в этом случае - это делает вещи немного проще.

"правильный ответ" действительно зависит от того, что вы ищите с точки зрения поведения блокировки/блокировки. Например, проще всего было бы пропустить промежуточную структуру данных, просто иметь метод WriteValues, чтобы каждый поток "сообщал" свои результаты и записывал их в файл. Что-то вроде:

StreamWriter writer = new StreamWriter("file");
public void WriteValues(IEnumerable<double> values)
{
    lock (writer)
    {
        foreach (var d in values)
        {
            writer.WriteLine(d);
        }
        writer.Flush();
    }
}

конечно, это означает, что рабочие потоки сериализуются во время фаз "результатов отчета" - в зависимости от характеристик производительности, что может быть просто отлично (5 минут для генерации, 500 мс для записи, например.)

на другом конце спектра у вас будут рабочие потоки, записывающие в структуру данных. Если вы находитесь в .NET 4, я бы рекомендовал просто использовать коллекции concurrentqueue вместо того, чтобы делать это самостоятельно.

кроме того, вы можете сделать файл ввода-вывода в больших партиях, чем те, которые сообщаются рабочими потоками, поэтому вы можете просто сделать запись в фоновом потоке на некоторой частоте. Что конце спектра выглядит ниже (вы удалите консоль.WriteLine вызывает в реальном коде, они просто есть, чтобы вы могли видеть, как он работает в действии)

public class ThreadSafeFileBuffer<T> : IDisposable
{
    private readonly StreamWriter m_writer;
    private readonly ConcurrentQueue<T> m_buffer = new ConcurrentQueue<T>();
    private readonly Timer m_timer;

    public ThreadSafeFileBuffer(string filePath, int flushPeriodInSeconds = 5)
    {
        m_writer = new StreamWriter(filePath);
        var flushPeriod = TimeSpan.FromSeconds(flushPeriodInSeconds);
        m_timer = new Timer(FlushBuffer, null, flushPeriod, flushPeriod);
    }

    public void AddResult(T result)
    {
        m_buffer.Enqueue(result);
        Console.WriteLine("Buffer is up to {0} elements", m_buffer.Count);
    }

    public void Dispose()
    {
        Console.WriteLine("Turning off timer");
        m_timer.Dispose();
        Console.WriteLine("Flushing final buffer output");
        FlushBuffer(); // flush anything left over in the buffer
        Console.WriteLine("Closing file");
        m_writer.Dispose();
    }

    /// <summary>
    /// Since this is only done by one thread at a time (almost always the background flush thread, but one time via Dispose), no need to lock
    /// </summary>
    /// <param name="unused"></param>
    private void FlushBuffer(object unused = null)
    {
        T current;
        while (m_buffer.TryDequeue(out current))
        {
            Console.WriteLine("Buffer is down to {0} elements", m_buffer.Count);
            m_writer.WriteLine(current);
        }
        m_writer.Flush();
    }
}

class Program
{
    static void Main(string[] args)
    {
        var tempFile = Path.GetTempFileName();
        using (var resultsBuffer = new ThreadSafeFileBuffer<double>(tempFile))
        {
            Parallel.For(0, 100, i =>
            {
                // simulate some 'real work' by waiting for awhile
                var sleepTime = new Random().Next(10000);
                Console.WriteLine("Thread {0} doing work for {1} ms", Thread.CurrentThread.ManagedThreadId, sleepTime);
                Thread.Sleep(sleepTime);
                resultsBuffer.AddResult(Math.PI*i);
            });
        }
        foreach (var resultLine in File.ReadAllLines(tempFile))
        {
            Console.WriteLine("Line from result: {0}", resultLine);
        }
    }
}

Итак, вы говорите, что хотите, чтобы куча потоков записывала данные в один файл с помощью StreamWriter? Простой. Просто заблокируйте объект StreamWriter.

код здесь создаст 5 потоков. Каждый поток будет выполнять 5 "действий", и в конце каждого действия он будет записывать 5 строк в файл с именем "файл"."

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            StreamWriter Writer = new StreamWriter("file");

            Action<int> ThreadProcedure = (i) => {
                // A thread may perform many actions and write out the result after each action
                // The outer loop here represents the multiple actions this thread will take
                for (int x = 0; x < 5; x++) {
                    // Here is where the thread would generate the data for this action
                    // Well simulate work time using a call to Sleep
                    Thread.Sleep(1000);
                    // After generating the data the thread needs to lock the Writer before using it.
                    lock (Writer) {
                        // Here we'll write a few lines to the Writer
                        for (int y = 0; y < 5; y++) {
                            Writer.WriteLine("Thread id = {0}; Action id = {1}; Line id = {2}", i, x, y);
                        }
                    }
                }
            };

            //Now that we have a delegate for the thread code lets make a few instances

            List<IAsyncResult> AsyncResultList = new List<IAsyncResult>();
            for (int w = 0; w < 5; w++) {
                AsyncResultList.Add(ThreadProcedure.BeginInvoke(w, null, null));
            }

            // Wait for all threads to complete
            foreach (IAsyncResult r in AsyncResultList) {
                r.AsyncWaitHandle.WaitOne();
            }

            // Flush/Close the writer so all data goes to disk
            Writer.Flush();
            Writer.Close();
        }
    }
}

результатом должен быть файл "file" со 125 строками в нем со всеми" действиями", выполняемыми одновременно, и результатом каждого действия, записанного синхронно папка.


код, который у вас есть, тонко сломан - в частности, если рабочий элемент в очереди запускается первым, то он немедленно очистит (пустой) список значений перед завершением, после чего ваш работник идет и заполняет список (который в конечном итоге будет проигнорирован). Событие auto-reset также ничего не делает, так как ничто никогда не запрашивает или не ожидает его состояния.

кроме того, поскольку каждый поток использует разные замок, замки не имеют никакого значения! Вы должны убедиться, что вы удерживайте одну общую блокировку при доступе к streamwriter. Вам не нужна блокировка между кодом промывки и кодом генерации; вам просто нужно убедиться, что промывка выполняется после завершения генерации.

вы, вероятно, на правильном пути, хотя - хотя я бы использовал массив фиксированного размера вместо списка и очистил все записи из массива, когда он заполнится. Это позволяет избежать возможности исчерпания памяти, если поток является долгоживущим.