Использование неблокирующих методов синхронизации

Use Non-blocking Locks When Possible [Eng., PDF 191KB]

Аннотация

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

Введение

Большинство потоковых механизмов, включая потоковые API Windows* и POSIX*, предоставляют как блокирующие, так и неблокирующие примитивы потоковой синхронизации. По умолчанию обычно используются блокирующие примитивы. Если установка блокировки прошла успешно, то поток получает возможность управлять блокировкой и выполнять код критической секции. Но в случае неудачи происходит операция смены контекста и поток помещается в очередь ожидающих потоков. Операция смены контекста достаточно дорогостоящая и ее следует избегать по следующим причинам:

  • Значительный уровень издержек при смене контекста, особенно если потоковая модель основана на потоках ядра.
  • Любая полезная работа в приложении после вызова синхронизации останавливается до тех пор, пока поток не получит контроль над блокировкой.

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

Рекомендации

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

Если попытка получить блокировку критической секции была успешной, то функция TryEnterCriticalSection возвращает True. Иначе она возвращает False и поток продолжает выполнение своего кода.

void EnterCriticalSection (LPCRITICAL_SECTION cs);
bool TryEnterCriticalSection (LPCRITICAL_SECTION cs);

Вот типичный вариант использования неблокирующего системного вызова:

CRITICAL_SECTION cs;
void threadfoo()
{
 while(TryEnterCriticalSection(&cs) == FALSE)
 {
 // some useful work
 }
 // Critical Section of Code
 LeaveCriticalSection (&cs);
 }
 // other work
}

Потоковая реализация POSIX также предлагает неблокирующие версии примитивов синхронизации mutex, semaphore и переменной condition. Например, блокирующая и неблокирующая версии примитива mutex выглядят так:

int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_try_lock (pthread_mutex_t *mutex);

Также в реализации потоков Windows* можно указать время ожидания для примитивов. В Win32* API существуют системные функции WaitForSingleObject и WaitForMultipleObjects для синхронизации объектов ядра. Поток, вызвавший данные функции, будет ждать сигнала соответствующего объекта ядра или пока не истечет отведенное пользователем время ожидания. Когда обозначенный интервал времени кончается, поток возвращается к исполнению полезной работы.
DWORD WaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds);

В данном коде hHandle является описателем объекта ядра, dwMilliseconds – интервал времени, после которого функция возвращает контроль в случае, если объект ядра сигнал не установил. Значение INFINITE указывает, что поток должен ждать бесконечно. Далее приведен пример кода, демонстрирующий использование данной функции.

void threadfoo ()
{
 DWORD ret_value;
 HANDLE hHandle;
 // Some work
 ret_value = WaitForSingleObject (hHandle,0);

 if (ret_value == WAIT_TIME_OUT)
{ 
 // Thread could not gain ownership of the kernel 
 // object within the time interval;
// Some useful work
 }
 else if (ret_value == WAIT_OBJECT_0)
{
 // Critical Section of Code
 }
 else { // Handle Wait Failure}
 // Some work
}

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

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

Дополнительные материалы

Intel® Developer Zone - Сообщество параллельного программирования

Aaron Cohen and Mike Woodring. Win32 Multithreaded Programming. O'Reilly Media; 1 edition. 1997.

Jim Beveridge and Robert Wiener. Multithreading Applications in Win32 – the Complete Guide to Threads. Addison-Wesley Professional. 1996.

Bil Lewis and Daniel J Berg. Multithreaded Programming with Pthreads. Prentice Hall PTR; 136th edition. 1997.

Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.