Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Многопоточное программирование. Часть 4. (DeadLock)


Домашняя страница www.devdoc.ru

DevDoc - это новые статьи по программированию каждую неделю.

Заходи и читай!

Домашняя страница Письмо автору Архив рассылки Публикация статьи

Выпуск №35

Здравствуйте уважаемые подписчики! Поздравляю всех с наступающим Новым годом! Желаю всех благ, а также новых достижений в области программирования.

Cегодня в номере:

  • Результаты опроса
  • Статья "Многопоточное программирование. Часть 4. (DeadLock)"

Результаты опроса

Опросы постоянно проводятся на сайте www.devdoc.ru.

Результаты опроса
Что вносит наибольшый вклад в качесвто кода?

1Предварительное проектирование
57% ( 73 )
 
2Комментарии
4% ( 5 )
 
3Регулярный рефакторинг
21% ( 27 )
 
4Code Review
12% ( 15 )
 
5Автоматизированое тестирование
7% ( 9 )
 

Всего голосов: 129
Последний голос отдан: Вторник - 23 Декабря 2008 - 22:19:32


Постоянная ссылка на статью (с картинками): http://www.devdoc.ru/index.php/content/view/multi_thread_4.htm

Автор: Кудинов Александр
Последняя модификация: 2008-12-26 17:15:33

Многопоточное программирование. Часть 4. (DeadLock)

Продолжение. Начало смотри в предыдущей статье.

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

 
HANDLE   hMutexA; 
HANDLE   hMutexB; 
 
DWORD WINAPI ThreadA(PVOID pParam) 
{ 
 for(int i = 0; i < 100; i++) 
 { 
  WaitForSingleObject(hMutexA, INFINITE); 
  WaitForSingleObject(hMutexB, INFINITE); 
  printf("ThreadA\n"); 
  Sleep(100); 
  ReleaseMutex(hMutexB); 
  ReleaseMutex(hMutexA); 
 } 
 return 0; 
} 
 
DWORD WINAPI ThreadB(PVOID pParam) 
{ 
 for(int i = 0; i < 100; i++) 
 { 
  WaitForSingleObject(hMutexB, INFINITE); 
  WaitForSingleObject(hMutexA, INFINITE); 
  printf("ThreadB\n"); 
  Sleep(100); 
  ReleaseMutex(hMutexA); 
  ReleaseMutex(hMutexB); 
 } 
 return 0; 
} 
 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
 hMutexA = CreateMutex(NULL, FALSE, NULL); 
 hMutexB = CreateMutex(NULL, FALSE, NULL); 
 
 DWORD dwID; 
 HANDLE hThread1 = CreateThread(NULL, 0, ThreadA, 0, 0, &dwID); 
 
 HANDLE hThread2 = CreateThread(NULL, 0, ThreadB, 0, 0, &dwID); 
 
 //Ждем завершеня потоков 
 HANDLE hEvents[2] = {hThread1, hThread2}; 
 WaitForMultipleObjects(2, hEvents, TRUE, INFINITE); 
 
 CloseHandle(hThread1); 
 CloseHandle(hThread2); 
 CloseHandle(hMutexA); 
 CloseHandle(hMutexB); 
 
 return 0; 
} 

Выполнение потоков ThreadA и ThreadB приводят к взаимной блокировки. Рассмотрим один из возможных сценариев.

  • Поток ThreadA захватывает hMutexA, после чего происходит переключение потоков.
  • Поток ThreadB захватывает hMutexB и пытается захватить hMutexA. Т.к. мьютекс уже занят, поток засыпает и ждет пока он освободится.
  • Управление снова получает поток ThreadA и пытается захватить hMutexB и тоже засыпает, т.к. он уже захвачен.

Как видим оба потока не могут продолжить выполнение. Оба мьютекса заняты и ни один из потоков не может их освободить. Это так сказать классический вариант. Чтобы такого не возникало, надо придерживатся простого правила: Если у нас есть несколько объектов синхронизации - их надо всегда захватывать и освобождать в одном и том же порядке. Т.е. в нашем случае надо переписать один из потоков. Например:

 
DWORD WINAPI ThreadB(PVOID pParam) 
{ 
 for(int i = 0; i < 100; i++) 
 { 
  WaitForSingleObject(hMutexA, INFINITE); 
  WaitForSingleObject(hMutexB, INFINITE); 
  printf("ThreadB\n"); 
  Sleep(100); 
  ReleaseMutex(hMutexB); 
  ReleaseMutex(hMutexA); 
 } 
 return 0; 
} 

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

Чтобы избежать этих неприятностей надо постараться проектировать код так, чтобы не было вложенных блокировок. Это относится ко всем объектам синхронизации: Mutex, Event, Semaphore, Critical Section и их комбинациям. Ниже еще несколько полезных приемов, которые значительно снижают риск взаимных блокировок:

  • Система должна захватывать объект синхронизации на минимально возможное время.
  • После захвата объекта синхронизации нельзя вызывать какие либо функции, которые могут синхронизировать потоки. Я бы рекомендовал вообще не вызывать какие либо функции. Эти функции могут вызывать еще что нибудь. Потому что после очередной модификации кода можно внезапно обнаружить, что какая либо функция в цепочке попыталась захватить объект синхронизации со всеми вытекающими. В 90% случаев можно избежать вызова функций во время блокировки.
  • Если вам нужно получить синхронный доступ к полям какой либо структуры или к переменной многократно в разных местах функции, то рассмотрите вариант работы с копией этих данных.

