Работа с потоками.

Содержание статьи:

Введение.
Класс для работы с потоками.
Класс для работы с параметрами функции потока.
Класс для работы с мьютексами.

Введение.

к началу статьи
Самая первая статья, так уж получилось, будет посвящена потокам. В этой теме нет ничего особенно сложного – здесь нет ни мудреного API, ни мозгодробительных и надуманных решений. Но это, несомненно, очень важная тема. Многие проблемы с которыми Вы столкнетесь, могут быть решены с помощью потоков. Как было сказано в одной книге: ”Займитесь потоками и Вы полюбите их”.

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

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

Класс для работы с потоками.

к началу статьи
Вот самая главная функция:

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
Входные параметры означают следующее: lpThreadAttributes – указатель на структуру типа SECURITY_ATTRIBUTES, они нам врат ли понадобятся, поэтому положим их равными нулю.
dwStackSize – размер выделяемого потоку стека, если передать в функцию NULL, то размер стека потока будет равен размеру стека породившего его потока.
lpStartAddress – указатель на потоковую функцию, у которой следующий интерфейс: DWORD WINAPI ThreadFunction(LPVOID)
lpParameter – указатель на область память в которой могут храниться данные необходимые потоку.
dwCreationFlag – если равен нулю, то выполнение потока начнется немедленно.
lpThreadId – идентификатор созданного потока.

Проанализировав все выше сказанное, можно сделать вывод, что нам нужны только два параметра – точка входа в поток и указатель на память с данными. Поэтому логично создать простенький класс, в который мы завернём эту функцию. Но для начала зададим себе вопрос:”Насколько удобно передавать параметры в поток через указатель(указатель на void)?”. Абсолютно неудобно. Поэтому так же неплохо было бы сделать класс, упрощающий эту процедуру.

Класс для работы с параметрами функции потока.

к началу статьи
class	cThreadParam{
public:
LPVOID Params;
cThreadParam( void *ptr ){Params = ptr;}
cThreadParam( void );
operator LPVOID( void );
__forceinline int GetParamNumber( void );
__forceinline void SetParamNumber( int i );
template<class type>__forceinline void AddParamPtr( type &ampamp );
__forceinline void AddParamByteSet( char* , int );
template<class type>__forceinline void AddParam_cd_type( type &ampamp );
template<class type>__forceinline void AddParam_ud_type( type &ampamp );
__forceinline void Expand( int );
__forceinline int GetParamSize( void );
__forceinline void SetParamSize( int i );

__forceinline char* GetParam( int );
__forceinline int GetParami( int );
__forceinline char* GetParamSize( int );
__forceinline int Length( int i )
{return( * ( ( int * ) GetParamSize( i ) ) );}
};
Что в ней может храниться? Либо побайтовые копии на переменные, либо указатели на эти же переменные. Также для удобства стоит хранить размер выделенного куска памяти(в байтах) и число параметров. Соответственно нам понадобятся две функции: добавление указателя на переменную и добавление побайтовой копии. Добавлять указатель будем следующей функцией: AddParamPtr(type&). Вот как она выглядит “в цвете”:
template<class type>__forceinline void cThreadParam::AddParamPtr( type &ampparam ){
//Первым делом надо увеличить область памяти, чтоб в неё влез указатель и его
//размер (вообще хранить размер указателя нет нужды, это просто сделано для того
//что бы код был более гладким… короче вы дальше сами все увидите).
Expand( GetParamSize() + 8 + 4 + 4 );
//Затем сдвигаемся в область памяти куда собственно можно пристроить
//указатель. Функция GetParamSize() возвращает размер блока памяти в байтах-8
char *tmp_ptr = ( ( char * )Params ) + 8 + GetParamSize();
//собственно пишем размер указателя
* ( ( int * )tmp_ptr ) = 4;
//опять сдвигаем указатель
tmp_ptr += 4;
//вот оно – пишем сам указатель
( ( int * )tmp_ptr )[ 0 ] = ( int )( int * )&ampparam;
//обновляем значения в заголовке нашей памяти – размер блока и число //параметров
SetParamSize( GetParamSize() + 8 );
SetParamNumber( GetParamNumber() + 1 );
}
Теперь надо бы начать писать значения переменных. Тут все несколько сложнее – тип может быть определён пользователем, а может быть встроенным, легко можно вызывать memcpy и молится чтобы ничего не произошло, но где гарантия, что кто то не захочет таким образом передать дерево(или другую динамическую структуру)? Поэтому будем использовать две функции. Это функция для встроенных типов(работает аналогично, за исключением memcpy) -
template<class type>__forceinline void	cThreadParam::AddParam_cd_type( type &ampparam ){
Expand( GetParamSize() + 8 + sizeof( param ) + 4 );
char *tmp_ptr = ( ( char * )Params ) + 8 + GetParamSize();
* ( ( int * )tmp_ptr ) = sizeof( param );
tmp_ptr += 4;
char *data_ptr = ( char * )&ampparam;
memcpy( tmp_ptr , data_ptr , sizeof( param ) );
SetParamSize( GetParamSize() + 4 + sizeof( param ) );
SetParamNumber( GetParamNumber() + 1 );
}
А это для типов определенных пользователем –
template<class type>__forceinline void	cThreadParam::AddParam_ud_type( type &ampparam ){
Expand( GetParamSize() + 8 + param.GetSize() + 4 );
char *tmp_ptr = ( ( char * )Params ) + 8 + GetParamSize();
* ( ( int * )tmp_ptr ) = param.GetSize();
tmp_ptr += 4;
char *data_ptr = ( char * )&ampparam.GetByteArray();
memcpy( tmp_ptr , data_ptr , param.GetSize() );
SetParamSize( GetParamSize() + 4 + param.GetSize() );
SetParamNumber( GetParamNumber() + 1 );
}
Тут все несколько сложнее. Мы, опять-таки для простоты использования, потребуем от типа определённого пользователем некоторого унифицированного интерфейса. А именно функций GetSize() и GetByteArray(). Первая возвращает размер объекта, вторая возвращает массив байтов, в который специальным образом закатан наш объект. Понятно, что еще нужна функция, которая восстанавливает объект из этого массива, но это уже мелочи… сами разберётесь.

