Главная » C# » Как избежать взаимоблокировок в Visual C# (Sharp)

0

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

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

List<int> elements = new List<int>(); elements.Add(10);

elements.Add(20);

Thread threadl = new Thread(

О  => {

Thread.Sleep(1000); int[] items;

lock (elements) {

while(elements.Count < 3) { Thread.Sleep(lOOO);

}

items = elements.ToArray();

}

foreach (int item in items) { Console.WriteLine("Item (" + item + ")"); Thread.Sleep(1000);

}

}) ;

Thread thread2 = new Thread(

О  => {

Thread.Sleep(1500);

lock (elements) { elements.Add(30);

}

}) ;

threadl.Start() ; thread2.Start();

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

Не прибегая к изменению блокировок, взаимоблокировки можно избежать, слегка модифицировав код таким образом:

List<int> elements = new List<int>(); elements-.Add(10) ;

elements.Add(20) ;

Thread threadl = new Thread(

О  =>  {

Thread.Sleep(lOOO); int[] items;

lock (elements) {

while (elements.Count < 3) { Thread.Sleep(lOOO);

}

items = elements.ToArray();

}  .

foreach (int item in items) { Console.WriteLine("Item (" + item + ")"); Thread.Sleep(1000);

}

}) ;

Thread thread2 = new Thread(

О  =>  {

Thread.Sleep(500);

lock (elements) {

}

}) ;

elements.Add(30);

threadl.Start(); thread2.Start();

Код становится работоспособным благодаря одному изменению  (выделено  жиым  шрифтом).  В  первоначальном  варианте читающий поток исполнялся первым, а во втором варианте первым исполняется записывающий поток. Это демонстрирт, что взаимоблокировки часто вызываются неправильными  временными соотнениями.

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

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

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

Для  объяснения  этого типа  вернемся  к  нашей  аналогии  с  несколькими  поварами в одной кухне. Скажем, что одному из них требуется особый нож для нарезки филе рыбы, но этот нож  используется другим поваром.  Что делает наш повар в таком случае? Стоит возле другого повара, нетерпеливо постукивая ногой, или же просит его дать ему знать, когда нож освободится, а тем временем идет заниматься чем-то другим, благо в кухне работы достаточно? Скорее всего, второе.

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

15 Зак. 555

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

class DoSomething { public void GetLock() {

Monitor.Enter(elements);

}

public void ReleaseLock() { Monitor.Exit(this);

}

}

Объект Monitor можно разместить в любом месте кода, но он привязывается к оеделенному потоку.  Поэтому,  когда поток захватывает объект Monitor, он удеивает управление до тех пор, пока не завершит исполнение или не освободит обкт. Это также приносит дополнительную пользу в том, что, получив блокировку, Monitor может продолжать получать ее снова и снова. Но если поток захватит Monitor, скажем, пять раз, он также должен освободить его пять раз, прежде чем другой поток может захватить блокировку.

Далее  приводится  код  обработки  коллекции  в  двух потоках,  модифицированный под использование типа Monitor:

List<int> elements = new List<int>(); elements.Add(10);

elements.Add(20);

Thread threadl = new Thread(

О  =>  {

Thread.Sleep(1000); int[] items; Monitor.Enter(elements);

while (elements.Count < 3) {

Monitor.Wait(elements, 1000);

}

items = elements.ToArray(); Monitor.Exit(elements); foreach (int item in items) {

Console.WriteLine("Item (" + item + ")"); Thread.Sleep(1000);

}

}) ?

Thread thread2 = new Thread(

( ) = > { .

Thread.Sleep(1500); Monitor.Enter(elements); elements.Add(30); Monitor.Pulse(elements); Monitor.Exit(elements);

}) ;

threadl.Start() ; thread2.Start() ;

Жирным шрифтом выделены модификации, использующие тип Monitor. В опредение потока, который получает блокировку первым, метод Monitor. Enter о вывается с параметром elements, который, как и в предыдущем примере блокировки, определяет область блокировки.   Получив  блокировку,  поток  проверяет,  равняется ли значение счета коллекции 3 или больше. Если значение счетчика меньше 3, то вызывается метод Monitor .wait о. Метод Monitor .wait ()  работает  подобно  мету Thread. Sleep (), ТОЛЬКО блокировка Monitor освобождается.

Освобождение блокировки является особой возможностью типа Monitor. Но блировка  освобождается  только   на   время,   когда   управление   имеет   метод Monitor. wait(). По возвращению управления методом Monitor .wait () код снова получает блокировку. Когда через 1 секунду поток выходит из  режима  сна,  он больше не имеет блокировку и должен ожидать, чтобы получить  ее.  Если  другой поток удерживает блокировку длительное время, первый поток может ожидать длительное время,  чтобы  получить  ее опять.

Другим способом завершения метода Monitor .wait () является получение спецльного СИГНала ОТ ДРУГОГО ПОТОКа, В КОТОРОМ кроме метОДОВ Enter о и Exit о также используется метод Pulse о. Метод Monitor. Pulse о активизирует сигнал, который пробуждает первый поток, но исполняться этот поток будет только  после того,  как второй  поток освободит блокировку.

Большим преимуществом типа Monitor по  сравнению  с  оператором  lock является то, что Monitor можно использовать в любом месте в коде, а также то, что он освождает блокировку на время ожидания ответа. Оператор lock применяется в том случае, когда необходимо контролировать доступ к блоку кода. Если же доступ не ограничен пределами  метода, тогда  предпочтительнее  использовать  тип  Monitor. Это не означает,  что  оператор  lock нельзя  применять  вне блока кода,  но если  нуо добавить код, который может вызвать взаимоблокировку,  то  управлять  кодом легче С ПОМОЩЬЮ типа Monitor.

Теперь, когда у нас имеются базовые знания о многопоточности, в следующих раелах мы рассмотрим  более  сложные  потоковые  архитектуры.  Особое  внимание будет уделено трем методикам программирования: читатель/писатель (reader/writer), поставщик/потребитель  (producer/consumer)  и  асинхронные  вызовы.

Источник: Гросс  К. С# 2008:  Пер. с англ. — СПб.:  БХВ-Петербург, 2009. — 576 е.:  ил. — (Самоучитель)

По теме:

  • Комментарии