Как создать класс Thread Safe

Я пишу приложение С#. У меня есть (какой-то) класс ведения журнала. и этот класс ведения журнала будет использоваться многими потоками. Как сделать этот поток классов безопасным? Должен ли я сделать это как одиночный? Какие там лучшие практики? Есть ли документ, который я могу прочитать о том, как сделать его потокобезопасным?

Спасибо

Ответы

Ответ 1

В С# любой объект может использоваться для защиты "критического раздела", или, другими словами, кода, который не должен выполняться двумя потоками одновременно.

Например, следующее будет синхронизировать доступ к методу SharedLogger.Write, поэтому только один поток регистрирует сообщение в любой момент времени.

public class SharedLogger : ILogger
{
   public static SharedLogger Instance = new SharedLogger();

   public void Write(string s)
   {
      lock (_lock)
      {
         _writer.Write(s);
      }
   }

   private SharedLogger() 
   { 
      _writer = new LogWriter();
   }

   private object _lock;
   private LogWriter _writer;
}

Ответ 2

Для этого я использовал бы регистратор полки, так как есть несколько, которые прочны и просты в использовании. Не нужно катиться самостоятельно. Я рекомендую Log4Net.

Ответ 3

Я не уверен, что могу добавить что-либо к тому, что уже было сказано о том, как сделать класс журналирования потокобезопасным. Как уже было сказано, для этого вам необходимо синхронизировать доступ к ресурсу, то есть файл журнала, так что только один поток пытается одновременно подключиться к нему. Ключевое слово С# lock - это правильный способ сделать это.

Однако я рассмотрю (1) одноэлементный подход и (2) удобство использования подхода, который вы в конечном итоге решите использовать.

(1) Если ваше приложение записывает все свои сообщения журнала в один файл журнала, то одноэлементный шаблон определенно является маршрутом. Файл журнала будет открыт при запуске и закрыт при завершении работы, а шаблон singleton идеально подходит для этой концепции операций. Однако, как заметил @dtb, помните, что создание класса singleton не гарантирует безопасность потоков. Используйте ключевое слово lock для этого.

(2) Что касается удобства использования подхода, рассмотрим это предлагаемое решение:

public class SharedLogger : ILogger
{
   public static SharedLogger Instance = new SharedLogger();
   public void Write(string s)
   {
      lock (_lock)
      {
         _writer.Write(s);
      }
   }
   private SharedLogger()
   {
       _writer = new LogWriter();
   }
   private object _lock;
   private LogWriter _writer;
}

Позвольте мне сначала сказать, что этот подход, как правило, в порядке. Он определяет один экземпляр SharedLogger с помощью статической переменной Instance и не позволяет другим создавать экземпляр класса в силу частного конструктора. В этом суть шаблона singleton, но я настоятельно рекомендую прочитать и следовать рекомендациям Jon Skeet относительно синглтонов в С#, прежде чем заходить слишком далеко.

Однако я хочу сосредоточиться на удобстве использования этого решения. "Юзабилити", я имею в виду способ использования этой реализации для регистрации сообщения. Посмотрите, как выглядит вызов:

SharedLogger.Instance.Write("log message");

Вся эта часть "экземпляра" выглядит неправильно, но нет способа избежать ее при реализации. Вместо этого рассмотрите эту альтернативу:

public static class SharedLogger : ILogger
{
   private static LogWriter _writer = new LogWriter();
   private static object _lock = new object();
   public static void Write(string s)
   {
       lock (_lock)
       {
           _writer.Write(s);
       }
   }
}

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

SharedLogger.Write("log message");

Это гораздо проще для кода.

Дело не в том, чтобы очернить прежнее решение, а в том, чтобы предположить, что удобство использования любого решения, которое вы выбираете, является важным аспектом, который нельзя упускать из виду. Хороший, полезный API может сделать код более простым для записи, более элегантным и простым в обслуживании.

Ответ 4

  • Попробуйте выполнить большинство вычислений с использованием локальных переменных, а затем измените состояние объекта одним быстрым блоком lock ed.
  • Имейте в виду, что некоторые переменные могут меняться между тем, когда вы их читаете, и когда вам нужно изменить состояние.

Ответ 5

используйте lock(), чтобы несколько потоков не использовали запись в то же время

Ответ 6

В ответ на ответ BCS:

BCS описывает случай объекта без сохранения. Такой объект по сути является потокобезопасным, поскольку он не имеет собственных переменных, которые могут быть сбиты вызовами из разных ада.

Описанный logger имеет дескриптор файла (извините, а не пользователь С#, возможно, он вызвал IDiskFileResource или некоторый такой MS-ism), который он должен использовать для сериализации.

Итак, отделите хранилище сообщений от логики, которая записывает их в файл журнала. Логика должна работать только по одному сообщению.

Один из способов сделать это: если объект журнала должен был хранить очередь объектов сообщения, а объект журнала не имеет логики, чтобы вывести сообщение из очереди, а затем извлечь полезный материал из объекта сообщения, затем напишите это в журнал, затем найдите другое сообщение в очереди - тогда вы можете сделать этот поток безопасным, выполнив поток очереди /add/delete/queue _size/etc. Для этого потребуется класс журнала, класс сообщений и потоковая безопасность (который, вероятно, является третьим классом, экземпляр которого является переменной-членом класса регистратора).

Ответ 7

на мой взгляд, приведенный выше код не является потокобезопасным: в предыдущем решении вам нужно было установить новый объект SharedLogger и метод Write существовал один раз для каждого объекта.

Теперь у вас есть только один метод Write, который используется всеми потоками, например:

поток 1: SharedLogger.Write( "Thread 1" )

поток 2: SharedLogger.Write( "Thread 2" );

   public static void Write(string s)
   {
       // thread 1 is interrupted here <=
       lock (_lock)
       {
           _writer.Write(s);
       }
   }
  • thread 1 хочет написать сообщение, но прерван потоком 2 (см. комментарий)
  • поток 2 переопределяет сообщение потока 1 и прерывается потоком 1

  • поток 1 получает блокировку и записывает "Thread 2"

  • поток 1 освобождает блокировку
  • поток 2 получает блокировку и записывает "Thread 2"
  • поток 2 освобождает блокировку

исправьте меня, когда я ошибаюсь...

Ответ 8

Если производительность не является основной проблемой, например, если класс не находится под большой нагрузкой, просто выполните следующее:

Сделайте свой класс наследованием ContextBoundObject

Применить этот атрибут к вашему классу [Синхронизация]

Весь ваш класс теперь доступен только для одного потока за раз.

Это действительно более полезно для диагностики, поскольку скорость - это почти самый худший случай... но быстро определить "это странная проблема гоночного состояния", бросить ее, повторить тест.. если проблема исчезнет... вы знаете, что это проблема с потоками...

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

Например, класс ConcurrentQueue в новом материале parallelism является хорошей потоковой безопасностью.

Или пользователь log4net RollingLogFileAppender, который уже потокобезопасен.