Сообщений 0    Оценка 130        Оценить  
Система Orphus

Await && Locks

Внутреннее устройство

Автор: Dmytro Sheyko
Источник: sourceforge.net
Опубликовано: 30.12.2002
Исправлено: 13.03.2005
Версия текста: 1.1
Введение
Библиотеки спутники: memory и thread
Класс memory::RefCounter
Класс memory::Ptr<class Class: virtual public memory::RefCounter>
Классы thread::Thread и thread::ThreadImpl
Интерфейс thread::Runnable
Библиотека Await && Locks
Классы await_n::Monitor и await_n::MonitorImpl
Классы await_n::LockSlot и await_n::LockSlotObject
Классы await_n::LockQuery и await_n::LockQueryObject
Классы await_n::Lock и await_n::LockManager, await_n::LockImpl и await_n::LockManagerImpl
Классы await_n::SynchObject и await_n::SynchHandle
Классы await_n::AwaitSwitchObject и await_n::AwaitSwitchHandle
Классы await_n::LockSwitchObject и await_n::LockSwitchHandle

Введение

ПРЕДУПРЕЖДЕНИЕ

Следует обратить особое внимание на то, что, возможно, та часть данного описания, которая непосредственно касается внутреннего устройства Await && Locks (структура внутренних классов и используемые алгоритмы) скоро станет устаревшей, либо уже стала. Тем не менее, это не должно никак отразиться (по крайней мере, в худшую сторону) на приложениях, которые данную библиотеку используют.

Прежде чем начать описание внутренностей Await && Locks, я хотел бы объяснить некоторые детали, которые могут показаться странными и неочевидными; а дизайн библиотеки – избыточным и хаотичным. Приведенные ниже действия касаются только проектирования на C++. Я сознательно использую эту комбинацию якобы несовместимых терминов C++ и проектирование, несмотря на то, что C++ считается языком кодирования, а для проектирования обычно используют другие средства, например UML. Это связано с тем, что язык кодирования, так или иначе, оказывает влияние на мышление программиста-проектировщика и как следствие на этап проектирования в целом.

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

	struct Monitor {
		void enter();
		void leave();
		bool sleep(AbsoluteMoment);
		void awake();
	};
ПРИМЕЧАНИЕ

При определении интерфейса я использую ключевое слово struct вместо class. Это связано с тем, что мне просто лень каждый раз писать слово public. Тем более что в этом определении не-public членов нет и, в последствии, не будет.

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

	struct Monitor {
		virtual void enter() =0;
		virtual void leave() =0;
		virtual bool sleep(AbsoluteMoment) =0;
		virtual void awake() =0;
	};

Что мы имеем на данный момент. Набор интерфейсных классов (без реализации). Эти интерфейсные классы могут образовывать иерархии. Т.е наследоваться от одного или более интерфейсного класса. Однако я стараюсь не злоупотреблять наследованием. Если интерфейсный класс не присутствует в качестве параметров или возвращаемых значений методов других интерфейсных классов, то он явно лишний. Кроме того, интерфейс A может наследоваться от интерфейса B только в том случае, если в принципе не может существовать объекта, который если уж реализует A, то он просто не может не реализовывать B. Если же эта связь неочевидна, то лучше разделить интерфейсы. Кто знает, может нам понадобиться создать такой класс, который реализует только интерфейс A (без B)?

Интерфейс интерфейсных классов должен быть как можно проще. Такой вариант избыточен

	struct Thread {
		virtual bool _isFinished() =0;
		virtual bool _isRunning() =0;
	};

В подобном случае необходимо выбрать базис, а остальные методы реализовать как утилиты класса (не важно в каком виде, либо как методы, либо внешние функции). Например, так

	struct Thread {
		virtual bool _isFinished() =0;
		inline bool _isRunning() { return !_isFinished(); }
	};

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

Реализовать интерфейсы необходимо путем наследования.

	class MonitorImpl: public Monitor {
	private:
		// данные класса
	public:
		MonitorImpl();
		virtual ~MonitorImpl();
		virtual void enter();
		virtual void leave();
		virtual bool sleep(AbsoluteMoment);
		virtual void awake();
	};

Возможно, здесь у кого-то возникнет вопрос: «А зачем нужно создавать еще один класс? Почему нельзя просто добавить данные в тот первый, и методы заодно в нем реализовать? Зачем вообще нужны эти интерфейсные классы?». Вкратце отвечу так: «Во имя reuse’а!».

Допустим, мы не стали создавать новый класс, а просто дописываем старый. Потом, кто-то его начал использовать, а мы вдруг обнаружили, что можно реализовать этот класс лучше и эффективнее или мы просто нашли баг. Мы меняем реализацию нашего класса (причем меняем и хеадер – это стандартная ситуация, не всегда можно предвидеть последующие изменения в данных или же мы просто добавили/удалили private метод) и отдаем пользователю класса. И оказывается, что весь код, который использовал наш класс необходимо перекомпилировать! То, что компиляторы C++ не самые быстрые компиляторы в мире, это еще не самое страшное Обидно то, что пользовательский код может не компилироваться. Например, мы из своего хеадера исключили хеадер <windows.h>, пользователь явно его не включал (компилятор не в состоянии выдавать предупреждения в этом случае) и теперь у него везде Undeclared Identifier. Но наиболее неприятное то, что мы не можем просто подменить dll’ки. Компилятор C++ смотрит в хеадер для того, чтобы узнать размер класса (т.е. сколько места занимают данные) и потом его использует для того, чтобы выделять память для экземпляров данного класса. Хорошо, если размер класса уменьшился после изменений, хуже, если он увеличился. После этого память будет перекрываться. Вот такая вот у нас инкапсуляция в C++. Даже private не помогает!

Естественно, что подход, основанный на интерфейсных классах, усложняет дизайн. В частности, количество классов увеличивается в среднем в 2 раза (интерфейсные классы + соответствующие им, классы-реализации). Да и еще нельзя явно создавать объекты при помощи конструкторов (поскольку фактический тип не известен) и приходится прибегать к фабрикам класса. Но зато этот подход дает гибкость. Повторяю, этот подход основан на особенностях языка C++. Возможно, что в других языках, все было бы гораздо проще.

Возвращаясь к Await && Locks, следует заметить, что большое количество классов объясняется именно моим подходом к проектированию. Почти каждый класс библиотеки имеет двойника (один класс – интерфейс, другой – реализация). И как обычно класс реализация и объявлен, и реализован в .cpp-файле. Ну а теперь, я думаю, что пора приступать к внутренностям.

Библиотеки спутники: memory и thread

Класс memory::RefCounter

Файлы <memory/refcounter.h>, “refcounter.cpp”

