MFC Программирование многопоточного приложения

Рубрика: MFC, Дата: 19 August, 2015, Автор:

Здорова! Сегодня разберем как создаются многопоточные приложения. Про то как создаются однопоточные приложения мы уже разобрали тут.

Немножко теории и терминов

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

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

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

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

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

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

CWinThread* pThread=AfxMeginThread(ThreadProc,GetSafeHwnd(),THREAD_PRIORITY_NORMAL);

а код функции ThreadProc выглядит примерно так

UINT ThreadProc(LPVOID pParam)
{
   //процесс обработки
   return 0;
}

Функция AfxBeginThread возвращает указатель на только что созданный объект “поток”. Этот указатель можно использовать для приостановки и возобновления исполнения потока (CWin-Thread::SuspendThread и ResumeThread), но у объекта “поток” нет функции-члена для уничтожения потока. Второй параметр передаваемое значение, а третий код приоритета потока.

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

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

Рассмотрим простой примерчик, допустим у нас есть глобальная переменная g_nCount, когда она достигает значений 500 и более, то поток завершается, от его код

UINT Proc(LPVOID pParam)
{
  g_nCount=0;
  while(g_nCount++<500)
  {
     //происходят вычисления
  }
  return 0;
}

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

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

UINT Proc(LPVOID pParam)
{
  g_nCount=0;
  while(g_nCount++<500)
  {
     //происходят вычисления
     ::InterlockedIncrement((long*)&g_nCount);
  }
  return 0;
}

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

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

Если мы будем проверять значение глобальной переменной в цикле, то у нас получится зацикливание, потому что не будут проверяться и обрабатываться сообщения Windows, можно конечно в цикл добавить обработку сообщений и все будет работать как положено, но для коммуникации рабочего потока с основным предпочтительнее передавать сообщения Windows в основной поток, так как у основного потока есть цикл выборки сообщений, однако это подразумевает что у основного потока есть окно видимое или невидимое, а у рабочего потока его описатель. Описатель окна передается через параметр потока – 32 разрядный параметр.

Следует ли посылать сообщения синхронно (send message) или асинхронно (post message)? Асинхронная передача предпочтительнее, так как синхронная может вызвать повторное вхождение в MFC – код для выборки сообщений основного потока, а это чревато проблемами.
Сообщение следует посылать любое пользовательское!

Пример многопоточного приложения

Создадим реальное многопоточное приложение, оно будет такое же как приложение однопоточное которое мы создали тут, но архитектура будет многопоточной.

Создание приложения

Откроем Visual Studio 2010, заходим в меню “Файл” -> “Создать”->”Проект…”, в появившемся окошке “Создать проект” выбираем “Приложение MFC” и вводим имя “MyTestThread”, жмем “Ок”. Появится “Мастер приложений MFC”, жмем “Далее”, в появившейся вкладке выставляем настройки как на скрине

MFC Createжмем “Готово”, у нас создастся проект.

Добавим к нему диалог как на скрине

MFC dialogидентификатор progress control IDC_PROGRESS1, а кнопки “start” IDSTART.

Добавим класс к диалогу и назовем его CMyDialog

MFC add classДобавим переменные в класс диалога

private:
    int m_nTimer;
public:
    enum { nMaxCount = 10000 };

Добавим глобальную переменную в самом начале файла MyDialog.cpp

int g_nCount = 0;

Добавим в самом начале файла MyDialog.h объявление процедуры потока и определение макроса идентификатора нашего сообщения

#define WM_THREADFINISHED		WM_USER + 5
UINT ThreadProc(LPVOID pParam);

Добавим в файл MyDialog.cpp  определение этой процедуры

ThreadProc »

//функция потока
UINT ThreadProc(LPVOID pParam)
{
	volatile int nTemp; //чтобы не было аптимизации

	for (g_nCount = 0; g_nCount < CMyDialog::nMaxCount; 
		::InterlockedIncrement((long*) &g_nCount)) {//блокирует чтобы g_nCount не была изенена в других потоках
		for (nTemp = 0; nTemp < 10000; nTemp++) {
			// uses up CPU cycles
		}
		TRACE("ksadfjlskdfjsdfn");//загружаем процесор
	}
	// WM_THREADFINISHED собственное сообщение
	//посылаем асинхронно свое собственное сообщение
	::PostMessage((HWND) pParam, WM_THREADFINISHED, 0, 0);
	g_nCount = 0;//обнуляем глобальную переменную
	return 0; // завершение потока
}

Добавим обработчики для “start”, “Отмена” и для сообщения WM_TIMER, ниже приведены их коды

CMyDialog::OnBnClickedStart »

void CMyDialog::OnBnClickedStart()
{
	// TODO: добавьте свой код обработчика уведомлений
	m_nTimer = SetTimer(1, 100, NULL); // 1/10 second
	ASSERT(m_nTimer != 0);
	GetDlgItem(IDSTART)->EnableWindow(FALSE);
	AfxBeginThread(ThreadProc,GetSafeHwnd(),THREAD_PRIORITY_NORMAL);
}

CMyDialog::OnBnClickedCancel »

void CMyDialog::OnBnClickedCancel()
{
	// TODO: добавьте свой код обработчика уведомлений
	if (g_nCount == 0) { // prior to Start button
		CDialogEx::OnCancel();
	}
	else { // computation in progress
		g_nCount = nMaxCount; // Force thread to exit
	}
	CDialogEx::OnCancel();
}

CMyDialog::OnTimer »

void CMyDialog::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: добавьте свой код обработчика сообщений или вызов стандартного
	CProgressCtrl* pBar = (CProgressCtrl*) GetDlgItem(IDC_PROGRESS1);
	pBar->SetPos(g_nCount * 100 / nMaxCount);

	CDialogEx::OnTimer(nIDEvent);
}

Добавим обработчик нашего собственного сообщения WM_THREADFINISHED

CMyDialog::OnThreadFinished »

//пользовательское сообщение
LRESULT CMyDialog::OnThreadFinished(WPARAM wParam, LPARAM lParam)
{
	CDialogEx::OnOK();
	return 0;
}

Конечно же нужно добавить определение

afx_msg LRESULT OnThreadFinished(WPARAM wParam, LPARAM lParam);

и в карту сообщений добавим код

ON_MESSAGE(WM_THREADFINISHED,OnThreadFinished)//собственное сообщение

Переходим к классу вида изменим функцию OnDraw

CMyTestThreadView::OnDraw »

void CMyTestThreadView::OnDraw(CDC* pDC)
{
	CMyTestThreadDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: добавьте здесь код отрисовки для собственных данных
	pDC->TextOut(0, 0, L"Нажми левую кнопку мышки здесь");
}

И обработчик нажатия левой клавишы мышки WM_LBUTTONDOWN, вот его код

CMyTestThreadView::OnLButtonDown »

void CMyTestThreadView::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO: добавьте свой код обработчика сообщений или вызов стандартного
	CMyDialog dlg;
	dlg.DoModal();
	CView::OnLButtonDown(nFlags, point);
}

И не забудьте подключить файл диалога в

#include "MyDialog.h"

Все нажимаем F5 и проверяем.

MFC progВсе отлично работает.

 

Комментарии:


Оставить комментарий

Your email address will not be published. Required fields are marked *