Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Обучение VC++ / ЛекцииИнтернетС++ / Лекция_лаб_практикум.doc
Скачиваний:
64
Добавлен:
16.02.2016
Размер:
932.35 Кб
Скачать

Программирование многопоточных приложений

Потоки в Windows бывают двух видов рабочие потоки (worker threads) и потоки пользовательского интерфейса (user-interface threads). MFC-библиотека поддерживает оба вида. У потока пользовательского интерфейса есть окна, а, значит, и свой цикл выборки сообщений, а у рабочего — нет. Рабочие потоки легче программировать, и они, как правило, полезнее.

Не забывайте, что даже в однопоточном приложении есть поток, который называется основным потоком (main thread). Здесь важно помнить, что приложение — это процесс, содержащий как минимум один поток.

Функция рабочего потока и запуск потока

Для выполнения длительных вычислений рабочий поток эффективнее обработчика сообщений, содержащего вызов PeekMessage. Однако прежде чем думать о запуске рабочего потока, надо написать для него глобальную функцию. Она должна возвращать значение типа UINT и принимать в качестве параметра одно 32-разрядное значение, объявленное как LPVOID. При запуске потока через этот параметр можно передать ему все, что угодно. Поток выполняет свои вычисления и завершается, когда глобальная функция возвращает управление. Он завершается и при закрытии процесса, но лучше, чтобы рабочий поток завершался раньше — это поможет избежать утечек памяти.

Чтобы запустить поток (с функцией, скажем, ComputeThreadProc), программа делает вызов:

CWinThread* pThread = AfxBeginThread(ConiputeThreadProc, GetSafeHwndO, THREAD_PRIORITY_NORMAL).

Код функции потока выглядит так:

UINT ComputeThreadProc (LPVOID pParam)

{

// процесс обработки

return О;

}

Функция AfxBeginThread возвращает указатель на только что созданный объект “поток”. Этот указатель можно использовать для приостановки и возобновления исполнения потока (CWinThread::SuspendTbread и ResumeThread), но у объекта “поток” нет функции-члена для уничтожения потока. Второй параметр AfxBeginThread — 32-разрядное значение, передаваемое глобальной функции, а третий параметр представляет собой код приоритета потока. После запуска рабочего потока оба потока исполняются независимо друг от друга Windows распределяет время между ними (и потоками других процессов) в соответствии с их приоритетами.

Общение основного потока с рабочим

Основной поток (ваша программа) может передавать информацию вспомогательному рабочему потоку разными способами. Однако отправка Windows-сообщения — один из способов, который работать не будет, так как у рабочего потока нет цикла выборки сообщений. Простейшее средство коммуникации — глобальная переменная, поскольку все глобальные переменные доступны всем потокам процесса. Допустим, рабочий поток в процессе вычислений увеличивает и проверяет значение глобальной целочисленной переменной, завершаясь, когда значение переменной достигает 100. Основной поток может принудительно завершить рабочий поток, присвоив глобальной переменной значение 100 или более.

Приведенный ниже код, на первый взгляд, должен работать, и, если Вы его протестируете, может быть, так и случится:

UINT ComputeThreadProc (LPVOID pParam)

{

g_nCount = 0;

while (g_nCo<jnt++ < 100)

{

// здесь выполняются какие-то вычисления

}

return 0;

}

Однако здесь есть одна проблема, которую можно обнаружить, лишь посмотрев на сгенерированный ассемблерный код. Значение g_nCount загружается в регистр, увеличивается там и переписывается обратно в g_nCount. Предположим, g_nCount равно 40 и Windows прерывает рабочий поток сразу же после того, как он загружает это значение в регистр. Теперь управление получает основной поток и присваивает g_nCount значение 100. При возобновлении рабочий поток увеличивает значение регистра и записывает обратно в g_nCount число 41, стирая предыдущее значение 100. И вот — цикл потока не завершается!

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

Однако, чтобы компилятор не хранил счетчик в регистре, можно объявить g_nCount как volatile.

Перепишем процедуру потока следующим образом:

UINT ConiputeThreadProc(LPVOID pParam)

{

g_nCount = 0;

while (g_nCount < 100)

{

// здесь выполняются какие-то вычисления

: :InterlockeclIncrenient((long*)&g_nCount);

}

return 0;

}

Функция Interlockedlncrement предотвращает обращение к переменной со стороны другого потока во время ее изменения. Теперь основной поток сможет завершить рабочий.

Итак, Вы познакомились с некоторыми ловушками, подстерегающими программиста при использовании глобальных переменных.