Это базовый класс всех объектов, для которых необходимо осуществлять подсчет ссылок.

	class MEMORY_API RefCounter {
	private:
		long m_nRefCount;
	protected:
		virtual ~RefCounter();
	public:
		RefCounter();
		virtual void decRefCount();
		virtual void incRefCount();
	};

Конструктор инициализирует счетчик ссылок (т.е. переменную m_nRefCounter) в 1. Это мне кажется логичным, так как когда объект создан, кто-то не него ссылку таки получит (после того, как operator new вернет управление). Метод incRefCounter увеличивает счетчик ссылок на 1, decRefCounter – уменьшает на 1. Как только количество ссылок достигнет 0, объект будет удален. Такой код в decRefCount

	delete this;

не должен никого смущать. Так как деструктор объявлен виртуальным, и при выполнении данного выражения будет вызван настоящий деструктор, который корректно освободит память под объектом. Кроме того, доступ к членам класса после этого выражения не осуществляется. Деструктор тоже объявлен как protected. Это сделано для того, чтобы ни у кого не было соблазна удалить объект явно самому.

Объекты, наследующиеся от RefCounter’а, должны обязательно быть созданы в куче. Также наследование от него должно быть виртуальным, для того, чтобы с объектом был связан только один счетчик.

Класс memory::Ptr<class Class: virtual public memory::RefCounter>

Файлы <memory/refcounter.h>

Это smart pointer для RefCounter’а.

	template<class Class /* : virtual public RefCounter */>
	class Ptr {
	private:
		Class *m_pObject;
	public:
		// . . .
	};

Я не буду на нем останавливаться, так как реализация довольно тривиальна.

Классы thread::Thread и thread::ThreadImpl

Файлы <thread/thread.h>, “win32/thread.cpp” “posix/thread.cpp”

Интерфейсный класс

	struct Thread: virtual public memory::RefCounter {
		virtual int _isFinished() =0;
		inline int _isRunning() { return !_isFinished(); }
	};

Класс-реализация для Win32 (для POSIX аналогично).

	class ThreadImpl: public Thread {
	private:
		Runnable *m_pRunnable;
		static DWORD WINAPI ThreadRoutine(PVOID);
	public:
		ThreadImpl(Runnable *);
		virtual ~ThreadImpl();
		virtual int _isFinished();
		ThreadImpl *init();
	};

Фабрика класса

	THREAD_API Thread *createThread(Runnable *);

В интерфейсе Thread нет метода start. Фабрика класса возвращает объект, соответствующий уже запущенному потоку. Интерфейс Thread’а такой, что можно только узнать работает ли поток еще или нет. Остальные виды взаимодействия (например, приостановка потока или обмен какими-либо значениями) должны осуществляться иными средствами.

Фабрика createThread сначала создает объект класса ThreadImpl. Конструктор ThreadImpl инициализирует объект, но не запускает поток. Поток запускается в методе init (вызываемом из фабрики класса). Статический метод ThreadRoutine и есть та системно-зависимая функция, которая передается в качестве параметра в CreateThread или pthread_create. Поток создается отвязанным. Т.е. нет необходимости ему вызывать join. И если после создания потока интерфейс Thread не нужен, то можно сделать так.

	createThread(new MyRunnable())->decRefCounter();

В этом случае создастся поток, который нормально отработает и после завершения автоматически освободит ресурсы. Метод ThreadRoutine, перед тем как окончательно завершить поток, уменьшает счетчик ссылок на m_pRunnable, и устанавливает эту переменную в 0. Это и служит критерием для определения того, работает ли поток еще или нет (в методе _isFinished). Метод _isFinished должен всегда вызываться внутри единой критической секции. Об этом говорит символ подчеркивания в имени метода. При этом чтобы дождаться завершения потока можно использовать выражение await

	await(pThread->_isFinished) {}

Интерфейс thread::Runnable

Файлы <thread/thread.h>

	struct Runnable: virtual public memory::RefCounter {
		virtual void run() =0;
	};

Этот интерфейсный класс не имеет двойника-реализации. Бремя реализации данного интерфейса ложится на пользователя библиотеки thread. Экземпляр класса, реализующего данный интерфейс, можно (и нужно) передать фабрике потока createThread для того, чтобы его единственной метод run был вызван в контексте нового потока.

Библиотека Await && Locks

Классы await_n::Monitor и await_n::MonitorImpl

Файлы <await/monitor.h>, “i_monitor.h”, “win32/i_monitor.cpp”, “posix/i_monitor.cpp”, “win32/monitor.cpp”, “posix/monitor.cpp”

Интерфейсный класс

	struct Monitor {
		virtual void enter() =0;
		virtual void leave() =0;
		virtual bool sleep(AbsoluteMoment) =0;
		virtual void awake() =0;
	};

Win32 реализация

	class MonitorImpl: public Monitor {
	private:
		long m_nCount;
		HANDLE m_hMutex;
		HANDLE m_hEvent;
	public:
		MonitorImpl();
		virtual ~MonitorImpl();
		virtual void enter();
		virtual void leave();
		virtual bool sleep(AbsoluteMoment);
		virtual void awake();
	};

POSIX реализация

	class MonitorImpl: public Monitor {
	private:
		long m_count;
		pthread_mutex_t m_mutex;
		pthread_cond_t m_cond;
	public:
		MonitorImpl();
		virtual ~MonitorImpl();
		virtual void enter();
		virtual void leave();
		virtual bool sleep(AbsoluteMoment);
		virtual void awake();
	};

Тип AbsoluteMoment – это очень длинное целое (64 бита). В Win32 реализации это количество миллисекунд с момента старта системы, в UNIX реализации – с 00:00:00 1 января 1970. Но к этим датам не нужно привязываться, так как они могут измениться, а для преобразования относительного времени в абсолютное следует использовать cvtRelativeToAbsolute.

О том, как должен работать монитор, описано в user guide. Но я хотел бы обратить на некоторые нетривиальные особенности реализации. Итак, общая схема работы с монитором такая (чтобы не усложнять код, опустим timeout):

	pMonitor->enter();
	while(!(cond))
		pMonitor->sleep(ABS_INFINITE);
	action;
	pMonitor->awake();
	pMonitor->leave();

Эту схему в Win32 можно выразить так

	WaitForSingleObject(m_hMutex, INFINITE);
	while(!(cond)) {
		SignalObjectAndWait(m_hMutex, m_hEvent, INFINITE, FALSE);
		WaitForSingleObject(m_hMutex, INFINITE);
	}
	action;
	PulseEvent(m_hEvent);
	ReleaseMutex(m_hMutex);

