Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

Глава 21

Глава 21. Потоковая многозадачность

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

Первый тип основан на процессах. Такая многозадачность поддерживалась уже с первых версий Windows. Процесс - это программа, или задача, которая выполняется. В многозадачных системах такого типа две и более программы могут выполняться одновременно.

Второй тип многозадачности основан на потоках. Такая многозадачность поддерживается оболочкой Win32 и используется в Windows 95 и Windows NT. Поток - это часть выполняющегося процесса. В Windows 95/NT каждый процесс имеет по крайней мере один поток, но потоков процесса может быть и два, и больше.

В потоковой многозадачности несколько частей одной и той же программы могут выполняться одновременно. Это дает возможность писать чрезвычайно эффективные программы путем разделения их на отдельные исполняемые блоки и управления ходом выполнения всей программы в целом. Для многозадачности такого типа в MFC предусмотрены специальные средства поддержки.

С введением потоковой многозадачности возникла необходимость в специальном механизме, называемом синхронизацией. Синхронизация позволяет контролировать выполнение потоков (и процессов) строго определенным образом. В Win32 для синхронизации выделена целая подсистема. Библиотека классов MFC полностью поддерживает средства многозадачности.

Использование потоков

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

По токи MFC

В MFC определены два типа потоков : интерфейсные и рабочие . Интерфейсный поток способен принимать и обрабатывать сообщения. Говоря языком MFC, интерфейсные потоки содержат канал сообщений. Главный поток MFC-программы (начинающийся при объявлении объекта класса CWinApp) является интерфейсным потоком. Рабочие потоки не принимают и не обрабатывают сообщения. Они обеспечивают дополнительные пути выполнения задачи внутри интерфейсного потока.

В MFC потоковая многозадачность реализуется с помощью класса CWinThread . Кстати, что производным от него является класс CWinApp , формирующий поток приложения.

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

При создании многопотоковых программ наиболее часто используются именно рабочие потоки - необходимость в нескольких каналах сообщений возникает достаточно редко, однако во многих приложениях используются вспомогательные потоки, позволяющие вести фоновую обработку данных. Сосредоточимся поэтому на рабочих потоках (важно понимать, что на уровне API и рабочие, и интерфейсные потоки обрабатываются одинаково ; различие между ними существует только в иерархии классов MFC.

Создание рабочего потока

Для создания рабочего потока предназначена функция AfxBeginThread библиотеки MFC:

	CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, 
		LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, 
		UINT nStackSize = 0, DWORD dwCreateFlags = 0, 
		LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );

Каждый поток внутри родительского процесса начинает свое выполнение с вызова специальной функции, называемой потоковой функцией . Выполнение потока продолжается до тех пор, пока не завершится его потоковая функция. Адрес данной функции (т.е. входная точка в поток) передается в параметре pfnThreadProc. Все потоковые функции должны иметь следующий прототип :

		UINT pfnThreadProc(LPVOID pParam);

Значение параметра pParam функции AfxBeginThread передается потоковой функции в качестве параметра. Это 32-разрядное число может использоваться для любых целей.

Начальный приоритет потока указывается в параметре nPriority. Если этот параметр равен 0, то используются установки приоритета текущего (родительского) потока.

Каждый поток имеет свой собственный стек. Размер стека указывается в параметре nStackSize. Если этот параметр равен нулю (общепринятый подход), то создаваемому потоку будет выделен стек такого же размера, что и у родительского потока, а при необходимости, размер стека может быть увеличен.

Параметр dwCreateFlags определяет состояние выполнения потока. Если данный параметр равен нулю, поток начинает выполняться немедленно. Если значение этого параметра равно CREATE_SUSPEND , то поток создается временно приостановленным, т.е. ожидающим запуска. Чтобы запустить такой поток, нужно вызвать функцию CWinThread::ResumeThread .

Параметр lpSecurityAttrs является указателем на набор атрибутов прав доступа, относящийся к данному потоку. Если этот параметр равен NULL, то набор атрибутов будет унаследован от родительского окна.

При успешном завершении функция AfxBeginThread возвращает указатель на объект потока, в противном случае возвращает ноль. Данный указатель необходимо сохранять, если впоследствии предполагается обращение из родительского потока к созданному потоку (например, для изменения приоритета или для временного приостановления потока).

