Синхронизация работы между потоками в C#

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

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

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

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

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

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

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

Во всех задачах синхронизации должен использоваться наиболее легковесный механизм синхронизации. Например, если предпринимается попытка совместно использовать блок данных между двумя потоками в одном и том же процессе, и нужно разграничить доступ между ними двумя, следует применять нечто вроде блокировки Monitor, а не Mutex. Почему? Потому, что блокировка Mutex предназначена для разграничения доступа к разделяемым ресурсам между процессами и представляет собой тяжеловесный объект операционной системы, который замедляет процесс, захватывая и освобождая блокировку.

Если не требуется никакой межпроцессной блокировки, используйте вместо нее Monitor. Даже еще более легковесным, чем Monitor, является набор методов класса Interlocked. Он идеален, когда известно, что вероятность ожидания при захвате блокировки является низкой.

Любого рода ожидание на объекте ядра, такое как ожидание на Mutex, Semaphore, EventWaitHanldle или любом другом, которое в конечном итоге обеспечивается ожиданием на объекте ядра Win32, требует перехода в режим ядра. Такой переход обходится дорого, и по мере возможности его всегда лучше избегать. Например, если синхронизируемые потоки находятся в одном и том же процессе, объекты синхронизации ядра, вероятно, чересчур тяжеловесны.

Наиболее легкая техника синхронизации предполагает изощренное использование класса Threading.Interlocked. Все его методы полностью реализованы в пользовательском режиме, что позволяет избежать переключений между пользовательским режимом и режимом ядра.

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

При использовании объектов синхронизации в многопоточной среде блокировку следует удерживать в течение как можно более короткого промежутка времени.

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

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

При синхронизации выполнения потока никогда не полагайтесь на такие методы, как Thread.Suspend или Thread.Resume. Как указывалось в предыдущем разделе главы, вызов Thread.Suspend на самом деле не приостанавливает поток немедленно. Вместо этого поток должен достигнуть безопасной точки управляемого кода, прежде чем он сможет прервать выполнение. Также для синхронизации потоков никогда не применяйте Thread.Sleep.

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

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

Вы просто скрывали ее и усугубляли. Не поступайте подобным образом!

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100