Event должен быть обязательно с ручным сбросом, для того, чтобы PulseEvent разбудил все потоки, которые его ждут. Следует также обратить особое внимание на вызов SignalObjectAndWait. Эта функция присутствует только в Windows серии NT (а именно, Windows NT 4.0, Windows 2000, Windows XP). В Windows 95, Windows 98 и Windows Me она отсутствует. Однако ее можно выразить через другие функции так:

	WaitForSingleObject(m_hMutex, INFINITE);
	while(!(cond)) {
		ReleaseMutex(m_hMutex);
		// опасная точка
		WaitForSingleObject(m_hEvent, INFINITE);
		WaitForSingleObject(m_hMutex, INFINITE);
	}
	action;
	PulseEvent(m_hEvent);
	ReleaseMutex(m_hMutex);

Тем не менее, я использую именно SignalObjectAndWait. Это гарантирует, что переключение потока не произойдет в опасной точке, так как в противном случае поток, не успевший заснуть на event’е, может пропустить сигнал, который может послать другой поток вызовом PulseEvent.

POSIX реализация выглядит так:

	pthread_mutex_lock(&m_mutex);
	while(!(cond))
		pthread_cond_wait(&m_cond, &m_mutex);
	action;
	pthread_cond_broadcast(&m_cond);
	pthread_mutex_unlock(&m_mutex);

Вызов pthread_cond_wait гарантирует атомарность операции a priori (по стандарту).

Далее, в спецификации к монитору Await && Locks сказано, что монитор обязан быть рекурсивным. Для этого необходимо сделать mutex, используемый при реализации этого монитора, тоже рекурсивным. Что касается Win32, то там все mutex’ы рекурсивны изначально. В POSIX по умолчанию нет. В конструкторе POSIX монитора mutex устанавливается рекурсивным.

Однако этого мало. Рассмотрим такую ситуацию.

	WaitForSingleObject(m_hMutex, INFINITE);
	// mutex уже был занят
	// . . .
	WaitForSingleObject(m_hMutex, INFINITE);
	while(!(cond)) {
		SignalObjectAndWait(m_hMutex, m_hEvent, INFINITE, FALSE);
		WaitForSingleObject(m_hMutex, INFINITE);
	}
	action;
	PulseEvent(m_hEvent);
	ReleaseMutex(m_hMutex);
	// . . .
	// повторно отпускаем mutex
	ReleaseMutex(m_hMutex);

Перед тем как вызвать SignalObjectAndWait поток уже дважды занял mutex. Отрабатывая вызов SignalObjectAndWait, поток отпускает mutex и засыпает на event’е. Но отпускает mutex он только один раз! Поэтому другие потоки, которые могли бы его разбудить не могут войти в критическую секцию. В результате – дедлок. Чтобы избежать подобной ситуации, необходимо обеспечить, чтобы внутренний счетчик блокировок в системном mutex’е перед вызовом SignalObjectAndWait был равен 1. Для этого заведем свой счетчик блокировок, и будем поддерживать его в соответствующем состоянии.

Инвариант представления класса MonitorImpl – внутренний счетчик блокировок в системном mutex’е равен либо 0, либо 1; и если системный счетчик равен 0, то и собственный равен 0, если собственный больше 0, то системный равен 1. Вот так выглядит enter

	void MonitorImpl::enter() {
		WaitForSingleObject(m_hMutex, INFINITE);
		if(m_nCount>0)
			ReleaseMutex(m_hMutex);
		m_nCount++;
	}

Вызов WaitForSingleObject увеличивает системный счетчик блокировок на 1. Т.е. сразу после вызова он равен либо 1, либо 2. Далее, если собственный счетчик блокировок больше 0, то значение системного счетчика до вызова WaitForSingleObject было 1 (следствие инварианта), следовательно, после вызова оно равно 2. Вызов ReleaseMutex уменьшает значение системного счетчика на 1 до 1, т.е. приводим к инварианту. И под конец увеличиваем собственный счетчик блокировок. Доступ к собственному счетчику блокировок осуществляется только когда значение системного больше 0, т.е. в заданный момент только один поток имеет к нему доступ.

Метод leave выглядит так

	void MonitorImpl::leave() {
		m_nCount--;
		if(m_nCount == 0)
			ReleaseMutex(m_hMutex);
	}

Сначала мы уменьшаем собственный счетчик и если он становится после этого равным 0, сбрасываем в 0 системный. Здесь тоже доступ к собственному счетчику защищен.

Метод awake простой. Он никак не влияет на инвариант.

	void MonitorImpl::awake() {
		PulseEvent(m_hEvent);
	}

А вот метод sleep несколько сложнее.

	bool MonitorImpl::sleep(AbsoluteMoment moment) {
		moment -= GetTickCount();
		DWORD dwTimeout = (moment >= MAX_ABSOLUTE)? INFINITE: (DWORD) moment;
		long nCount = m_nCount;
		m_nCount = 0;
		DWORD res = SignalObjectAndWait(m_hMutex, m_hEvent, dwTimeout, FALSE);
		WaitForSingleObject(m_hMutex, INFINITE);
		m_nCount = nCount;
		return (res == WAIT_TIMEOUT);
	}

В первых двух строчках вычисляется значение таймаута для вызова SignalObjectAndWait. Далее, поскольку мы освобождаем mutex вызовом SignalObjectAndWait, другие потоки могут поменять значение собственного счетчика блокировок, который, между прочим, имеет смысл только в контексте данного потока. Поэтому мы сохраняем значение собственного счетчика блокировок в области данных, специфичной для данного потока, например, в локальной переменной. Мы также обнуляем значение собственного счетчика блокировок для истинности инварианта к тому моменту, когда другие потоки смогут получить к нему доступ, т.е. когда текущий поток заснет на event’е. Затем мы засыпаем. После пробуждения сохраняем причину пробуждения (по таймауту или по сигналу) и снова входим в критическую секцию. Как только мы вошли, мы восстанавливаем значение собственного счетчика блокировок, тем самым, восстанавливая значение инварианта для данного потока. Ну и под конец возвращаем причину, по которой был прерван сон.

Это все также относится и к POSIX реализации.

В приложении, использующем Await && Locks, изначально присутствует как минимум один экземпляр монитора, который можно получить вызовом getGlobalMonitor. Этот же экземпляр используется в конструкциях await, await_switch и synch.

Классы await_n::LockSlot и await_n::LockSlotObject

Файлы <await/lockmanager.h>, “i_lock.h”, “lockquery.cpp”

Вот объявление класса LockSlot

	class LockSlotObject;
	class AWAIT_API LockSlot {
	private:
		LockSlotObject *m_pObject;
		LockSlot(LockSlotObject *);
	public:
		LockSlot();
		LockSlot(LockSlot const &);
		~LockSlot();
		LockSlot &operator = (LockSlot const &);
		LockSlot &operator &= (LockSlot const &);
		LockSlot operator && (LockSlot const &) const;
		static LockSlot makeExclusiveLock(void const *pResource);
		static LockSlot makeSharedLock(void const *pResource);
	// for internal use
		inline LockSlotObject *getObject() const { return m_pObject; }
	};