Рассмотрим пример создания рабочего потока для однодокументного приложения example при обработке сообщения о выборе пользователем пункта меню “Start”. В качестве родительского потока выступает главный поток приложения. Рабочий поток после запуска осуществляет 100-кратный вывод некоторого изображения в окно приложения :

	UINT MyThread(LPVOID pParam); // объявление функции потока

	// реализация метода класса C
ExampleView (класса обликов),
	// наследованного от базового 
CView из 
MFC.
	void CExampleView::OnStart() // обработка сообщения от меню

	{
		//Создать новый поток. Функция потока имеет имя MyThread,
		// в качестве параметра функции потока передается указатель
		// на текущее окно просмотра для вывода в него изображения

		AfxBeginThread(MyThread,this);
	}
	// определение функции потока

	UINT MyThread(LPVOID pParam)
	{
		// через параметр передается указатель на окно просмотра

		CExampleView *ptrView=(CExampleView *)pParam; 

		for(int i=0; i<100; i++)
		{	CDC *dc=ptrView->GetDC(); // получить контекст отображения

			CRect r;
			ptrView->GetClientRect(&r); // получить клиентскую область окна

			dc->TextOut(rand()%r.Width(),rand()%r.Height(),"*",1); // вывод

		}
		return 0;
	}

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

Иногда бывает необходимо приостановить поток на заданное количество миллисекунд. Это можно сделать, вызвав API-функцию Sleep .

Использование нескольких потоков

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

	UINT MyThread1(LPVOID pParam);
	UINT MyThread2(LPVOID pParam);
	void CExampleView::OnStart() 
	{
		AfxBeginThread(MyThread1,this);
		AfxBeginThread(MyThread2,NULL); // параметр не передается

	}
	UINT MyThread1(LPVOID pParam)
	{
		CExampleView *ptrView=(CExampleView *)pParam; 

		for(int i=0; i<100; i++)
		{	CDC *dc=ptrView->GetDC();
			CRect r;	ptrView->GetClientRect(&r);
			dc->TextOut(rand()%r.Width(),rand()%r.Height(),"*",1);
		}
		return 0;
	}
	UINT MyThread2(LPVOID pParam)
	{
		for(int i=0; i<50; i++)
		{	Sleep(2000);
			MessageBeep(0);
		}
		return 0;
	}

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

Остановка и возобновление выполнения потоков

Остановить выполнение потока можно с помощью метода SuspendThread класса CWinThread. В остановленном состоянии поток не выполняется. Продолжить выполнение потока можно с помощью метода ResumeThread класса CWinThread .

Каждый поток имеет связанный с ним счетчик остановок. Если этот счетчик равен нулю, значит поток выполняется нормально. При ненулевом значении счетчика поток находится в остановленном состоянии. С каждым вызовом метода SuspendThread значение счетчика остановок увеличивается на единицу. И, наоборот, с каждым вызовом функции ResumeThread значение счетчика остановок уменьшается на единицу. Остановленный поток может продолжить выполнение только после того, как значение счетчика достигнет нуля.

Управление п риоритетами потоков

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

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

Получить класс приоритета процесса можно с помощью функции GetPriorityClass , а установить класс приоритета можно с помощью функции SetPriorityClass . Обе эти функции являются API-функциями и не входят в класс CWinThread .

Ниже показаны константы, соответсвующие классам приоритетов в порядке убывания :

  • REALTIME_PRIORITY_CLASS
  • HIGH_PRIORITY_CLASS
  • NORMAL_PRIORITY_CLASS
  • IDLE_PRIORITY_CLASS

По умолчанию программе присваивается приоритет NORMAL_PRIORITY_CLASS. Как правило, причин менять его нет. Фактически, изменение приоритета процесса может негативно сказаться на производительности всей системы. Так например, увеличение класса приоритета программы до REALTIME_PRIORITY_CLASS приведет к захвату программой всех ресурсов процессора.

Приоритет процесса (независимо от класса приоритета) говорит о том, сколько времени процессора занимает отдельный поток в пределах своего процесса. При создании потока ему присваивается нормальный приоритет. Но это значение можно изменить, причем даже во время выполнения потока.

