Окт 28

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

Как только вы разобьете приложение на несколько потоков, вам придется решать проблемы, о существовании которых вы даже не подозревали, занимаясь программированием в однопоточном режиме.
Например, простая операция считывания и записи в общую глобальную переменную из нескольких потоков без должной синхронизации, может быть источником ошибок в работе программы.
Первичный поток создает с помощью 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. В описанной ситуации квант работы дочернего потока пошел насмарку — его результаты утеряны!
Итак, мы показали, что одновременное использование несколькими потоками общей глобальной переменной без должной синхронизации может быть источником ошибок.