Этот класс «виден» пользователю. Виден в том смысле, что этот класс объявлен в public хеадере. Что касается LockSlotObject, то он может только смутно догадываться о нем. И знать только, что LockSlot его как-то использует. Сам же LockSlotObject объявлен в private хеадере.

	enum { SHARED_LOCK = 0, EXCLUSIVE_LOCK = 1 };
	typedef map<void const *, int> UnitedLockTable;

	class LockSlotObject {
	private:
		UnitedLockTable m_mContent;
	public:
		LockSlotObject();
		LockSlotObject(void const *pResource, int nLockType);
		void add(LockSlotObject const *p);
		UnitedLockTable const &getUnitedLockTable() const { return m_mContent; }
	};
ПРИМЕЧАНИЕ

Отношение между двумя этими классами не является описанным выше отношением интерфейс-реализация. Тем не менее, данное отношение также необходимо для обеспечения инкапсуляции. Класс LockSlot является неким подобием smart pointer’а для LockSlotObject. Он также контролирует жизнь своего внутреннего объекта. Размер объекта класса LockSlot всегда равен размеру указателя на LockSlotObject и это не будет меняться в последствии. Интерфейс класса LockSlot четко определен и тоже не будет меняться. Интерфейс же и реализация класса LockSlotObject скрыта от пользователя библиотеки, и это дает возможность разработчику менять их по своему усмотрению. Собственно говоря, класс LockSlot нужен только из-за автоматического деструктора и операторов конъюнкции. Он в своей реализации делегирует вызовы LockSlotObject, а тот, в свою очередь, и выполняет всю полезную работу.

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

Конструктор LockSlot объявлен как private, а создать новый экземпляр этого класса можно либо при помощи статических методов makeExclusiveLock и makeSharedLock, либо при помощи оператора &&. В случае статических методов, будет создана конъюнкция, состоящая из одного конъюнкта; конъюнкта соответствующего типа (либо чтения, либо записи). Параметр, который передается в статические методы – адрес объекта, на который накладывается блокировка.

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

Классы await_n::LockQuery и await_n::LockQueryObject

Файлы <await/lockmanager.h>, “i_lock.h”, “lockquery.cpp”

Эти классы схожи со своими аналогами LockSlot и LockSlotObject. Только если те классы представляли конъюнкцию блокировок, то эти представляют дизъюнкцию конъюнкций (блокировок).

Вот LockQuery

	class LockQueryObject;
	class AWAIT_API LockQuery {
	private:
		LockQueryObject *m_pObject;
		LockQuery(LockQueryObject *);
	public:
		LockQuery();
		LockQuery(LockQuery const &);
		~LockQuery();
		LockQuery &operator = (LockQuery const &);
		LockQuery &operator |= (LockQuery const &);
		friend LockQuery operator || (LockQuery const &, LockQuery const &);
		friend LockQuery tagLock(long tag, LockSlot const &);
	// for internal use
		inline LockQueryObject *getObject() const { return m_pObject; }
	};

А вот LockQueryObject.

	typedef map<int, UnitedLockTable> ChoiceLockTable;

	class LockQueryObject {
	private:
		ChoiceLockTable m_mContent;
	public:
		LockQueryObject();
		LockQueryObject(long, LockSlotObject *);
		void add(LockQueryObject const *p);
		ChoiceLockTable const &getChoiceLockTable() const { return m_mContent; }
	};

Экземпляр класса LockQuery можно создать из конъюнкции блокировок при помощи функции tagLock, либо объединив несколько дизъюнктов при помощи оператора ||. В случае применения функции tagLock, с конъюнкцией связывается уникальное значение, ярлык дизъюнкта, по которому в последствии можно будет определить, какой дизъюнкт сработал.

LockQueryObject содержит в себе таблицу соответствия: ярлыка с конъюнкцией блокировок.

Классы await_n::Lock и await_n::LockManager, await_n::LockImpl и await_n::LockManagerImpl

Файлы <await/lockmanager.h>, “lockmanager.cpp”

Вот описание интерфейсных классов

	struct Lock {
		virtual long tag() =0;
		virtual void unlock() =0;
	};

	struct LockManager {
		virtual Lock *lock(LockQuery const &, long timeout, ExtraInfo const &) throw(LockException) =0;
		virtual void dumpLockTables() =0;
	};

Вот описание классов реализаций с сопутствующими им типами

	class LockManagerImpl;
	class LockImpl: public Lock {
	private:
		LockManagerImpl *m_pLockManager;
		long m_nTag;
		UnitedLockTable m_LockTable;
		threadid_t m_owner;
	public:
		LockImpl(LockManagerImpl *pLockManager, long nTag, UnitedLockTable const &rLockTable, threadid_t owner):
			m_pLockManager(pLockManager), m_nTag(nTag), m_LockTable(rLockTable), m_owner(owner)
		{}
		virtual ~LockImpl() {}
		virtual long tag() { return m_nTag; }
		virtual void unlock();
	};

	struct AcquiredLockInfo {
		long m_nAnyLockCount;
		long m_nExclusiveLockCount;
		AcquiredLockInfo(): m_nAnyLockCount(0), m_nExclusiveLockCount(0) {}
	};
	typedef map<void const *, AcquiredLockInfo> ResourceTable;
	typedef map<threadid_t, ResourceTable> ThreadTable;
	typedef map<threadid_t, ChoiceLockTable> QueryTable;

	typedef map<threadid_t, ExtraInfo const *> ExtraTable;

	class LockManagerImpl: public LockManager {
	private:
		MonitorImpl m_oMonitor;
		ResourceTable m_mResourceTable;
		ThreadTable m_mThreadTable;
		QueryTable m_mQueryTable;
		ExtraTable m_mExtraTable;
		void _dumpLockTables();
	public:
		LockManagerImpl();
		virtual ~LockManagerImpl();
		virtual Lock *lock(LockQuery const &, long secRelTimeout, ExtraInfo const &) throw(LockException);
		virtual void dumpLockTables();
		virtual void unlock(long tag, UnitedLockTable const &, threadid_t);
		int _may(threadid_t, long &);
		int _ddcheck(threadid_t, threadid_t) const;
	};

Итак, LockManager контролируется отдельным монитором. В приложении, использующем Await && Locks, теперь 2 монитора. Это сделано из-за того, что события, возникающие при блокировке конструкциями типа lock_it, никак не связаны с событиями типа await. И чтобы не тревожить лишний раз потоки, спящие на какой-то своей конструкции, я выделил 2 монитора.

