Использование потоков
Перевод А. И. Легалова
Англоязычный оригинал находится на сервере компании
Reliable Software
Многозадачность - один из
наиболее трудных аспектов
программирования. Поэтому, для нее
тем более важно обеспечить простой
набор абстракций и инкапсулировать
их в хорошей объектно-ориентированной
оболочке. В ОО мире, естественным
аналогом потока, являющегося, чисто
процедурной абстракцией, служит
"Активный объект". Активный
объект обладает удерживаемым
потоком, который асинхронно
выполняет некоторые задачи. Этот
поток имеет доступ к всем
внутренним (закрытым) данным и
методам объекта. Открытый
интерфейс Активного объекта
доступен внешним агентам (таким как
основному потоку, или потоку,
несущему сообщения Windows). Поэтому,
они могут манипулировать
состоянием объекта также, как эта
манипуляция осуществляется из
удерживаемого потока. Хотя, режим
управления при этом сильно
ограничен.
Активный Объект сформирован как
каркас по имени ActiveObject. Построение
производного класса, как
предполагается, обеспечивает
реализацию для чистых виртуальных
методов InitThread, Run и Flush (также как и
написание деструктора).
class ActiveObject
{
public:
ActiveObject ();
virtual ~ActiveObject () {}
void Kill ();
protected:
virtual void InitThread () = 0;
virtual void Run () = 0;
virtual void FlushThread () = 0;
static DWORD WINAPI ThreadEntry (void *pArg);
int _isDying;
Thread _thread;
};
Конструктор класса ActiveObject
инициализирует удерживаемый поток,
передавая ему указатель функции,
которую предполагается выполнить и
указатель "this" на себя. Мы
должны отключить предупреждение,
сишнализирующее об использовании
"this" до полного создания
объекта. Мы знаем, что этот объект
не будет использоваться раньше
положенного, потому что поток
создается в неактивном состоянии.
Предполагается, сто конструктор
производного класса вызывает _thread.Resume
() чтобы активизировать поток.
// The constructor of the derived class
// should call
// _thread.Resume ();
// at the end of construction
ActiveObject::ActiveObject ()
: _isDying (0),
#pragma warning(disable: 4355) // 'this' used before initialized
_thread (ThreadEntry, this)
#pragma warning(default: 4355)
{
}
Метод Kill вызывает виртуальный
метод FlushThread - это необходимо для
завершения потока из любого
состояния ожидания и дает ему
возможность запустить _isDying для
проверки флажка.
void ActiveObject::Kill ()
{
_isDying++;
FlushThread ();
// Let's make sure it's gone
_thread.WaitForDeath ();
}
Мы также имеем каркас для функции
ThreadEntry (это - статический метод
класса ActiveObject, поэтому мы можем
определять соглашение о вызовах,
требуемое API). Эта функция
выполняется удерживаемым потоком.
Параметр, получаемый потоком от
системы является тем, который мы
передали конструктору объекта
потока - это указатель "this"
Активного Объекта. API ожидает void-указатель,
поэтому мы должны делать явное
приведение указателя на ActiveObject. Как
только мы овладеваем Активным
Объектом, мы вызываем его чистый
виртуальный метод InitThread, делать все
специфические для реализации
приготовления, а затем вызываем
основной рабочий метод Run.
Реализация метода Run оставлена
клиенту каркаса.
DWORD WINAPI ActiveObject::ThreadEntry (void* pArg)
{
ActiveObject * pActive = (ActiveObject *) pArg;
pActive->InitThread ();
pActive->Run ();
return 0;
}
Объект Thread - это тонкая
инкапсуляция API. Обратите внимание
на флажок CREATE_SUSPENDED, который
гарантирует, что нить не начнет
выполняться прежде, чем мы не
закончим конструирование объекта
ActiveObject.
class Thread
{
public:
Thread ( DWORD (WINAPI * pFun) (void* arg), void* pArg)
{
_handle = CreateThread (
0, // Security attributes
0, // Stack size
pFun,
pArg,
CREATE_SUSPENDED,
&_tid);
}
~Thread () { CloseHandle (_handle); }
void Resume () { ResumeThread (_handle); }
void WaitForDeath ()
{
WaitForSingleObject (_handle, 2000);
}
private:
HANDLE _handle;
DWORD _tid; // thread id
};
Синхронизация - это то, что
действительно делает
многозадачный режим столь
интенсивно используемым.
Давайте, начнем со взаимных
исключений. Класс Mutex - тонкая
инкапсуляция API. Вы внедряете Mutexes (мутации)
в ваш Активный Объект, а затем
используете их через Блокировки.
Блокировка (Lock) - умный объект,
который создается на стеке. В
результате чего, во время
обслуживания, ваш объект защищен от
любых других потоков. Класс Lock -
одно из приложений методологии Управления ресурсами.
Вы должны поместить Lock внутри всех
методов вашего Активного Объекта,
которые разделяют доступ к данным с
другими потоками.
class Mutex
{
friend class Lock;
public:
Mutex () { InitializeCriticalSection (& _critSection); }
~Mutex () { DeleteCriticalSection (& _critSection); }
private:
void Acquire ()
{
EnterCriticalSection (& _critSection);
}
void Release ()
{
LeaveCriticalSection (& _critSection);
}
CRITICAL_SECTION _critSection;
};
class Lock
{
public:
// Acquire the state of the semaphore
Lock ( Mutex & mutex )
: _mutex(mutex)
{
_mutex.Acquire();
}
// Release the state of the semaphore
~Lock ()
{
_mutex.Release();
}
private:
Mutex & _mutex;
};
Событие - это сигнальное
устройство, которое потоки
используют, чтобы связаться друг с
другом. Вы внедряете Событие (Event) в
ваш активный объект. Затем Вы
переводите удерживаемый поток в
состояние ожидания, пока некоторый
другой поток не освободит его. Не
забудьте однако, что, если ваш
удерживаемй поток ожидает события,
он не может быть завершен. Именно
поэтому Вы должны вызывать Release из
метода Flush.
class Event
{
public:
Event ()
{
// start in non-signaled state (red light)
// auto reset after every Wait
_handle = CreateEvent (0, FALSE, FALSE, 0);
}
~Event ()
{
CloseHandle (_handle);
}
// put into signaled state
void Release () { SetEvent (_handle); }
void Wait ()
{
// Wait until event is in signaled (green) state
WaitForSingleObject (_handle, INFINITE);
}
operator HANDLE () { return _handle; }
private:
HANDLE _handle;
};
Чтобы увидеть, как эти классы
могут быть использованы, я
предлагаю небольшую подсказку. Вы
можете перейти к странице, которая
объясняет, как класс ActiveObject
используется в Частотном
анализаторе для асинхронной
модификации дисплеев. Или, Вы
можете изучить более простой
пример Наблюдателя
Папки, который спокойно ждет,
отображая папки, и пробуждается
только, когда присходит изменение
ее содержимого.
Жаль, что я не могу сказать, что
программирование потоков является
простым. Однако, оно будет проще,
если Вы станете использовать
правильные примитивы. Это примитивы,
которые я рекламировал: ActiveObject, Thread,
Mutex, Lock и Event. Некоторые из них
фактически доступны в MFC. К примеру,
там есть блокирующий объект CLock (а
может быть - это ЧАСЫ [CLock - игра слов]?)
деструктор которого управляет,
критической секцией (он немного
менее удобен из-за "двухшаговой"
конструкции: Вы должны создать его,
а затем выбирать его в два
отдельных шага - как будто бы вы захотели
создать его, а затем передумать).
Другое отличие: MFC предлагает
только некоторую тонкую фанеру над
API и ничего нового.
Вы можете также распознать в
некоторых из механизмов, которые я
представил здесь, аналоги из языка
программирования Java. Конечно, когда
Вы имеете свободу проектирования
многозадачного режима в языке, Вы
можете позволять себе быть
изящными. Их версия ActiveObject
называется Runnable, и она имеет метод
run. Каждый объект Java потенциально
имеет встроенный mutex и все, что Вам
надо сделать, чтобы осуществить
блокировку, - это объявить метод (или
область) засинхронизированным.
Точно так же события реализованные
с ожиданиями подтверждениями
вызываются внутри любого
синхронизированного метода.
Поэтому, если Вы знаете, как
программировать на Java, Вы знаете,
как программировать на C++ (как будто
есть знатоки Java, неосведомленные о C++).
Далее:
использование потоков на примере Наблюдателя за папками.
|