Осталось только организовать удобный доступ “а ля массив”:

__forceinline int	cThreadParam::GetParami( int i ){
char *tmp_ptr = ( ( char * )Params )+ 8;
//именно ради этого цикла мы хранили размер указателя
for( int j( 0 ) ; j < i ; j++ ){
//узнаем размер следующего блока данных
int p_size( * ( ( int * )tmp_ptr ) );
//сдвигаемся на величину = длина блока + размер целого в котором
//хранится величина этого блока

tmp_ptr += 4 + p_size;
}
//возвращаем целое, которое в программе надо будет ещё привести к
//нужному типу

return( ( ( int * )( tmp_ptr + 4 ) )[ 0 ] );
}
Если вы заметили, мы храним указатели в виде массива целых, эта функция как раз и возвращает такое целое, нам в программе стоит только привести типы. Ну и чтоб совсем все красиво выглядело:
cThreadParam::operator LPVOID( void ){return( Params );}
Эта функция преобразует тип cThreadParam к LPVOID, чтобы в местах вызова не было уродливых конструкций типа SomeThreadParam.Params. Вот теперь, когда все приготовления были сделаны, можно с комфортом реализовывать потоки.
class	cThread{
public:
HANDLE THREAD_HANDLE;
LPDWORD THREAD_ID;
DWORD (*THREAD_FUNCTION)( LPVOID );
cThread( DWORD ( * INNER_THREAD_FUNCTION )( LPVOID ) )
{THREAD_FUNCTION = INNER_THREAD_FUNCTION;THREAD_ID = new DWORD;}
cThread( void ){THREAD_FUNCTION = NULL;THREAD_ID = new DWORD;}
void SetThreadPriority( int PRIORITY )
{::SetThreadPriority( THREAD_HANDLE , PRIORITY );}
void CreateThread( cThreadParam &ampThreadParam );
void CreateThread( DWORD ( * INNER_THREAD_FUNCTION )( LPVOID ) ,
cThreadParam &ampThreadParam );
void CreateThread( DWORD ( * INNER_THREAD_FUNCTION )( LPVOID ) );
void CreateThread( void );
};
Класс достаточно простой, т.к. содержит в основном перегруженные варианты одной функции. Рассматривать их, я думаю, нет нужды. Лучше остановимся на функции SetThreadPriority( int PRIORITY ). Она выставляет приоритет нашего потока. В качестве параметра могут передаваться следующие значения:
	THREAD_PRIORITY_IDLE (1||16)