Далее, LockManager содержит таблицу ресурсов (ResourceTable m_mResourceTable). Она позволяет узнать, сколько раз все потоки занимали этот ресурс (определяемый своим адресом) для чтения и сколько для записи. При этом каждому ресурсу соответствует структура AcquiredLockInfo, которая содержит поля: m_nAnyLockCount (которое подсчитывает все типы блокировок) и m_nExclusiveLockCount (которое подсчитывает только блокировоки записи). Допустим, что некому ресурсу соответствует такие значения: m_nAnyLockCount=5, а m_nExclusiveLockCount=2. Это означает, что на этот ресурс было наложено 2 блокировки записи и 5-2=3 блокировки чтения. По всей видимости, это натворил один и тот же поток, так как только один поток может наложить блокировку записи на ресурс в определенный момент, а если на ресурс были наложены блокировки записи, то наложить блокировку чтения может только тот поток, который уже наложил блокировки записи. И обратно, наложить блокировку записи на ресурс поток может только в том случае, если никто кроме него самого не накладывал не этот ресурс никаких блокировок. Последнее утверждение используется для того, чтобы определить, может ли поток наложить заданную блокировку. Что касается блокировок чтения, то можно сформулировать следующее утверждение. Наложить блокировку чтения на ресурс поток может только в том случае, если никто кроме него самого не накладывал не этот ресурс блокировок записи.

Кроме глобальной таблицы блокировок ресурсов, LockManager содержит таблицу блокировок ресурсов для каждого потока (ThreadTable m_mThreadTable). И по разнице значений количества блокировок в глобальной таблице и в таблице для конкретного потока определяется, сколько раз на данный ресурс было наложено блокировок всеми остальными потоками. Строго говоря, глобальная таблица избыточна, так как ее значение можно определить, используя таблицу блокировок ресурсов потоками. Но ее использование позволяет быстро определять, можно ли данному потоку занимать ресурс – для этого не нужно просматривать все потоки с целью определить, не занял ли какой из них данный ресурс.

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

Запрос, который какой-либо поток посылает LockManager’у, сохраняется в таблице запросов (QueryTable m_mQueryTable). Это позволяет знать, какие потоки чего ждут. Если поток в данный момент не ждет каких-либо блокировок, то, соответствующая ему запись, пуста. Данная таблица используется для обнаружения дедлоков.

И последняя таблица, которая содержится в LockManager’е, – это таблица дополнительной информации (ExtraTable m_mExtraTable). Она служит для отладки и содержит информацию, какой поток, на каком исходном файле и в какой строке сейчас выполняет блокировку. Содержимое этой таблицы участвует при выводе в методе LockManager::dumpLockTables().

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

	Lock *LockManagerImpl::lock(LockQuery const &query, long sRelTimeout, ExtraInfo const &extra) throw(LockException) {
		Lock *result = 0;
		threadid_t self = getSelfThreadId();
		AbsoluteMoment sAbsTimeout = cvtRelativeToAbsolute(sRelTimeout);
		int bTimedout = 0;
		int bDeadlocked = 0;

		m_oMonitor.enter();
		{
			m_mQueryTable[self] = query.getObject()->getChoiceLockTable();
			m_mExtraTable[self] = &extra;
	. . . . .

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

Если же ресурсы можно занять, то _may возвращает true и записывает в tag ярлык дизъюнкта, который можно занять. Если же нет, то он возвращает false и выполняется следующий код:

	. . . . .
			long tag;
			if(!_may(self, tag)) {
				bDeadlocked = _ddcheck(self, self);
				if(!bDeadlocked) {
					do {
						bTimedout = m_oMonitor.sleep(sAbsTimeout);
						if(bTimedout) break;
					} while(!_may(self, tag));
				}
				else {
					fprintf(stderr, "DEADLOCK HAS BEEN DETECTED!\n");
					fprintf(stderr, "victim is %.8lx\n", self);
					_dumpLockTables();
				}
			}
	. . . . .

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

Далее, если требуемые ресурсы можно занять, то мы их просто занимаем

	. . . . .
			ChoiceLockTable &choice = m_mQueryTable[self];
			ResourceTable &global = m_mResourceTable;
			ResourceTable &local = m_mThreadTable[self];
			if(!bTimedout && !bDeadlocked) {
				UnitedLockTable &united = choice[tag];
				for(UnitedLockTable::const_iterator j = united.begin(), m = united.end(); j != m; ++j) {
					void const *pResource = j->first;
					int nLockType = j->second;
					AcquiredLockInfo &gg = global[pResource];
					AcquiredLockInfo &ll = local[pResource];
					gg.m_nAnyLockCount++;
					ll.m_nAnyLockCount++;
					if(nLockType == EXCLUSIVE_LOCK) {
						gg.m_nExclusiveLockCount++;
						ll.m_nExclusiveLockCount++;
					}
					CHECK_VALIDITY(gg, ll);
				}
				result = new LockImpl(this, tag, united, self);
			}
	. . . . .

И создаем экземпляр класса LockImpl, который является удостоверением того, что ресурсы были заблокированы.

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

	. . . . .
			if(local.empty())
				m_mThreadTable.erase(self);
			m_mQueryTable.erase(self);
			m_mExtraTable.erase(self);
		}
		m_oMonitor.leave();
		if(bTimedout)
			throw TimeoutException();
		if(bDeadlocked)
			throw DeadlockException();
		return result;
	}

Проверка, может ли поток заблокировать необходимые, ресурсы осуществляется следующим образом (метод _may).

	int LockManagerImpl::_may(threadid_t self, long &tag) {
		int result = 0;
		ChoiceLockTable &choice = m_mQueryTable[self];
		ResourceTable &global = m_mResourceTable;
		ResourceTable &local = m_mThreadTable[self];
	. . . . .

Метод принимает в качестве параметра идентификатор потока. Во второй параметр, передаваемый по ссылке, будет записан ярлык дизъюнкта, дизъюнкта, который данный поток может занять. Возвращает false в том случае, если занять нельзя, true – если можно.

	. . . . .
		for(ChoiceLockTable::const_iterator i = choice.begin(), n = choice.end(); i != n; ++i) {
			long ctag = i->first;
			UnitedLockTable const &united = i->second;
			int found = 1;
	. . . . .

Сначала перебираются все дизъюнкты в запросе, с целью найти подходящий.

	. . . . .
			for(UnitedLockTable::const_iterator j = united.begin(), m = united.end(); j != m; ++j) {
				void const *pResource = j->first;
				int nLockType = j->second;
				AcquiredLockInfo gg, ll;
	. . . . .

Затем для каждого конъюнкта в дизъюнкте проверяется возможность его занять.

	. . . . .
				{
					ResourceTable::iterator ri;
					ri = global.find(pResource);
					if(ri != global.end())
						gg = ri->second;
					ri = local.find(pResource);
					if(ri != local.end())
						ll = ri->second;
				}
				CHECK_VALIDITY(gg, ll);
				found = (nLockType == EXCLUSIVE_LOCK)?
					(gg.m_nAnyLockCount == ll.m_nAnyLockCount):
					(gg.m_nExclusiveLockCount == ll.m_nExclusiveLockCount);
				if(!found)
					break;
			}
	. . . . .

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

	. . . . .
			if(found) {
				tag = ctag;
				result = 1;
				break;
			}
		}
		return result;
	}