Приоритеты потоков контролируются методами класса CWinThread . Определить значение приоритета можно с помощью метода GetThreadPriority , а изменить его - с помощью метода SetThreadPriority

Ниже приведены константы, соответствующие установкам приоритетов в порядке убывания :

  • THREAD_PRIORITY_TIME_CRITICAL
  • THREAD_PRIORITY_HIGHEST
  • THREAD_PRIORITY_ABOVE_NORMAL
  • THREAD_PRIORITY_NORMAL
  • THREAD_PRIORITY_BELOW_NORMAL
  • THREAD_PRIORITY_LOWEST
  • THREAD_PRIORITY_IDLE

Благодаря различным сочетаниям значений приоритета процесса и приоритета потока в Win32 поддерживается 31 различная установка приоритета.

Синхр онизация потоков

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

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

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

В Windows имеется специальные сервисы, которые позволяют определенным образом ограничить доступ к разделяемым ресурсам, ведь без помощи операционной системы отдельный процесс или поток не может сам определить, имеет ли он единоличный доступ к ресурсу. Операционная система Windows содержит процедуру, которая в течении одной непрерывной операции проверяет и, если это возможно, устанавливает флаг доступа к ресурсу. На языке разработчиков операционной системы такая операция называется операцией проверки и установки . Флаги, используемые для обеспечения синхронизации и управления доступом к ресурсам, называются семафорами (semaphore) . Интерфейс Win32 API обеспечивает поддержку семафоров и других объектов синхронизации. Библиотека MFC также включает поддержку данных объектов.

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

Интерфейс Win32 поддерживает четыре типа объектов синхронизации - все они так или иначе основаны на понятии семафора.

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

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

Третьим типом объектов синхронизации является событие , или объект события ( event object). Он используется для блокирования доступа к ресурсу до тех пор, пока какой-нибудь другой процесс или поток не заявит о том, что данный ресурс может быть использован. Таким образом, данный объект сигнализирует о выполнении требуемого события.

При помощи объекта синхронизации четвертого типа можно запрещать выполнения определенных участков кода программы несколькими потоками одновременно. Для этого данные участки должны быть объявлены как критический раздел ( critical section) . Когда в этот раздел входит один поток, другим потокам запрещается делать тоже самое до тех пор, пока первый поток не выйдет из данного раздела.

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

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

  • CCriticalSection - реализует критический раздел.
  • CEvent - реализует объект события
  • CMutex - реализует исключающий семафор.
  • CSemaphore - реализует классический семафор.

Кроме этих классов в MFC определены также два вспомогательных класса синхронизации : CSingleLock и CMultiLock . Они контролируют доступ к объекту синхронизации и содержат методы, используемы для предоставления и освобождения таких объектов. Класс CSingleLock управляет доступом к одному объекту синхронизации, а класс CMultiLock - к нескольким объектам. Далее будем рассматривать только класс CSingleLock .

Когда какой-либо объект синхронизации создан, доступ к нему можно контролировать с помощью класса CSingleLock . Для этого необходимо сначала создать объект типа CSingleLock с помощью конструктора :

	CSingleLock
( CSyncObject* pObject, BOOL bInitialLock = FALSE );

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

Когда объект типа CSingleLock создан, доступ к объекту, на который указывал параметр pObject, может контролироваться с помощью двух функций : Lock и Unlock класса CSingleLock .

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

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

При работе с классом CSingleLock общая процедура управления доступом к ресурсу такова :

  • создать объект типа CSyncObj (например, семафор), который будет использоваться для управления доступом к ресурсу ;
  • с помощью созданного объекта синхронизации создать объект типа CSingleLock;
  • для получения доступа к ресурсу вызвать метод Lock;
  • выполнить обращение к ресурсу ;
  • вызвать метод Unlock, чтобы освободить ресурс.

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

Работа с семафорами

Рассмотрим, как обеспечить синхронизацию потоков на основе семафоров. Прежде всего необходимо создать семафор путем объявления объекта типа CSemaphore . Конструктор этого класса имеет следующий вид :

		CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, 
			LPCTSTR pstrName = NULL, 
			LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );

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

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

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

Последний параметр конструктора является указателем на набор атрибутов прав доступа, связанный с семафором. Если этот параметр равен NULL, то семафор наследует данный набор у вызвавшего его потока.

