Синхронизация в многопоточных приложениях MFC

ОГЛАВЛЕНИЕ

Введение в концепции и методы синхронизации в многопоточных приложениях

Введение

В данной статье обсуждаются основные принципы и методы, которые должны быть полезны для начинающих в многопоточном программировании. Речь идет не о людях, начинающих изучать язык C++, а о тех, кто только начинает знакомство с многопоточным программированием. Большая часть этой статьи посвящена методам синхронизации. То есть статья фактически является руководством по синхронизации.

Общий обзор

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

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

  • Семафоры
  • Взаимоисключающие блокировки
  • Критические секции
  • События

Каждый из этих объектов применяется для различных специальных целей и задач, но общей их задачей является поддержка синхронизации. Это будет описано позже в данной статье. Есть и другие объекты, которые можно использовать в качестве среды для синхронизации, такие как объект Process (процесс) и Thread (поток). Их использование позволяет программисту определять, например, закончил данный процесс или поток выполняться или еще нет.

Чтобы применять объекты Process и Thread для задач синхронизации, мы должны использовать ожидающие функции. Перед тем как начать изучать эти функции, вам нужно понять то, что любой объект ядра, который можно использовать как объект синхронизации, может находиться в одном из двух состояний: сигнализирующем состоянии и  не сигнализирующем состоянии. За исключением критических секций, все объекты синхронизации могут быть в одном из этих двух состояний. Например, что касается объектов Process и Thread, они находятся в не сигнализирующем состоянии, когда начинают выполняться, и переходят в сигнализирующее состояние, когда заканчивают свое выполнение. Чтобы определить, закончилось ли выполнение данного процесса или потока, мы должны выяснить, когда представляющие их объекты находятся в сигнализирующем состоянии; чтобы реализовать это, мы должны разобраться с ожидающими функциями.


Ожидающие функции

Следующая функция - это простейшая ожидающая функция среди многих возможных. Ее объявление имеет такой формат:

DWORD WaitForSingleObject
(
  HANDLE hHandle,
  DWORD dwMilliseconds
);

 

Параметр hHandle принимает дескриптор (описатель) объекта, чье сигнализирующее или не сигнализирующее состояние нужно будет проверять. Параметр dwMilliseconds принимает время, которое вызывающий поток должен ждать до тех пор, пока проверяемый объект не перейдет в сигнализирующее состояние. Как только объект перешел в сигнализирующее состояние или заданный временной интервал истек, функция возвращает управление вызывающему потоку. Если dwMilliseconds принимает значение INIFINITE (-1), поток будет ждать до тех пор, пока объект не перейдет в сигнализирующее состояние. Если объект так и не перейдет в сигнализирующее состояние, то поток будет ждать вечно.

Например, данный фрагмент кода проверяет, выполняется процесс [обозначаемый дескриптором hProcess] или нет:

DWORD dw = WaitForSingleObject(hProcess, 0);
switch (dw)
{
   case WAIT_OBJECT_0:
      // процесс завершился
      break;

   case WAIT_TIMEOUT:
      // процесс еще выполняется
      break;

   case WAIT_FAILED:
      // произошел сбой
      break;
}

Как вы можете заметить, мы передали 0 в параметр функции dwMilliseconds, в этом случае функция немедленно проверит состояние объекта [сигнализирующее или не сигнализирующее] и сразу же возвратит управление. Если объект находится в сигнализирующем состоянии, функция вернет WAIT_OBJECT_0. Если состояние объекта не сигнализирующее – функция вернет WAIT_TIMEOUT. В случае сбоя функция вернет WAIT_FAILED (сбой может произойти, если в функцию был передан неправильный дескриптор).

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

DWORD WaitForMultipleObjects
(
  DWORD nCount,
  CONST HANDLE *lpHandles,
  BOOL fWaitAll,
  DWORD dwMilliseconds
);

Параметр nCount принимает число дескрипторов, подлежащих проверке. Параметр lpHandles должен указывать на массив дескрипторов. Если параметр fWaitAll имеет значение TRUE (истина), функция будет ждать, пока все объекты не перейдут в сигнализирующее состояние. Если он имеет значение FALSE (ложь), функция возвратит результат работы и завершится, если хотя бы один объект перейдет в сигнализирующее состояние (при этом неважно, в каком состоянии будут другие объекты). dwMilliseconds имеет такое же значение, что и в предыдущей функции.

Например, следующий фрагмент кода определяет, какой процесс завершится первым из списка дескрипторов процессов HANDLE:

HANDLE h[3];
h[0] = hThread1;
h[1] = hThread2;
h[2] = hThread3;

DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch (dw)
{
   case WAIT_FAILED:
      // произошел сбой
      break;

   case WAIT_TIMEOUT:
      // ни один процесс не завершился в течение последних 5000мс
      break;

   case WAIT_OBJECT_0 + 0:
      // процесс с дескриптором h[0] завершился
      break;

   case WAIT_OBJECT_0 + 1:
      // процесс с дескриптором h[1] завершился
      break;

   case WAIT_OBJECT_0 + 2:
      // процесс с дескриптором h[2] завершился
      break;
}

Как мы видим, функция может возвращать различные значения, которые показывают причину завершения работы функции. Вы уже знаете, что обозначают первые два значения. Следующие значения возвращаются по такой логике; возвращенный WAIT_OBJECT_0 + index показывает, что объект из массива HANDLE, индекс которого равен index, перешел в сигнализирующее состояние. Если параметр fWaitAll равен TRUE (истина), WAIT_OBJECT_0 будет возвращен (если все объекты перешли в сигнализирующее состояние).

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

 

Давайте перейдем к MFC и посмотрим, чем это может нам помочь. Имеются два класса, которые инкапсулируют вызовы ожидающих функций; CSingleLock и CMultiLock. Мы рассмотрим их использование позже в этой статье.

Объект синхронизации

Эквивалентный класс C++

События

CEvent

Критические секции

CCriticalSection

Взаимоисключающие блокировки

CMutex

Семафоры

CSemaphore

Каждый из этих классов является потомком единственного класса – CsyncObject, чьим наиболее часто используемым членом является перегруженный оператор HANDLE, который возвращает основной дескриптор данного объекта синхронизации. Все эти классы объявлены во включаемом файле <AfxMt.h>.


События

Обычно события используются в случаях, когда поток (или потоки) должны начать выполняться после того, как произошло определенное событие. Например, поток может ждать до тех пор, пока не будут собраны необходимые данные, и затем начать сохранение данных на жестком диске. Есть два типа событий: с ручным сбросом и с автоматическим сбросом. С помощью использования события мы легко можем сообщить другому потоку, что определенное действие имело место. С помощью события первого типа - это ручной сброс, поток может уведомить более чем один поток об определенном действии. Но с помощью события второго типа, а именно при помощи события с автоматическим сбросом, можно уведомить только один поток. В MFC есть класс CEvent, который инкапсулирует объект события (в терминах Windows, он представлен значением HANDLE). Конструктор CEvent позволяет нам создавать события с ручным сбросом и с автоматическим сбросом. По умолчанию создается второй тип события. Чтобы уведомить ожидающие потоки, мы должны использовать метод CEvent::SetEvent, это означает, что такой тип вызова заставит событие перейти в сигнализирующее состояние. Если это  событие с ручным сбросом, то оно будет оставаться в сигнализирующем состоянии до тех пор, пока не будет вызвана соответствующая процедура  CEvent::ResetEvent , которая заставит событие перейти в не сигнализирующее состояние.  Это функция, которая позволяет потоку уведомить более чем один поток с помощью единственного вызова функции SetEvent. Если событие с автоматическим сбросом, то только один поток из всех ожидающих потоков сможет получить сообщение. После того как поток получит сообщение, событие автоматически перейдет в не сигнализирующее состояние. Следующие два примера иллюстрируют эту теорию. Первый пример:

// создать событие с автоматическим сбросом
CEvent g_eventStart;

UINT ThreadProc1(LPVOID pParam)
{
    ::WaitForSingleObject(g_eventStart, INFINITE);

        ...

    return 0;
}

UINT ThreadProc2(LPVOID pParam)
{
    ::WaitForSingleObject(g_eventStart, INFINITE);

        ...

    return 0;
}

В этом коде создается глобальный объект CEvent с автоматическим сбросом. В дополнение к этому, есть два работающих потока, которые ожидают событие, которое сообщит им о том, что нужно начать выполняться. Как только третий поток вызовет SetEvent для того объекта, один и только один поток из этих двух потоков (заметьте, что никто не говорит, какой конкретно поток) получит сообщение, и после этого событие перейдет в не сигнализирующее состояние, которое не позволит второму потоку захватить событие. Код, хотя и не очень полезный, показывает, как работает событие с автоматическим сбросом. Давайте посмотрим на второй пример:

 

// создать событие с ручным сбросом
CEvent g_eventStart(FALSE, TRUE);