Если оказалось, что все конъюнкты в текущем дизъюнкте можно занять, то мы прекращаем поиск и возвращаем true. Если же мы не нашли такой дизъюнкт, все конъюнкты которого можно занять, возвращаем false.

Разблокировка ресурсов осуществляется методом LockImpl::unlock и выполняется следующим образом.

	void LockImpl::unlock() {
		assert(m_owner == getSelfThreadId());
		m_pLockManager->unlock(m_nTag, m_LockTable, m_owner);
		delete this;
	}

Экземпляр класса Lock делегирует разблокирование LockManager’у, передавая ему конъюнкцию занятых блокировок (UnitedLockTable m_LockTable) и идентификатор потока владельца. В свою очередь, LockManager освобождает занятые потоком ресурсы так.

	void LockManagerImpl::unlock(long tag, UnitedLockTable const &united, threadid_t self) {
		m_oMonitor.enter();
		{
			ResourceTable &global = m_mResourceTable;
			ResourceTable &local = m_mThreadTable[self];
	. . . . .

Входит в критическую секцию.

	. . . . .
			for(UnitedLockTable::const_iterator j = united.begin(), m = united.end(); j != m; ++j) {
				void const *pResource = j->first;
				int nLockType = j->second;
				AcquiredLockInfo &gg = global[pResource];
				AcquiredLockInfo &ll = local[pResource];
				gg.m_nAnyLockCount--;
				ll.m_nAnyLockCount--;
				if(nLockType == EXCLUSIVE_LOCK) {
					gg.m_nExclusiveLockCount--;
					ll.m_nExclusiveLockCount--;
				}
	. . . . .

И для каждого конъюнкта освобождает ресурсы.

	. . . . .
				CHECK_VALIDITY(gg, ll);
				if(ll.m_nAnyLockCount == 0)
					local.erase(pResource);
				if(gg.m_nAnyLockCount == 0)
					global.erase(pResource);
			}
			if(local.empty())
				m_mThreadTable.erase(self);
	. . . . .

При этом по ходу подчищает таблицы.

	. . . . .
		}
		m_oMonitor.awake();
		m_oMonitor.leave();
	}

Ну и под конец, будит потоки, которые ждут освобождения ресурсов, и выходим из критической секции.

Проверка, приводит ли блокировка данного потока к дедлоку, осуществляется вызовом _ddcheck. Алгоритм проверки наличия дедлока основывается на поиске циклов в цепочке поток-ресурс-поток. Рассмотрим пример. Допустим, данный поток T пытается заблокировать ресурсы x для чтения, y и z для записи. К тому моменту, когда он пытается это сделать, ресурс x был уже занят потоком A для записи, ресурс y – потоками B и C для чтения, а ресурс z еще ни кем не был занят. В таком случае поток T должен дождаться, когда потоки A, B и C освободят ресурсы. Если все эти потоки, которые ждет поток T, в данный момент работают, то у потока T есть шанс, что когда-то он дождется желанных ресурсов. В том же случае, когда хотя бы один из них тоже ждет каких-то ресурсов, необходима дополнительная проверка. И если вдруг окажется, то поток B ждет ресурс t, который занял поток T, то это означает, то данные потоки (T и B) вовлечены в дедлок. Может также оказаться, что поток B зависит от потока T не напрямую, а посредством других потоков и ресурсов. Это означает, что необходимо продолжать анализ зависимостей потоков до тех пор, пока мы не дойдем до потока, который в данный момент работает (и не от кого не зависит), либо до потока T, и это будет означать, что поток T зависит от самого себя.

Следует также обратить внимание на то, что у потока может быть выбор, какие ресурсы занимать. И если он, собирается занять либо ресурсы x, y, z (которые приводит к взаимоблокировке), либо другую группу ресурсов (которая к дедлоку не приводит), то это означает, что у него еще есть шанс нормально продолжить свою работу.

В целом _ddcheck выглядит так. Этот метод проверяет, зависит ли явно или неявно поток another от потока self.

	int LockManagerImpl::_ddcheck(threadid_t self, threadid_t another) const {
		int result = 0;
		QueryTable::const_iterator ichoice = m_mQueryTable.find(another);
		if(ichoice != m_mQueryTable.end()) {
	. . . . .

Сначала ищем запись о потоке another в таблице запросов, если запись не найдена, то он явно не зависит от self. В таком случае возвращаем false (result = 0). В случае если он все-таки чего-то ждет, то перебираем дизъюнкты его запроса.

	. . . . .
			ChoiceLockTable const &choice = ichoice->second;
			result = 1;
			for(ChoiceLockTable::const_iterator i = choice.begin(), n = choice.end(); (result == 1) && (i != n); ++i) {
	. . . . .

В той гипотетической ситуации, если у него нет дизъюнктов, у него нет и выбора, и, следовательно, он находится в дедлоке с самим собой – возвращаем true (result = 1). Но, как правило, дизъюнкты есть, и мы для каждого дизъюнкта перебираем его конъюнкты.

	. . . . .
				UnitedLockTable const &united = i->second;
				result = 0;
				for(UnitedLockTable::const_iterator j = united.begin(), m = united.end(); (result == 0) && (j != m); ++j) {
	. . . . .

Конъюнкты тоже, в принципе, могли бы отсутствовать, поэтому result = 0.

	. . . . .
					void const *pResource = j->first;
					int nLockType = j->second;
	. . . . .

Итак, в этом месте мы имеем ресурс (pResource), который ждет поток another и тип его блокировки (int nLockType).

Далее, ищем потоки, которые его заняли и не дают жизни потоку another.

	. . . . .
					for(ThreadTable::const_iterator k = m_mThreadTable.begin(), l = m_mThreadTable.end(); (result == 0) && (k != l); ++k) {
						threadid_t thr = k->first;
						if(thr == another) continue;
	. . . . .

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

	. . . . .
						int bThreadFound = 0;
						ResourceTable::const_iterator h = k->second.find(pResource);
						if(h != k->second.end()) {
							if(nLockType == EXCLUSIVE_LOCK)
								bThreadFound = (h->second.m_nAnyLockCount > 0);
							else
								bThreadFound = (h->second.m_nExclusiveLockCount > 0);
						}
						if(bThreadFound) {
							result = (self == thr) || _ddcheck(self, thr);
	. . . . .

Опа! Мы нашли его! Если, как оказалось, тот поток (thr), что мы нашли и есть тот изначальный (self), для которого мы проверяли зависимость, то мы обнаружили дедлок. В противном случае проверяем зависимость найденного потока thr от self.

	. . . . .
						}
					} // for(k = [m_mThreadTable.begin() .. end()])
				} // for(j = [united.begin() .. end()])
			} // for(i = [choice.begin() .. end()])
		} // if(choice != m_mQueryTable.end())
		return result;
	}