Внесем некоторые изменения в пример, рассматриваемый в предыдущем параграфе данной главы. Добавим в класс C ExampleView объект-семафор :

	class CExampleView : public CView
	{
	protected:
		// только один поток сможет одновременно обращаться к ресурсу

		CSemaphore sem;
	// другие описания класса

	.....
	};

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

	UINT MyThread1(LPVOID pParam);
	UINT MyThread2(LPVOID pParam);
	void CExampleView::OnStart() 
	{
		AfxBeginThread(MyThread1,this);
		AfxBeginThread(MyThread2,this; 
	}
	UINT MyThread1(LPVOID pParam)
	{
		CExampleView *ptrView=(CExampleView *)pParam; 
		CSingleLock syncObj(&(ptrView->sem));
		.......
		syncObj.Lock(); // получение семафора
		действия, связанные с доступом к ресурсу

		syncObj.Unlock(); // освобождение семафора

		.......
		return 0;
	}
	UINT MyThread2(LPVOID pParam)
	{
		CExampleView *ptrView=(CExampleView *)pParam; 
		CSingleLock syncObj(&(ptrView->sem));
		.......
		syncObj.Lock(); // получение семафора
		действия, связанные с доступом к ресурсу

		syncObj.Unlock(); // освобождение семафора

		.......
		return 0;
	}

В каждой из функций потока создается объект типа CsingleLock на базе семафора, а затем вызывается метод Lock. Когда первый поток получает доступ к семафору, другой поток приостанавливается до тех пор, пока первый поток не освободит семафор с помощью функции Unlock. Таким образом, семафор в данном примере предоставляется только одному потоку. Это напоминает работу с исключающим семафором.

Работа с объектами событий

Объект событие используется для оповещения процесса или потока о том, сто произошло некоторое событие. Для работы с такими объектами предназначен класс CEvent . Конструктор класса имеет следующий прототип :

		CEvent( BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, 
				LPCTSTR lpszName = NULL, 
				LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );

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

Второй параметр указывает, каким образом состояние объекта будет изменяться при выполнении события. Если значение параметра равно TRUE (не ноль), то объект может быть сброшен только путем вызова метода ResetEvent класса CEvent . В противном случае объект автоматически сбрасывается после предоставления блокированному потоку доступа к ресурсу.

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

Последний параметр конструктора является указателем на набор атрибутов прав доступа, связанный с объектом события. Если этот параметр равен NULL, то объект событие наследует данный набор у вызвавшего его потока.

Когда объект событие создан, то поток, ожидающий данное событие, должен с помощью этого объекта создать объект типа CSingleLock , для которого затем следует вызвать метод Lock . При этом выполнение данного потока останавливается до тех пор, пока не произойдет ожидаемое событие.

Для сигнализации о том, что событие произошло, предназначена функция SetEvent класса CEvent .

При вызове данной функции первый поток, ожидающий событие, выйдет из остановленного состояния (вызванный им метод Lock завершится и продолжит свое выполнение.

При работе с программным объектом события необходимо вызывать метод ResetEvent класса CEvent всякий раз, когда поступил сигнал о выполнении события . После его вызова соответствующий объект будет сброшен.

Чтобы продемонстрировать вышесказанное, заменим в предыдущем примере семафор на объект событие. В этой версии функция MyThread1 будет блокирована до тех пор, пока функция MyThread2 не завершится и не сигнализирует о своем завершении. Таким образом, завершение функции MyThread2 является событием, которое ожидает MyThread1:

	class CExampleView : public CView
	{
	protected:
		// 1. событие не установлено
; 2. сброс его будет автоматическим

		CEvent event;
	// другие описания класса

	...…..
	};
	..........
	UINT MyThread1(LPVOID pParam);
	UINT MyThread2(LPVOID pParam);
	void CExampleView::OnStart() 
	{
		AfxBeginThread(MyThread1,this);
		AfxBeginThread(MyThread2,this; 
	}
	UINT MyThread1(LPVOID pParam)
	{
		CExampleView *ptrView=(CExampleView *)pParam; 
		CSingleLock syncObj(&(ptrView->event));
		.......
		syncObj.Lock(); // ожидание события
		действия, выполняемые после события

		syncObj.Unlock(); // освобождение объекта события

		.......
		return 0;
	}
	UINT MyThread2(LPVOID pParam)
	{
		CExampleView *ptrView=(CExampleView *)pParam; 
		.......
		ptrView->event.SetEvent(); // сигнализирует о событии

		return 0;
	}

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

Использование к ритических секций

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

	int g_count; // глобальная переменная

	……
	UINT MyThread(LPVOID pParam)
	{
		g_count=0;
		while(g_count++<100)
		{
			// здесь выполняются какие-то действия

		}
		return 0;
	}

Однако, здесь есть одна проблема, которую можно обнаружить, посмотрев сгенерированный ассемблерный код. Значение g_count загружается в регистр, увеличивается там и переписывается обратно в g_count. Предположим, g_count равно 40 и Windows прерывает выполнение рабочего потока сразу после того, как он загружает это значение в регистр. Пусть теперь управление получает основной поток и присваивает переменной g_count значение 100 (ожидая, что рабочий поток вслед за этим прекрарит свою работу). При возобновлении рабочий поток увеличивает значение регистра и записывает обратно в g_count число 41, стирая при этом предыдущее значение 100. Итог – цикл рабочего потока не завершился. Допустим, процедура потока модифицируется следующим образом:

		….
		g_count=0;
		while(g_count<100)
		{
			// здесь выполняются какие-то действия

			g_count++;
		}
		….

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

Однако есть и другой способ корректного использования разделяемых переменных. Если необходимо, чтобы несколько потоков использовали глобальные переменные или другие разделяемые ресурсы, то для этого подходит такое средство синхронизации как критические секции . События хорошо подходят для “сигнализации”, а критические секции (разделы или секции кода, требующие монопольного доступа к разделяемым данным) удобны для управления доступом к данным.

Допустим, программа отслеживает показания времени как часы, минуты и секунды, а каждое из этих значений хранится в отдельной целочисленной переменной. Теперь представим, что значения времени совместно используются двумя потоками. Поток А изменяет значение времени и прерывается потоком Б после обновления часов, но до обновления минут и секунд. Результат: поток Б получает недостоверные показания времени.

Если для данного формата времени создается класс C++, то можно легко управлять доступом к данным, сделав элементы данных закрытыми и предусмотрев открытые функции-члены. Именно таков класс CHMS, рассматриваемый ниже. Следует отметить: в этом классе есть элемент данных типа CRITICAL_SECTION. (Здесь не применяется поддержка из MFC.) Конструктор вызывает Win32-функцию InitializeCriticalSection , а деструктор — DeleteCriticalSection . Таким образом, с каждым объектом CHMS связан объект критическая секция.

	#include "stdafx.h"
	class CHMS
	{
	private:
		int m_nHr, m_nMn, m_nSc;
		CRITICAL_SECTION in_cs;
	public:
		CHMS() : m_nHr(0), m_nMn(0), m_nSc(0)
		{
			::InitializeCriticalSection(&m_cs);
		}
		~CHMS()
		{
			::DeleteCriticalSection(&m_cs);
		}
		void SetTime(int nSecs)
		{
			::EnterCriticalSection(&m_cs):
			m_nSc = nSecs % 60;
			m_nMn = (nSecs / 60) % 60;
			m_nHr = nSecs / 3600;
			::LeaveCriticalSection(&m_cs);
		}
		void GetTotalSecs()
		{
			int nTotalSecs;
			::EnterCriticalSection(&m_cs);
			nTotalSecs = m_nHr * 3600 + m_nMn * 60 + m_nSc;
			::LeaveCriticalSection(&m_cs);
			return nTotalSecs;
		}
		void IncrementSecs()
		{
			::EnterCriticalSection(&m_cs);
			SetTime(GetTotalSecs() + 1):
			::LeaveCriticalSection(&m_cs);
		}
	};

Обратим внимание, что функции-члены вызывают функции EnterCriticalSection и LeaveCriticalSection . Если поток А исполняется в середине SetTime, поток Б будет блокирован вызовом EnterCriticalSection в GetTotalSecs до тех пор, пока поток А не вызовет LeaveCriticalSection . Функция IncrementSecs вызывает SetTime, что означает наличие вложенных критических секций. Это допустимо, так как Windows отслеживает уровни вложения.

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



  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте