Окт 28 2010

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

Как только вы разобьете приложение на несколько потоков, вам придется решать проблемы, о существовании которых вы даже не подозревали, занимаясь программированием в однопоточном режиме.
Например, простая операция считывания и записи в общую глобальную переменную из нескольких потоков без должной синхронизации, может быть источником ошибок в работе программы.
Первичный поток создает с помощью CreateThread дочерний поток, имеющий входную функцию ThreadFunc. В теле функции ThreadFunc вызывается функция IncCounter, которая выполняет в цикле N раз операцию инкремента для глобального счетчика g_counter.
Запустив дочерний поток и не дожидаясь окончания его выполнения, первичный поток вызывает функцию DecCounter. Функция DecCounter выполняет в цикле N раз операцию декремента для глобального счетчика g_counter.
После возврата из DecCounter первичный поток ждет окончания работы дочернего потока с помощью функции WaitForSingleObject. Вызов функции
WaitForSingleObject(hThread, INFINITE):
обеспечивает приостановку выполнения потока на неограниченное время (INFINITE), пока не освободится объект ядра hThread.
После этого, вызывая функцию InvalidateRect, первичный поток заставляет Windows сформировать сообщение WM_PAINT. Обрабатывая это сообщение, функция WndProc выводит значение переменной g_counter в главное окно приложения. Нетрудно догадаться, что правильным результатом выполнения этого приложения должно быть значение g_counter = 0.
При маленьких значениях N так и происходит. Но вот для значения 50 000 000 эта программа дает самые разные результаты на разных компьютерах. Эксперименты проводились в основном на компьютерах, имеющих процессор Intel Celeron и Intel Pentium. Заметим, что при тактовой частоте 2 ГГц функция IncCounter так же, как и функция DecCounter, требует для своего выполнения примерно 150 мс. При такой продолжительности выполнения каждый поток будет прерываться не менее пяти раз, отдавая процессор другому потоку.
На всех компьютерах, имеющих процессор с технологией HyperThreating, был получен ненулевой результат счетчика g_counter. Для компьютеров с процессором без поддержки HyperThreating результат был иногда ненулевой, иногда нулевой.
Чем же вызвано искажение результата работы программы? — Прерыванием потока в тот момент, когда процессор не полностью завершил очередную операцию! Рассмотрим это чуть подробнее.
Допустим, что функция DecCounter первичного потока приступила к очередному вычитанию единицы из счетчика. Процессор, реализуя эту операцию, скопировал значение глобальной переменной g_counter в свой регистр. Предположим, что это значение равно 100 000. Далее процессор успел вычесть единицу, но не успел переписать новое значение 99 999 обратно в глобальную переменную. В этот момент система отбирает процессор у первичного потока, сохранив контекст потока, и передает его на использование дочернему потоку. Предположим, что функция IncCounter дочернего потока успела 50 000 раз добавить в счетчик единицу, так что переменная g_counter стала равна 150 000. В этот момент квант дочернего потока истек, и после окончания очередной операции система возвращает процессор первичному потоку. При этом система восстанавливает контекст первичного потока с незавершенной операцией. Завершая эту операцию, процессор переписывает значение 99 999 из своего регистра в глобальную переменную g_counter. В описанной ситуации квант работы дочернего потока пошел насмарку — его результаты утеряны!
Итак, мы показали, что одновременное использование несколькими потоками общей глобальной переменной без должной синхронизации может быть источником ошибок.


Окт 19 2010

Синхронизация

Кроме проблем, связанных с использованием общих глобальных переменных, у потоков могут быть также проблемы доступа к общим системным ресурсам. Потоки должны взаимодействовать друг с другом в двух основных случаях:
совместно используя разделяемый ресурс (чтобы не разрушить его);
когда нужно уведомить другие потоки о завершении каких-либо операций.
Примитив синхронизации — это объект, который помогает управлять многопоточным приложением. В Windows 2000 доступны следующие основные типы примитивов синхронизации:
атомарные операции API-уровня;
критические секции;
события;
ожидаемые таймеры;
семафоры;
мыотексы.
Каждый из указанных типов примитивов синхронизации применяется в определенных ситуациях.


Окт 01 2010

Атомарный доступ и семейство Interlocked-функций

Большая часть синхронизации потоков связана с атомарным доступом (atomic access) — монопольным захватом ресурса обращающимся к нему потоком. Win32 API предоставляет несколько функций для реализации взаимно блокированных операций. Все Interlocked-функций работают корректно только при условии, что их аргументы выровнены по границе двойного слова (DWORD).
Функция Interlockedlncrement, имеющая прототип
LONG InterlockedIncrement(LPLONG IpAddend); инкрементирует 32-разрядную переменную, адрес которой задается параметром IpAddend. Функция возвращает новое значение указанной переменной.
Функция Interlocked Decrement определена аналогично функции Interlockedlncrement, но она декрементирует 32-разрядную переменную.
Пара функций
LONG Inter!ockedExchange(LPLONG IpTarget. LONG Value);
PVOID InterlockedExchangePointer(PVOID ppvTarget. PVOID pvValue);
монопольно заменяет текущее значение переменной типа LONG, адрес которой передается в первом параметре, значением, передаваемым во втором параметре. В 32-разрядном приложении обе функции работают с 32-разрядными значениями. В 64-разрядной программе первая функция оперирует 32-разрядными значениями, а вторая — 64-разрядными. Обе функции возвращают исходное значение пе-ременной.
Следующая функция добавляет к значению переменной, адрес которой передается в первом параметре, значение, передаваемое во втором параметре:
LONG InterlockedExchangeAdd(LPLONG IpAddend. LONG Increment);
Еще две функции выполняют операцию сравнения и присваивания по результату сравнения:
LONG InterlockedCompareExchangetLPLONG IpDestination, LONG Exchange.
LONG Comparand); PVOID InterlockedCompareExchangePointeKPVOID ppvDestination.
PVOID pvExchange. PVOID pvComparand);
Если значение переменной, адрес которой передается в первом параметре, совпадает со значением, передаваемым в третьем параметре, то оно заменяется значением, передаваемым во втором параметре. В 32-разрядном приложении обе функции работают с 32-разрядными значениями. В 64-разрядной программе первая функция оперирует 32-разрядными значениями, а вторая — 64-разрядными. Обе функции возвращают исходное значение переменной, заданной первым параметром.
Вернемся к нашему приложению BadCount (см. листинг 9.3), работающему некорректно из-за одновременного доступа к общей глобальной переменной g_counter из разных потоков. Имея на вооружении функции с атомарным доступом, совсем несложно заставить приложение работать правильно. Для этого нужно в реализации функции IncCounter заменить инструкцию
++g_counter: вызовом следующей функции:
InterlockedIncrement(&g_counter);
Аналогично, в реализации функции DecCounter необходимо операцию
--g_counter; заменить следующим вызовом:
InterlockedDecrement(&g_counter);
Проверьте, как будет работать на вашем компьютере программа BadCount после указанных изменений в ее исходном тексте. Модифицированное приложение, по-видимому, достойно и нового имени GoodCount!