Ну и под конец просто возвращаем результат.

Внимание, данный алгоритм может зацикливатся в том случае, если у нас в таблицах блокировок уже существовали циклы, в которых поток T не участвовал. Например, если поток T зависит от A, A зависит от B, а B зависит от A. Однако такая ситуация невозможна, потому, что Await && Locks разрешает дедлоки и, тем самым, сохраняет таблицы обесцикленными.

На этом описание большей части кода закончилось. Переходим к реализации макросов.

Классы await_n::SynchObject и await_n::SynchHandle

Файлы <await/await.h>, “await.cpp”.

Данные классы по тому, как они взаимодействию между собой, похожи на LockSlot и LockSlotObject. При этом полезную работу в данном случае выполняет SynchObject, а SynchHandle это просто класс-оболочка, который делегирует вызовы SynchObject.

Эти классы нужны для обеспечения работы конструкции synch, и в явном виде не должны нигде использоваться. Вот объявление и реализация класса SynchHandle

	class SynchObject;
	class AWAIT_API SynchHandle {
	private:
		SynchObject *m_pObject;
	public:
		SynchHandle();
		~SynchHandle();
		bool cont();
		void next();
	};

	SynchHandle::SynchHandle(): m_pObject(new SynchObject()) {}

	SynchHandle::~SynchHandle() { delete m_pObject; }

	bool SynchHandle::cont() { return m_pObject->cont(); }

	void SynchHandle::next() { m_pObject->next(); }

Вот объявление и реализация класса SynchObject

	class SynchObject {
	private:
		Monitor *m_pMonitor;
		bool m_bCont;
	public:
		SynchObject(): m_pMonitor(getGlobalMonitor()), m_bCont(true) {
			m_pMonitor->enter();
		}
		~SynchObject() {
			m_pMonitor->leave();
		}
		bool cont() { return m_bCont; }
		void next() { m_bCont = false; }
	};

А вот и сам макрос synch.

	#define synch() FOR(await_n::SynchHandle theAuxiliaryObject; theAuxiliaryObject.cont(); theAuxiliaryObject.next())

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

	#define FOR for

Для нестандартных (таких как MSVC) так.

	#define FOR if(0); else for

Итак, как же работает конструкция synch. Рассмотрим пример.

	synch()
		action();

Это выражение препроцессором разворачивается так.

	for(await_n::SynchHandle theAuxiliaryObject; theAuxiliaryObject.cont(); theAuxiliaryObject.next())
		action();

И на этапе выполнения это работает так. Сначала создается объект theAuxiliaryObject класса SynchHandle и вызывается его конструктор. Поскольку объекты классов SynchHandle и SynchObject жить друг без друга не могут, то в конструкторе класса SynchHandle создается объект класса SynchObject. А в конструкторе SynchObject у экземпляра глобального монитора вызывается метод enter, т.е. после создания theAuxiliaryObject работа ведется в глобальной критической секции. Далее следует проверка условия цикла for и вызывается метод cont. Сразу после создания объектов он всегда возвращает true, тем самым, разрешая выполнения тела цикла. Выполняется тело цикла (действие action). После выполнения – вызов метода next, который переменную m_bCont сбрасывает в false. Затем снова проверка условия цикла. На этот раз – false, значит выходим. При выходе вызываются деструкторы, и у глобального монитора вызывается leave.

Классы await_n::AwaitSwitchObject и await_n::AwaitSwitchHandle

Файлы <await/await.h>, “await.cpp”.

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

	#define await(cond) await_switch(await_n::FOREVER) await_case(cond)

Класс AwaitSwitchHandle

	class AwaitSwitchObject;
	class AWAIT_API AwaitSwitchHandle {
	private:
		AwaitSwitchObject *m_pObject;
	public:
		AwaitSwitchHandle(long);
		~AwaitSwitchHandle();
		bool cont();
		void next();
		bool check(bool);
		bool may();
		bool timedout();
	};

	AwaitSwitchHandle::AwaitSwitchHandle(long lTimeout): m_pObject(new AwaitSwitchObject(lTimeout)) {}

	AwaitSwitchHandle::~AwaitSwitchHandle() { delete m_pObject; }

	bool AwaitSwitchHandle::cont() { return m_pObject->cont(); }

	void AwaitSwitchHandle::next() { m_pObject->next(); }

	bool AwaitSwitchHandle::check(bool bFired) { return m_pObject->check(bFired); }

	bool AwaitSwitchHandle::may() { return m_pObject->may(); }

	bool AwaitSwitchHandle::timedout() { return m_pObject->timedout(); }

Класс AwaitSwitchObject

	class AwaitSwitchObject {
	private:
		Monitor *m_pMonitor;
		AbsoluteMoment m_tMoment;
		bool m_bTimedout;
		bool m_bFired;
	public:
		AwaitSwitchObject(long lTimeout):
			m_pMonitor(getGlobalMonitor()),
			m_tMoment(cvtRelativeToAbsolute(lTimeout)),
			m_bTimedout(false), m_bFired(false)
		{
			m_pMonitor->enter();
		}
		~AwaitSwitchObject() {
			m_pMonitor->awake();
			m_pMonitor->leave();
		}
		bool cont() {
			return !m_bFired;
		}
		void next() {
			if(m_bTimedout)
				m_bFired = true;
			if(!m_bFired)
				m_bTimedout = m_pMonitor->sleep(m_tMoment);
		}
		bool check(bool bFired) {
			m_bFired = bFired;
			return m_bFired;
		}
		bool may() {
			return (!m_bFired) && (!m_bTimedout);
		}
		bool timedout() {
			return m_bTimedout;
		}
	};

Макросы await_switch, await_case и await_timeout.

	#define await_switch(timeout)
		FOR(await_n::AwaitSwitchHandle theAuxiliaryObject(timeout); theAuxiliaryObject.cont(); theAuxiliaryObject.next())

	#define await_case(cond) if(theAuxiliaryObject.may() && theAuxiliaryObject.check(cond))

	#define await_timeout() if(theAuxiliaryObject.timedout())

Такое выражение

	await_switch(100) {
		await_case(cond1())
			action1();
		await_case(cond2())
			action2();
		await_timeout()
			action3();
	}

Разворачивается так

	for(await_n::AwaitSwitchHandle theAuxiliaryObject(100); theAuxiliaryObject.cont(); theAuxiliaryObject.next()) {
		if(theAuxiliaryObject.may() && theAuxiliaryObject.check(cond1()))
			action1();
		if(theAuxiliaryObject.may() && theAuxiliaryObject.check(cond2()))
			action2();
		if(theAuxiliaryObject.timedout())
			action3();
	}