UINT ThreadProc1(LPVOID pParam)
{
    ::WaitForSingleObject(g_eventStart, INFINITE);

        ...

    return 0;
}

UINT ThreadProc2(LPVOID pParam)
{
    ::WaitForSingleObject(g_eventStart, INFINITE);

        ...

    return 0;

Этот код отличается от предыдущего только параметром конструктора CEvent. Но в смысле функциональности, есть принципиальное различие в способе выполнения двух потоков. Если третий поток вызывает метод SetEvent для этого объекта, то будет возможно гарантировать, что два потока начнут выполняться одновременно (почти одновременно). Это происходит, потому что событие с ручным сбросом после перехода в сигнализирующее состояние не перейдет в не сигнализирующее состояние до тех пор, пока соответствующий метод ResetEvent не закончит работу.

Есть и другой метод для работы с этими событиями - CEvent::PulseEvent. Этот метод сначала заставляет событие перейти в сигнализирующее состояние и затем заставляет вернуться обратно в не сигнализирующее состояние. Если событие с ручным сбросом, при переходе события в сигнализирующее состояние все ожидающие потоки будут уведомлены, и затем событие перейдет в не сигнализирующее состояние. Если событие с автоматическим сбросом, то только один поток будет уведомлен, даже если есть много ожидающих потоков. Если отсутствуют ожидающие потоки, то вызов ResetEvent не будет ничего делать.


Пример – рабочие потоки

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

 

Все эллипсы перемещаются в клиентской области и не выходят за ее пределы.

  1. У вас должно быть открыто приложение с однодокументным интерфейсом (SDI). Предположим, что название проекта - WorkerThreads.
  2. Давайте будем использовать обработчик сообщений в WM_LBUTTONDOWN для запуска наших потоков.
  3. Объявим управляющую функцию. Управляющую функцию можно объявить в любом файле; важно, чтобы была возможность глобального доступа к файлу. Предположим, что у нас есть файл Threads.h/Threads.cpp, в котором управляющая функция объявлена/определена таким образом:
// Threads.h
#pragma once

struct THREADINFO
{
    HWND hWnd;
    POINT point;
};


UINT ThreadDraw(PVOID pParam);
     
// Threads.cpp
extern CEvent g_eventEnd;

UINT ThreadDraw(PVOID pParam)
{
    static int snCount = 0;
    snCount ++;
    TRACE("- ThreadDraw %d: started...\n", snCount);

    THREADINFO *pInfo = reinterpret_cast<threadinfo /> (pParam);

    CWnd *pWnd = CWnd::FromHandle(pInfo->hWnd);

    CClientDC dc(pWnd);

    int x = pInfo->point.x;
    int y = pInfo->point.y;

    srand((UINT)time(NULL));
    CRect rectEllipse(x - 25, y - 25, x + 25, y + 25);

    CSize sizeOffset(1, 1);

    CBrush brush(RGB(rand()% 256, rand()% 256, rand()% 256));
    CBrush *pOld = dc.SelectObject(&brush);
    while (WAIT_TIMEOUT == ::WaitForSingleObject(g_eventEnd, 0))
    {
        CRect rectClient;
        pWnd->GetClientRect(rectClient);

        if (rectEllipse.left < rectClient.left ||
            rectEllipse.right > rectClient.right)
            sizeOffset.cx *= -1;

        if (rectEllipse.top < rectClient.top ||
            rectEllipse.bottom > rectClient.bottom)
            sizeOffset.cy *= -1;

        dc.FillRect(rectEllipse, CBrush::FromHandle
            ((HBRUSH)GetStockObject(WHITE_BRUSH)));

        rectEllipse.OffsetRect(sizeOffset);

        dc.Ellipse(rectEllipse);
        Sleep(25);
    }

    dc.SelectObject(pOld);

    delete pInfo;

    TRACE("- ThreadDraw %d: exiting.\n", snCount --);
    return 0;
}

Эта функция принимает единственный объект через ее параметр PVOID, являющийся структурой, поля которой содержат дескриптор вида, в клиентской области которого необходимо рисовать, и место, где начинается цикл. Заметьте, что мы должны передать дескриптор, а не указатель на CWnd, чтобы позволить каждому потоку создать временный объект C++ поверх дескриптора и использовать его. В противном случае все потоки должны будут совместно использовать единственный объект C++, что представляет потенциальную угрозу безопасности многопоточного программирования. Управляющая функция формирует изображение движущейся окружности в клиентской области вида. Кроме того, включите файл <Afxmt.h> в файл "StdAfx.h" , чтобы сделать событие CEvent видимым.

Другим важным пунктом является то, что мы подготовили структуру THREADINFO для передачи потоку. Этот способ в основном используется тогда, когда потоку необходимо передать больше чем одно значение (или получить значения от потока). Необходимо передать описатель окна вида и начальную точку цикла, который будет создан. Каждый поток удаляет объект THREADINFO, переданный в этот же поток. Учтите, что это удаление выполняется в соответствии с нашими условиями; то есть главный поток должен зарезервировать память кучи для объекта THREADINFO, и целевой поток должен удалить этот объект. Идея заключается в том, что основной поток не знает, когда выполнять удаление, поскольку объект принадлежит дополнительному потоку непосредственно.

        4.     Объявим переменную типа «массив» в классе CWorkerThreadView. Мы должны сохранить указатель на объекты CWinThread, чтобы использовать их позже:

private:
    CArray<CWinThread *, CWinThread *> m_ThreadArray;

Также включите файл <AfxTempl.h> в файл "StdAfx.h" , чтобы сделать объект CArray видимым.

        5.     Измените файл WorkerThreadsView.cpp. Сначала определите переменную глобального события с ручным сбросом CEvent где-нибудь в начале файла:
// событие с ручным сбросом
CEvent g_eventEnd(FALSE, TRUE);

        6.      Теперь добавьте код в обработчик сообщений WM_LBUTTONDOWN:

void CWorkerThreadsView::OnLButtonDown()
{
    THREADINFO *pInfo = new THREADINFO;
    pInfo->hWnd = GetSafeHwnd();
    pInfo->point = point;

    CWinThread *pThread = AfxBeginThread(ThreadDraw,
    (PVOID) pInfo, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
    pThread->m_bAutoDelete = FALSE;
    pThread->ResumeThread();
    m_ThreadArray.Add(pThread);
}

Необходимо отключить свойство автоматического удаления только что созданного потока, но вместо этого сохранить указатель на тот объект CWinThread в нашем массиве. Учтите, что мы создаем экземпляр THREADINFO в куче и позволяем потоку удалить его после завершения работы со структурой. Чтобы сделать ThreadDraw и THREADINFO видимыми в файле WorkerThreadsView.cpp, включите файл "Threads.h".

        7.    Позаботьтесь о том, чтобы правильно уничтожить потоки. Поскольку все потоки связаны с объектом вида (потоки работают с ним), разумно разрушать их в обработчике сообщений вида WM_DESTROY:

 

void CWorkerThreadsView::OnDestroy()
{
    CView::OnDestroy();

    // TODO: Добавьте сюда ваш код обработчика сообщений
    g_eventEnd.SetEvent();
    for (int j = 0; j < m_ThreadArray.GetSize(); j ++)
    {
    ::WaitForSingleObject(m_ThreadArray[j]->m_hThread, INFINITE);
    delete m_ThreadArray[j];
    }
}

Сначала эта функция переводит событие в сигнализирующее состояние, чтобы сообщить выполняющимся потокам о том, что пришло время им уничтожиться, и затем использует WaitForSingleObject, чтобы заставить основной поток ждать до тех пор, пока последний рабочий поток не уничтожится полностью. Для этого нужно иметь действительный указатель на CWinThread, даже когда соответствующий поток уничтожен; вот почему мы убрали свойство автоматического удаления у объектов CWinThread на предыдущем шаге. Как только рабочий поток завершается, вторая строка цикла «for» разрушает соответствующий объект C++. Во время каждой итерации выполняется вызов метода WaitForSingleObject, который вызывает переход из режима пользователя в режим ядра. Например, для 10 итераций потребуется приблизительно 10000 циклов процессора. Чтобы преодолеть это неудобство, мы можем использовать WaitForMultipleObjects. В этом случае нам потребуется массив дескрипторов потока. Вышеупомянутый цикл с параметром можно заменить следующим кодом:

//второй способ (описан выше для цикла с параметром)
int nSize = m_ThreadArray.GetSize();
HANDLE *p = new HANDLE[nSize];

for (int j = 0; j < nSize; j ++)
{
    p[j] = m_ThreadArray[j]->m_hThread;
}

::WaitForMultipleObjects(nSize, p, TRUE, INFINITE);

for (j = 0; j < nSize; j ++)
{
    delete m_ThreadArray[j];
}
delete [] p;

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

        8.    Это все. Вы можете проверить работу примера.



Критические секции

В отличие от других объектов синхронизации, критические секции работают в пользовательском режиме, за исключением случаев, когда требуется переход в режим ядра. Если поток пытается выполнить код, который может быть критической секцией, он сначала выполняет циклическую блокировку и после заданного количества времени переходит в режим ядра, чтобы ждать критическую секцию. Фактически, критическая секция состоит из счетчика цикла и семафора; счетчик цикла предназначен для ожидания в пользовательском режиме, и семафор предназначен для ожидания в режиме ядра (режим ожидания). В Win32 API есть структура CRITICAL_SECTION, которая представляет объекты критической секции. В MFC есть класс CCriticalSection. В принципе, критическая секция - это часть исходного кода, требующая интегрированного выполнения, то есть выполнение этой части кода не должно прерываться никаким другим потоком. Такие части кода могут требоваться в случаях, когда необходимо предоставить одному потоку монополию на использование общего ресурса. Простой случай – использование глобальных переменных более чем одним потоком. Например: 

int g_nVariable = 0;

UINT Thread_First(LPVOID pParam)
{
    if (g_nVariable < 100)
    {
       ...
    }
    return 0;
}

UINT Thread_Second(LPVOID pParam)
{
    g_nVariable += 50;
    ...
    return 0;
}

Этот код не является безопасным, поскольку ни один поток не имеет монопольного доступа к переменной g_nVariable. Рассмотрим следующий сценарий: предположим, что начальное значение переменной g_nVariable равняется 80, управление передано первому потоку, который видит, что значение переменной g_nVariable меньше 100, и пытается выполнить фрагмент кода при данном условии. Но в это же время процессор переключается на второй поток, который прибавляет 50 к значению переменной, так что оно становится больше 100. Впоследствии процессор переключается обратно на первый поток и продолжает выполнять блок (фрагмент кода) условия. Внутри условного блока значение g_nVariable больше 100, хотя оно должно быть меньше 100. Чтобы справиться с этой проблемой, можно использовать критическую секцию таким образом:

 CCriticalSection g_cs;
int g_nVariable = 0;

UINT Thread_First(LPVOID pParam)
{
    g_cs.Lock();
    if (g_nVariable < 100)
    {
       ...
    }
    g_cs.Unlock();
    return 0;
}

UINT Thread_Second(LPVOID pParam)
{
    g_cs.Lock();
    g_nVariable += 20;
    g_cs.Unlock();
    ...
    return 0;
}

Здесь использованы два метода из класса CCriticalSection. Вызов функции Lock сообщает системе, что выполнение основного кода не должно прерываться до тех пор, пока этот же поток не вызовет функцию Unlock. В ответ на этот вызов система сначала проверяет, не захвачен ли код другим потоком с тем же самым объектом критической секции. Если код захвачен, поток ждет до тех пор, пока захвативший код поток не освободит критическую секцию и затем захватывает этот код сам.

Если необходимо защитить более двух разделяемых ресурсов, следует использовать отдельную критическую секцию для каждого ресурса. Не забудьте согласовать Unlock с каждым Lock. При использовании критических секций нужно быть осторожным, чтобы не создавать ситуаций взаимной блокировки для потоков, работающих совместно. Это означает, что поток может ждать, пока критическую секцию не освободит другой поток, который в свою очередь ждет освобождения критической секции, захваченной первым потоком. Обычно в такой ситуации два потока будут ждать вечно.

 

Можно внедрять критические секции в классы C++ , таким образом, делая их ориентированными на многопоточное исполнение. Это может потребоваться, когда объекты определенного класса должны использоваться более чем одним потоком одновременно. Выглядит это примерно так:

class CSomeClass
{
    CCriticalSection m_cs;
    int m_nData1;
    int m_nData2;

public:
    void SetData(int nData1, int nData2)
    {
        m_cs.Lock();
        m_nData1 = Function(nData1);
        m_nData2 = Function(nData2);
        m_cs.Unlock();
    }

    int GetResult()
    {
        m_cs.Lock();
        int nResult = Function(m_nData1, m_nData2);
        m_cs.Unlock();
        return nResult;
    }
};

Возможно, что в один и тот же момент два или более потока вызовут методы SetData и/или GetData для одного и того же объекта типа CSomeClass. Следовательно, путем заключения в оболочку (обертывания) содержимого тех методов мы предотвращаем искажение данных во время вызовов тех методов.

 


Взаимоисключающие блокировки

Взаимоисключающие блокировки, как и критические секции, предназначены для защиты общих ресурсов от одновременного доступа. Взаимоисключающие блокировки реализованы внутри ядра, и поэтому для выполнения они переходят в режим ядра. Взаимоисключающая блокировка может выполнять синхронизацию не только между различными потоками, но и между различными процессами. Такая взаимоисключающая блокировка должна иметь уникальное время, чтобы она распознавалась другим процессом (такие взаимоисключающие блокировки называются именованными взаимоисключающими блокировками). MFC предоставляет класс CMutex для работы с взаимоисключающими блокировками. Взаимоисключающую блокировку можно использовать таким образом:

CSingleLock singleLock(&m_Mutex);
singleLock.Lock();    // пытаемся захватить общий ресурс
if (singleLock.IsLocked())  // мы сделали это

{
    // используем общий ресурс ...

    // После завершения позволяем другим потокам использовать ресурс
    singleLock.Unlock();
}

То же самое делается с помощью функций Win32 API:

// пытаемся захватить общий ресурс
::WaitForSingleObject(m_Mutex, INFINITE);

// используем общий ресурс ...

// После завершения позволяем другим потокам использовать ресурс
::ReleaseMutex(m_Mutex);

Взаимоисключающую блокировку также можно использовать для ограничения числа выполняющихся экземпляров одним экземпляром. Следующий код можно поместить в начало метода InitInstance (или WinMain):

HANDLE h = CreateMutex(NULL, FALSE, "MutexUniqueName");
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
    AfxMessageBox("Экземпляр уже выполняется.");
    return(0);
}

Чтобы обеспечить глобально уникальное имя, используйте Глобально уникальный идентификатор.

Семафоры

Чтобы ограничить число потоков, использующих общие ресурсы, следует использовать семафоры. Семафор – это объект ядра. Он сохраняет переменную счетчика, чтобы отслеживать число потоков, использующих общий ресурс. Например, следующий код создает семафор с помощью класса MFC CSemaphore, который может обеспечить то, что только максимум 5 потоков смогут использовать общий ресурс в данный период времени (это устанавливается в первом параметре конструктора). Предполагается, что ни один поток не захватил никаких ресурсов изначально (второй параметр):

CSemaphore g_Sem(5, 5);

Как только поток получает доступ к общему ресурсу, переменная счетчика семафора уменьшается на единицу. Если счетчик станет равным нулю, то все дальнейшие попытки использовать ресурс будут отвергаться до тех пор, пока хотя бы один поток, захвативший ресурс, не освободит его (другими словами, освободит семафор). Мы можем использовать классы CSingleLock и/или CMultiLock, чтобы ждать/захватить/освободить семафор. Мы также можем использовать функции API, как показано ниже:

// Пытаемся использовать общий ресурс
::WaitForSingleObject(g_Sem, INFINITE);
// Теперь счетчик использования семафора уменьшился на единицу

//... Используем общий ресурс ...

// После завершения позволяем другим потокам использовать ресурс
::ReleaseSemaphore(g_Sem, 1, NULL);
// Теперь счетчик использования семафора увеличился на единицу

Взаимодействие между дополнительными потоками и основным потоком

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

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

#define WM_MYMSG WM_USER + 1

WM_USER+n сообщений должны быть уникальными в рамках класса окна, но не во всем приложении. Более безопасный способ (в смысле уникальности) – использовать сообщения WM_APP+n таким образом:

#define WM_MYMSG WM_APP + 1

Метод обработчика сообщения должен быть объявлен внутри объявления класса окна, которому (окну) сообщение будет адресовано:

afx_msg LRESULT OnMyMessage(WPARAM , LPARAM );

Конечно, должно быть определение нескольких методов:

LRESULT CMyWnd::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
    // Сообщение получено
    // Делаем что-нибудь ...
    return 0;
}

И в итоге, чтобы назначить обработчик определителю типа сообщения, макроопределение ON_MESSAGE нужно использовать внутри пар BEGIN_MESSAGE_MAP и END_MESSAGE_MAP:

BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ...
    ON_MESSAGE(WM_MYMSG, OnMyMessage)
END_MESSAGE_MAP()

Теперь дополнительный поток имеет дескриптор окна (которое принадлежит основному потоку) и может отправить ему определенное пользователем сообщение, как показано ниже:

UINT ThreadProc(LPVOID pParam)
{
    HWND hWnd = (HWND) pParam;
    ...
    // Отправляем сообщение окну основного потока
    ::PostMessage(hWnd, WM_MYMSG, 0, 0);

    return 0;
}

Автор: Arman S.

Загрузить исходный код - 47.7 KB