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

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

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

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

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

Ответы [ 5 ]

25 голосов
/ 29 января 2009

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

  • Старайтесь избегать вызова значительных кусков кода, пока у вас есть блокировка
  • Избегайте блокировки ссылок, код которых вне класса может также заблокировать
  • Если вам когда-либо нужно получить более одной блокировки одновременно, всегда приобретайте эти блокировки в одном и том же порядке
  • Там, где это целесообразно, используйте неизменяемые типы - они могут свободно распределяться между потоками
  • За исключением неизменяемых типов, постарайтесь избежать необходимости обмена данными между потоками
  • Избегайте попыток сделать ваши типы безопасными для потоков; большинство типов не обязательно должны быть, и обычно код, который должен обмениваться данными, должен сам управлять блокировкой
  • В приложении WinForms:
    • Не выполнять никаких длительных или блокирующих операций в потоке пользовательского интерфейса
    • Не прикасайтесь к пользовательскому интерфейсу из какого-либо потока, кроме потока пользовательского интерфейса. (Используйте BackgroundWorker, Control.Invoke / BeginInvoke)
  • По возможности избегайте локальных переменных потоков (так называемых потоковых статик) - они могут привести к непредвиденному поведению, особенно в ASP.NET, где запрос может обслуживаться различными потоками (поиск по «гибкости потока» и ASP.NET)
  • Не пытайся быть умным. Одновременный код без блокировок чрезвычайно трудно понять правильно.
  • Документируйте модель (и безопасность потока) ваших типов
  • Monitor.Wait почти всегда должен использоваться вместе с какой-то проверкой в ​​цикле while (то есть, пока (я не могу продолжить) Monitor.Wait (monitor))
  • Учитывайте разницу между Monitor.Pulse и Monitor.PulseAll, каждый раз, когда вы используете один из них.
  • Вставка темы. Спать, чтобы решить проблему, никогда не бывает по-настоящему.
  • Посмотрите на "Параллельные расширения" и "Среда выполнения координации и параллелизма" как способы упрощения параллелизма. Parallel Extensions станет частью .NET 4.0.

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

11 голосов
/ 30 января 2009

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

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

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

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

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

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

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

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

Преимущества этого стиля - устранение блокировок, когда один процесс завершается неудачей, он не разрушает все ваше приложение. Вот краткое изложение параллелизма в стиле Erlang: http://www.defmacro.org/ramblings/concurrency.html

2 голосов
/ 04 июля 2014

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

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

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

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

2 голосов
/ 22 февраля 2009

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

0 голосов
/ 22 февраля 2009

Это этапы написания качественного (более легкого для чтения и понимания) многопоточного кода:

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