И работает следующим образом. Вызываются конструкторы AwaitSwitchHandle и затем AwaitSwitchObject, и осуществляется вход в критическую секцию. Проверка условия цикла – true. Выполняем тело цикла. До тех пор пока не сработало условие cont1 или cont2 или не было таймаута, may возвращает true. Метод check возвращает то же самое значение, которое ему передается в качестве параметра. Если условие сработало (значение которого ему и передается) он также устанавливает в true значение переменной m_bFired, которое предотвращает последующие проверки условий.

Если в течение данной итерации ни одно из условий не сработало, next засыпает на вызове монитора sleep. Если поток проснулся по сигналу, происходят следующие итерации (т.е. проверки условий и выполнение действия, чье условие сработало). Если поток проснулся по таймауту, то совершается последняя итерация, в которой ищется выражение await_timeout (timedout возвращает true). Когда выполнение цикла заканчивается, независимо от того, по таймауту или по сработанному условию, вызываются деструкторы AwaitSwitchHandle и AwaitSwitchObject и при этом вызываются методы монитора awake и leave.

Классы await_n::LockSwitchObject и await_n::LockSwitchHandle

Файлы <await/await.h>, “await.cpp”.

Эти классы обеспечивают работу конструкций lock_it и lock_switch. Конструкция lock_it выражается через lock_switch следующим образом.

	#define lock_it(slot) lock_switch(await_n::FOREVER) lock_case(slot)

Класс LockSwitchHandle

	class LockSwitchObject;
	class AWAIT_API LockSwitchHandle {
	private:
		LockSwitchObject *m_pObject;
	public:
		LockSwitchHandle(long, ExtraInfo const &);
		~LockSwitchHandle();
		bool cont();
		void next();
		bool prep();
		void collect(LockSlot const &);
		bool selected();
		bool timedout();
		bool victimed();
	};

	LockSwitchHandle::LockSwitchHandle(long lTimeout, ExtraInfo const &rInfo):
		m_pObject(new LockSwitchObject(lTimeout, rInfo)) {}

	LockSwitchHandle::~LockSwitchHandle() { delete m_pObject; }

	bool LockSwitchHandle::cont() { return m_pObject->cont(); }

	void LockSwitchHandle::next() { m_pObject->next(); }

	bool LockSwitchHandle::prep() { return m_pObject->prep(); }

	void LockSwitchHandle::collect(LockSlot const &o) { m_pObject->collect(o); }

	bool LockSwitchHandle::selected() { return m_pObject->selected(); }

	bool LockSwitchHandle::timedout() { return m_pObject->timedout(); }

	bool LockSwitchHandle::victimed() { return m_pObject->victimed(); }

Класс LockSwitchObject

	class LockSwitchObject {
	private:
		LockManager *m_pLockManager;
		Lock *m_pLock;
		long m_lTimeout;
		ExtraInfo m_oExtraInfo;
		bool m_bTimedout;
		bool m_bVictimed;
		LockQuery m_oLockQuery;
		int m_nIndex;
		int m_nPhase;
	public:
		LockSwitchObject(long lTimeout, ExtraInfo const &rInfo):
			m_pLockManager(getLockManager()), m_pLock(0), m_lTimeout(lTimeout), m_oExtraInfo(rInfo),
			m_bTimedout(false), m_bVictimed(false), m_oLockQuery(), m_nIndex(0), m_nPhase(0)
		{}
		~LockSwitchObject()	{
			if(m_pLock)
				m_pLock->unlock();
		}
		bool cont() {
			return (m_nPhase<2);
		}
		void next() {
			++m_nPhase;
			m_nIndex = 0;
			if(1 == m_nPhase) {
				try {
					m_pLock = m_pLockManager->lock(m_oLockQuery, m_lTimeout, m_oExtraInfo);
				}
				catch(TimeoutException const &) {
					m_bTimedout = true;
				}
				catch(DeadlockException const &) {
					m_bVictimed = true;
				}
			}
		}	
		bool prep() {
			return (0 == m_nPhase);
		}
		void collect(LockSlot const &slot) {
			m_oLockQuery |= tagLock(m_nIndex++, slot);
		}
		bool selected() {
			return (m_pLock != 0) && (m_nIndex++ == m_pLock->tag());
		}
		bool timedout() {
			return m_bTimedout;
		}
		bool victimed() {
			return m_bVictimed;
		}
	};

Макросы lock_switch, lock_case, lock_timeout, lock_victim, rlock и wlock.

	#define lock_switch(timeout)
			FOR(await_n::LockSwitchHandle theAuxiliaryObject(timeout, ExtraInfo(__FILE__, __LINE__, __FUNCTION_NAME__));
				theAuxiliaryObject.cont(); theAuxiliaryObject.next())

	#define lock_case(slot)
			if(theAuxiliaryObject.prep())
				theAuxiliaryObject.collect(slot);
			else if(theAuxiliaryObject.selected())

	#define lock_timeout() if(theAuxiliaryObject.timedout())

	#define lock_victim() if(theAuxiliaryObject.victimed())

	#define rlock(lvalue) await_n::LockSlot::makeSharedLock(&(lvalue))

	#define wlock(lvalue) await_n::LockSlot::makeExclusiveLock(&(lvalue))

Такое выражение

	lock_switch(13) {
		lock_slot(wlock(g_oR1) && rlock(g_oR2))
			action1();
		lock_slot(wlock(g_oR3))
			action2();
		lock_timeout()
			action3();
		lock_victim()
			action4();
	}

Развернется в

	for(await_n::LockSwitchHandle theAuxiliaryObject(13, ExtraInfo(“myfile.cpp”, 45, “myfunction(...)”));
		theAuxiliaryObject.cont(); theAuxiliaryObject.next())
	{
		if(theAuxiliaryObject.prep())
			theAuxiliaryObject.collect(await_n::LockSlot::makeExclusiveLock(&g_oR1) &&
				await_n::LockSlot::makeSharedLock(&g_oR2));
		else if(theAuxiliaryObject.selected())
			action1();
		if(theAuxiliaryObject.prep())
			theAuxiliaryObject.collect(await_n::LockSlot::makeExclusiveLock(&g_oR3));
		else if(theAuxiliaryObject.selected())
			action2();
		if(theAuxiliaryObject.timedout())
			action3();
		if(theAuxiliaryObject.victimed())
			action4();
	}

И основная особенность выполнения конструкций lock_switch заключается в том, что цикл выполняется 2 раза. Первый раз – этап подготовки и составления запроса. Второй раз выполняется уже после того, когда LockManager либо заблокировал ресурсы, либо бросил исключение. И на этом, заключительном этапе в теле цикла просто выбирается действие, которое необходимо выполнить. А в остальном lock_switch выполняется, так же как и await_switch.


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