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

ОГЛАВЛЕНИЕ

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

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

 

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

  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.    Это все. Вы можете проверить работу примера.