Подтвердить что ты не робот

Как написать безопасный/правильный многопоточный код в .NET?

Сегодня мне пришлось исправить некоторый старый код VB.NET 1.0, который использует потоки. Проблема заключалась в обновлении элементов пользовательского интерфейса из рабочего потока вместо UI-потока. Мне потребовалось некоторое время, чтобы узнать, что я могу использовать утверждения с InvokeRequired, чтобы найти проблему.

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

  • Есть ли хорошие шаблоны для написания многопоточного кода? Что такое Dos и Don'ts?
  • Какие методы вы используете для отладки проблем с потоками?

Пожалуйста, укажите примерный код, если это применимо и возможно. Ответы должны быть связаны с платформой .NET(любая версия).

4b9b3361

Ответ 1

Это может быть массивный список - отлично прочитайте Джо Даффи " Параллельное программирование в Windows" для более подробной информации. Это в значительной степени свалка мозга...

  • Старайтесь избегать вызова значительных фрагментов кода, пока вы владеете блокировкой.
  • Избегайте блокировки ссылок, которые также могут блокировать код вне класса.
  • Если вам когда-либо нужно приобретать более одного замка за раз, всегда приобретайте эти блокировки в том же порядке
  • Где разумно использовать неизменяемые типы - они могут свободно использоваться между потоками.
  • Помимо неизменяемых типов, старайтесь избегать необходимости обмена данными между потоками.
  • Не пытайтесь сделать ваши типы потокобезопасными; большинство типов не обязательно должны быть, и обычно код, которому необходимо обмениваться данными, должен управлять самой блокировкой.
  • В приложении WinForms:
    • Не выполняйте длительные или блокирующие операции в потоке пользовательского интерфейса.
    • Не прикасайтесь к пользовательскому интерфейсу из любого потока, кроме потока пользовательского интерфейса. (Используйте BackgroundWorker, Control.Invoke/BeginInvoke)
  • По возможности избегайте локальных переменных потока (aka thread-statics) - они могут привести к неожиданному поведению, особенно на ASP.NET, где запрос может обслуживаться разными потоками (поиск "гибкости потока" и ASP.NET)
  • Не пытайтесь быть умными. Непрерывный параллельный код с блокировкой очень трудно получить.
  • Документируйте модель потоков (и безопасность потоков) ваших типов.
  • Monitor.Wait следует почти всегда использовать в сочетании с какой-то проверкой во время цикла (т.е. while (я не могу продолжить) Monitor.Wait(monitor))
  • Учитывайте разницу между Monitor.Pulse и Monitor.PulseAll внимательно каждый раз, когда вы используете один из них.
  • Вставка Thread.Sleep для устранения проблемы никогда не является реальным исправлением.
  • Посмотрите на "Параллельные расширения" и "Координация и Concurrency Runtime" как способы сделать Concurrency проще. Параллельные расширения будут частью .NET 4.0.

С точки зрения отладки у меня нет очень много советов. Использование Thread.Sleep для повышения шансов увидеть условия гонки и взаимоблокировки может работать, но вы должны иметь достаточно разумное представление о том, что неправильно, прежде чем вы узнаете, куда его поместить. Запись очень удобна, но не забывайте, что код переходит в своеобразное квантовое состояние - наблюдение за ним через журнал почти неизбежно изменяет его поведение!

Ответ 2

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

Неизменяемые объекты

Если вам нужно разделить состояние между потоками, состояние должно быть неизменным. Если один поток должен внести изменения в объект, он создает совершенно новую версию объекта с изменением, а не изменяет состояние объекта.

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

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

Передача сообщений в стиле Erlang

Вам не нужно изучать язык, но посмотрите на Erlang, чтобы увидеть, как он приближается к concurrency. Приложения Erlang могут масштабироваться практически неограниченно, потому что каждый процесс полностью отделен от всех остальных (обратите внимание: это не точно процессы, но не точно нити).

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

Advantanges для этого стиля - это устранение блокировок, когда один процесс выходит из строя, он не разрушает ваше приложение. Здесь хорошее резюме стиля Erlang concurrency: http://www.defmacro.org/ramblings/concurrency.html

Ответ 3

Использовать FIFO. Многие из них. Это древняя тайна аппаратного программиста, и он спас мой бекон несколько раз.

Ответ 4

Это шаги для написания многостраничного кода качества (проще читать и понимать):

  • Посмотрите Power Threading Library от Джеффри Рихтера
  • Смотреть видео - поразиться
  • Потратьте некоторое время, чтобы получить более глубокое представление о том, что происходит на самом деле, прочитайте статьи "Concurrent Affair", найденные здесь.
  • Начните писать надежные, безопасные многопоточные приложения!
  • Поймите это еще не так просто, сделайте некоторые ошибки и узнайте от них... повторите... повторите... повторите: -)

Ответ 5

Кажется, никто не ответил на вопрос, как отлаживать многопоточные программы. Это реальная проблема, потому что, если есть ошибка, ее нужно исследовать в реальном времени, что почти невозможно для большинства инструментов, таких как Visual Studio. Единственное практическое решение - писать трассы, хотя сама трассировка должна:

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

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

public const int MaxMessages = 0x100;
string[] messages = new string[MaxMessages];
int messagesIndex = -1;

public void Trace(string message) {
  int thisIndex = Interlocked.Increment(ref messagesIndex);
  messages[thisIndex] = message;
}

Метод Trace() является многопоточным безопасным, не блокирующим и может быть вызван из любого потока. На моем ПК требуется около 2 микросекунд, что должно быть достаточно быстрым.

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

Более подробное описание этого подхода, который также собирает информацию о потоке и времени, перерабатывает буфер и выводит трассировку, которую вы можете найти: CodeProject: отладка многопоточного кода в режиме реального времени 1