Обновление глобальных переменных из одного рабочего потока: нужны ли мьютексы? - PullRequest
0 голосов
/ 21 октября 2010

Кажется, что этот вопрос получает , задаваемый часто , но я не приду к какому-либо окончательному выводу.Мне нужна небольшая помощь в определении того, должен ли я (или должен!) Реализовывать код блокировки при доступе / изменении глобальных переменных, когда у меня есть:

  • глобальные переменные, определенные в области видимости файла
  • один «рабочий» поток для чтения / записи в глобальные переменные
  • вызовы из основного потока процесса, вызывающие функции доступа, которые возвращают эти глобальные переменные

Итак, вопрос в том, должен ли я блокироватьдоступ к моим глобальным переменным с мьютексами?

В частности, я пишу библиотеку C ++, которая использует веб-камеру для отслеживания объектов на странице бумаги - компьютерное зрение сильно загружает процессор, поэтому производительность критична.У меня есть один рабочий поток, который запускается в функции Open().Этот поток обрабатывает все объекты отслеживания.Он прекращается (косвенно с глобальным флагом), когда вызывается функция Close().

Такое ощущение, что я просто просил повреждения памяти, но я не обнаружил никаких проблем взаимоблокировки и не столкнулся с какими-либо плохимизначения, возвращаемые этими функциями доступа.И после нескольких часов исследований у меня сложилось общее впечатление: «Мех, наверное. Что угодно. Веселитесь с , что ».Если я действительно должен использовать мьютексы, почему у меня еще не возникло проблем?

Вот слишком упрощение моей текущей программы:

// *********** lib.h ***********
// Structure definitions
struct Pointer
{
  int x, y;
};
// more...

// API functions
Pointer GetPointer();
void Start();
void Stop();
// more...

Реализация выглядит следующим образом ...

// *********** lib.cpp ***********
// Globals
Pointer p1;
bool isRunning = false;
HANDLE hWorkerThread;
// more...

// API functions
Pointer GetPointer()
{
  // NOTE: my current implementation is actually returning a pointer to the
  // global object in memory, not a copy of it, like below...

  // Return copy of pointer data
  return p1;
}

// more "getters"...

void Open()
{
  // Create worker thread -- continues until Close() is called by API user
  hWorkerThread = CreateThread(NULL, 0, DoWork, NULL, 0, NULL);
}

void Close()
{
  isRunning = false;

  // Wait for the thread to close nicely or else you WILL get nasty
  // deadlock issues on close
  WaitForSingleObject(hWorkerThread, INFINITE);
}

DWORD WINAPI DoWork(LPVOID lpParam)
{
  while (isRunning)
  {
    // do work, including updating 'p1' about 10 times per sec
  }

  return 0;
}

Наконец, этот код вызывается из внешнего исполняемого файла.Примерно так (псевдокод):

// *********** main.cpp ***********
int main()
{
  Open();

  while ( <esc not pressed> )
  {
    Pointer p = GetPointer();
    <wait 50ms or so>
  }
  Close();
}

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

Спасибо

Ответы [ 5 ]

5 голосов
/ 21 октября 2010

Если доступ к объекту имеет только один поток (как чтение, так и запись), блокировка не требуется.

Если объект доступен только для чтения, блокировка не требуется.(Предполагая, что вы можете гарантировать, что только один поток обращается к объекту во время построения).

Если какой-либо поток записывает (изменяет состояние) объекта.Если есть другие потоки, которые обращаются к этому объекту, тогда ALL доступы (как чтение, так и запись) должны быть заблокированы.Хотя вы можете использовать блокировки чтения, которые позволяют нескольким читателям.Но операции записи должны быть исключительными, и никакие читатели не могут получить доступ к объекту во время изменения состояния.

1 голос
/ 21 октября 2010

Ситуация довольно ясна - читатель может не видеть обновления, пока что-то не вызовет синхронизацию (мьютекс, барьер памяти, атомарная операция ...).Процессы многих вещей неявно инициируют такую ​​синхронизацию - например, вызовы внешних функций (по причинам, изложенным в разделе часто задаваемых вопросов о потоке в Usenet (http://www.lambdacs.com/cpt/FAQ.html)) - см. Ответ Дэйва Бутенхофа о потребности в volatile, так что если ваш код имеет дело с значениями, которые настолько малы, что онине может быть наполовину записан (например, числа, а не строки, фиксированный адрес, а не динамическое (пере) распределение), тогда он может хромать без явной синхронизации.

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

1 голос
/ 21 октября 2010

Полагаю, это зависит от того, что вы делаете в своей функции DoWork (). Давайте предположим, что это записывает значение в p1. По крайней мере, у вас есть следующая возможность условия гонки, которое возвратит недопустимые результаты в основной поток:

Предположим, что рабочий поток хочет обновить значение p1. Например, давайте изменим значение p1 с (A, B) на (C, D). Это будет включать как минимум две операции: сохранить C в x и сохранить D в y. Если основной поток решает прочитать значение p1 в функции GetPointer (), он также должен выполнить как минимум две операции, загрузить значение для x и загрузить значение для y. Если последовательность операций:

  1. ветка обновления: магазин C
  2. основной поток: нагрузка x (основной поток получает C)
  3. основной поток: нагрузка y (основной поток получает B)
  4. Обновление темы: магазин D

Основной поток получит точку (C, B), что не правильно.

Эта конкретная проблема не подходит для потоков, так как основной поток не выполняет никакой реальной работы. Я бы использовал один поток и такой API, как WaitForMultipleObjectsEx, который позволяет одновременно ожидать ввода от дескриптора стандартного ввода клавиатуры, события ввода-вывода с камеры и значения времени ожидания.

1 голос
/ 21 октября 2010

Вы не получите тупиковую ситуацию, но вы можете увидеть случайное плохое значение с крайне низкой вероятностью: поскольку чтение и запись занимают доли наносекунды, а вы только читаете переменную 50 раз в секунду, вероятностьстолкновение что-то вроде 1 на 50 миллионов.

Если это происходит на Intel 64, «Указатель» выровнен по 8-байтовой границе, и он читается и записывается за одну операцию (все 8 байтов с одной инструкцией по сборке), тогда доступы являются атомарными ивам не нужен мьютекс.

Если какое-либо из этих условий не выполнено, есть вероятность, что читатель получит неверные данные.

Я бы включил мьютекс, чтобы просто включитьбезопасная сторона, так как он будет использоваться только 50 раз в секунду, и это не будет проблемой производительности.

0 голосов
/ 21 октября 2010

Возможно, вы не видите проблем из-за характера информации в Pointer.Если он отслеживает координаты какого-либо объекта, который движется не очень быстро, и позиция обновляется во время чтения, тогда координаты могут быть «немного смещены», но их недостаточно, чтобы заметить.

Например,Предположим, что после обновления px равно 100, а py равно 100. Объект, который вы отслеживаете, немного перемещается, поэтому после следующего обновления px равно 102, а py равно 102. Если вы случайно прочитали в середине этого обновления,после обновления x, но до обновления y, вы закончите получать значение указателя px как 102, а py как 100.

...