Например, у нас есть такая функция:

 
DWORD WINAPI ThreadA(PVOID pParam) 
{ 
 WaitForSingleObject(hMutexA, INFINITE); 
 
 POINT *pt = reinterpret_cast(pParam); 
 for(int i = 0; i < pt->x; i++) 
 { 
  DrawNextPoint(foo(i, pt->y)); 
 } 
 ReleaseMutex(hMutexA); 
 return 0; 
} 

Эта потоковая функция должна рисует некий график. Ей необходимо иметь монопольный доступ к входному параметру (pt) на протяжении всего процесса рисования, чтобы получить корректный результат. Пример конечно же надуманный, но для демонстрации сойдет. Здесь мы вызываем функцию DrawNextPoint при захваченном hMutexA. Это место потенциального deadlock, как я уже говорил. Даже если DrawNextPoint не содержит "криминального" кода, мы получаем вторую проблему. Раз есть синхронизация, то предполагается наличие другого потока, которому нужна переменная pt. И в то время пока ThreadA будет рисовать - остальные потоки будут простаивать. Налицо потеря производительности.

Можно заметить, что в нашем примере мы только читаем из pt, поэтому нашу мы можем убить сразу двух зайцев:

 
DWORD WINAPI ThreadA(PVOID pParam) 
{ 
 POINT pt; 
 WaitForSingleObject(hMutexA, INFINITE); 
 
 POINT *ptTemp = reinterpret_cast(pParam); 
 pt = *ptTemp; 
 
 ReleaseMutex(hMutexA); 
 
 for(int i = 0; i < pt.x; i++) 
 { 
  DrawNextPoint(foo(i, pt.y)); 
 } 
 return 0; 
} 

Теперь в цикле мы работаем с копией данных. Вызовы функции делаются асинхронно без опасности вызвать deadlock. Время захвата hMutexA тоже минимально - остальные потоки работают без потери производительности.

Ну и теперь рассмотрим последний пример.

 
HANDLE   hMutexA; 
 
DWORD WINAPI ThreadA(PVOID pParam) 
{ 
 WaitForSingleObject(hMutexA, INFINITE); 
 
 HWND pWnd = (HWND)(pParam); 
 
 for(int i = 0; i < 1000; i++) 
 { 
  SendMessage(pWnd, WM_USER, 0, 0); 
 } 
 
 ReleaseMutex(hMutexA); 
 
 return 0; 
} 
 
 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
 hMutexA = CreateMutex(NULL, FALSE, NULL); 
 
 HWND hWnd = CreateSomeWindow(); 
 DWORD dwID; 
 HANDLE hThread1 = CreateThread(NULL, 0, ThreadA, hWnd, 0, &dwID); 
 
 HANDLE hThread2 = CreateThread(NULL, 0, ThreadA, hWnd, 0, &dwID); 
 
 MSG msg; 
 
 while(GetMessage(&msg, NULL, 0,0)) 
 { 
  //assert(msg.message != WM_QUIT); 
  if(WM_QUIT == msg.message) 
  { 
   PostQuitMessage((int)msg.wParam); 
   break; 
  } 
  WaitForSingleObject(hMutexA, INFINITE); 
  Sleep(100);     //emulate some data processing 
  ReleaseMutex(hMutexA); 
 
  DispatchMessage(&msg); 
 } 
 
 //Ждем завершеня потоков 
 HANDLE hEvents[2] = {hThread1, hThread2}; 
 WaitForMultipleObjects(2, hEvents, TRUE, INFINITE); 
 
 CloseHandle(hThread1); 
 CloseHandle(hThread2); 
 CloseHandle(hMutexA); 
 
 return 0; 
} 

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

Правильно, мы вызываем функции во время блокировки hMutexA. И не просто функции, а SendMessage. Эта функция доставляет сообщение окну и возвращает результат обработки. Т.е. работает синхронно. Сценарий такой:

  • Рабочий поток захватывает hMutexA и начинает посылать сообщения окну.
  • Главный поток пытается захватить hMutexA в цикле обработки оконных сообщений и засыпает, т.к. объект уже занят.
  • Рабочий поток во время очередной посылки сообщения тоже заснет, т.к. для продолжения работы необходимо, чтобы главный поток обработал сообщение в цикле выборки оконных сообщений.

Если SendMessage заменить на PostMessage - все заработает, т.к. последняя просто помещает сообщение в очередь и не дожидается результатов выполнения. Еще раз повторюсь - старайтесь не вызывать другие функции во время блокировки. Они могут работать с окнами или сами использовать механизмы синхронизации, которые пересекаются с синхронизацией вызывающего кода.

На этом мы завершаем поверхностное ознакомление с потоками. Многопоточное программирование содержит множество тонкостей, которые трудно поместить в формате статьи. Поэтому если есть конкретные вопросы - спрашивайте. Я буду рад ответить :)


Если вам нравиться эта рассылка рекомендуйте ее своим друзьям. Подписаться можно по адресу http://subscribe.ru/catalog/comp.soft.prog.devdoc

Copyright (C) Kudinov Alexander, 2006-2007

Перепечатка и использование материалов запрещена без писменного разрешения автора.


В избранное