THREAD_PRIORITY_LOWEST (-2)
THREAD_PRIORITY_BELOW_NORMAL (-1)
THREAD_PRIORITY_NORMAL (0)
THREAD_PRIORITY_ABOVE_NORMAL (+1)
THREAD_PRIORITY_HIGHEST (+2)
THREAD_PRIORITY_TIME_CRITICAL (15||31)
Хотя в Windows Ваш процесс может иметь один из тридцати двух приоритетов (0-31), Вам в распоряжения даются только эти семь констант. Дело здесь в том, что все процессы могут иметь следующие статусы: фоновый, нормальный, высокоприоритетный, realtime, и системный. Со второй по шестую константы устанавливают приоритет потока относительно приоритета породившего его процесса, изменяя на величину указанную в скобках. Константы THREAD_PRIORITY_IDLE и THREAD_PRIORITY_TIME_CRITICAL сбрасывают приоритет потока в значения указанные в скобках (опять же зависит от приоритета породившего процесса). Например, у нас есть realtime ПРОЦЕСС с приоритетом 24, который порождает поток с приоритетом THREAD_PRIORITY_LOWEST, тогда абсолютный приоритет потока будет равен 24-2=22, если выставить приоритет THREAD_PRIORITY_IDLE , то абсолютный приоритет потока будет равен 16.

С потоками вроде все понятно, но давайте представим такую ситуацию: у нас есть некоторый указатель char *ptr, к которому обращаются два наших потока. Пусть в первом потоке выполняется такой код:

//******************************
delete [] ptr;
ptr = NULL;
//******************************
А во втором потоке следующий код:
//******************************
if( ptr )
ptr[0] = ptr[12];
//******************************

Класс для работы с мьютексами.

к началу статьи
Может случится так, что первый поток память удалит но указатель обнулить не успеет, т.к. операционная система может передать ресурсы второму потоку, который, будучи в полной уверенности, что с указателем все в порядке, “уронит” нашу программу. Что бы таких ситуаций не произошло, мы будем пользоваться мьютексами.
class	cMutex{
HANDLE MUTEX;
public:
cMutex( void ){
MUTEX = CreateMutex( NULL , false , NULL );
if( MUTEX == NULL ){
MessageBox(0,"Ошибка при создании мьютекса.","Ошибка.",0);
exit( 0 );
}
}
~cMutex(){CloseHandle( MUTEX );}
void EnterCriticalSection( void )
{WaitForSingleObject( MUTEX , INFINITE );}
void LeaveCriticalSection( void )
{ReleaseMutex( MUTEX );}
};
Использование этого класса изменит приведенный пример следующим образом:
//объявляем глобальный мьютекс, доступный обоим потокам
//******************************
cMutex CriticalSection;
//******************************
первый поток
	//******************************
CriticalSection.EnterCriticalsection();
delete [] ptr;
ptr = NULL;
CriticalSection.LeaveCriticalsection();
//******************************
второй поток
//******************************
CriticalSection.EnterCriticalsection();
if( ptr )
ptr[0] = ptr[12];
CriticalSection.LeaveCriticalsection();
//******************************
Функция WaitForSingleObject (если ресурс занят) ожидает освобождения мьютекса в течение заданного количества миллисекунд (второй параметр функции), если передать INFINITE, ожидание будет длиться вечно. Если ресурс свободен то она “закрывает за собой дверку“, и данный поток становится единственным держателем ресурса, в нашем примере это указатель. При выходе из критической секции остается только “открыть” ресурс(функция ReleaseMutex, или метод класса LeaveCriticalSection). Вот пожалуй и все что можно было сказать о потоках. Напоследок мне хотелось бы дать несколько советов:
  1. не рекомендуется использовать внутри критических секций недетерминированные циклы (циклы с неизвестным на этапе компиляции числом итераций, например while(1){…}, for(;eps<(xn-xn_1);){…})
  2. не рекомендуется проводить многочисленные вычисления внутри критической секции
  3. не рекомендуется пользоваться функцией WaitForMultipleObjects(ожидание освобождения нескольких ресурсов), т.к. это на порядок усложнит код, и заставит Вас решать проблему обхода тупиков.
Используйте потоки с умом и не злоупотребляйте ими, т.к. они могут стать как панацеей, так и головной болью.
исходные коды
© 2004-2005 Savardge.ru
Hosted by uCoz