Интернет-журнал "Домашняя лаборатория", 2007 №6 - Усманов
Шрифт:
Интервал:
Закладка:
Одна из важнейших связанных с потоками проблем — синхронизация их доступа к разделяемым данным. Это затрагивает глобальные и статические переменные. Рассмотрим следующий сценарий. Поток А считывает значение глобальной переменной X, после чего его выполнение прерывается. После этого поток В считывает и модифицирует значение переменной X. После восстановления контекста потока А этот поток модифицирует переменную X, уничтожая тем самым результат работы потока В.
Один из способов решения указанной проблемы — использование критических секций. Некоторый участок кода объявляется критической секцией, и поток, желающий войти в эту критическую секцию (начать выполнение содержащегося в ней кода), сообщает об этом другим потокам данного процесса. Он входит в критическую секцию только получив разрешение от всех других потоков. Алгоритм переговоров между потоками, гарантирующий достижение соглашения, был предложен первоначально Дейкстра и позже улучшен Кнутом.
Следующий стандартный псевдокод иллюстрирует использование критических секций
class CoClass { public:
CoClass::CoClass ()
{
InitializeCriticalSection(&m_cs);
}
CoClass::~CoClass()
{
DeleteCriticalSection(&m_cs);
}
HRESULT __ stdcall CoClass::Method()
{
EnterCriticalSection(&m cs);
……
LeaveCriticalSection(&m_cs);
return S_OK;
}
private:
CRITICAL_Section m_cs;
};
В данном случае критическая секция охватывает целиком метод Method. Возможно, конечно, включить в критическую секцию и небольшой участок кода.
Использование критических секций позволяет запретить параллельное выполнение некоторых участков кода. Но это не решает всех проблем. Например, как с помощью критических секций обеспечить синхронизацию доступа к глобальной переменной g_objCount, используемой для подсчета числа активированных экземпляров классов, определенных в сервере PubInProcServer? Здесь удобнее применять функции
InterLockedIncrement(&g_objCount)
и
InterLockedDecrement (&g_objcount),
обеспечивающие безопасное увеличение и уменьшение на единицу значения параметра функции.
Последнее, что нужно упомянуть в связи с потоками — понятие окна и очереди сообщений. В Windows все потоки делятся на два класса: потоки с окном и рабочие потоки. В первом случае с потоком связано некоторое окно (возможно, скрытое), оконная процедура и очередь сообщений. Поток выбирает очередное сообщение из очереди сообщений и переправляет его в оконную процедуру, которая и определяет способ обработки данного сообщения. Это реализуется с помощью следующего цикла
MSG msg;
while(GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
Во втором случае окно, оконная процедура и очередь сообщений не используются.
Потоки с окном используются прежде всего при реализации интерфейса пользователя. Однако, эти потоки играют особую роль и в СОМ. Связанная с таким потоком очередь сообщений используется для синхронизации доступа к объектам, которые не рассчитаны на одновременный вызов их методов из различных потоков. Подробнее об этом рассказывается в следующем разделе.
Апартаменты
СОМ появился до того, как в Windows появилась многопоточность. В связи с этим, в рамках одного приложения могут взаимодействовать объекты, имеющие различный уровень знаний о потоках. Некоторые объекты ничего не знают о потоках. Если их погрузить в среду, в которой выполняется несколько потоков, то возможны проблемы. При параллельном вызове одного и того же метода такого объекта возможны, например, ранее описанные ошибки модификации глобальных и статических переменных. Другие объекты могут быть написаны с учетом возможности параллельного вызова их методов, например, с использованием критических секций. Такие объекты называются потоко-безопасными.
Апартаменты — это способ обеспечить совместную работу в рамках одного приложения объектов обоих типов. Тривиальное решение — запретить параллельное обращение ко всем объектам. Но это во многих случаях недопустимо. Например, система должна быстро реагировать на ввод данных от нескольких параллельно работающих пользователей. Следовательно, необходимо обеспечить для объекта каждого типа безопасную работу с максимально возможной производительностью.
Апартамент — это относительно сложное понятие. Но, тем не менее, следует затратить определенные усилия на то, чтобы разобраться в данном вопросе. Имеются экспертные оценки, в соответствии с которыми до 40 процентов ошибок при создании систем, основанных на технологии СОМ, вызваны недостаточным пониманием апартаментов.
Итак, начнем с иерархии процессов, апартаментов, потоков и объектов:
• В каждом процессе имеется один или несколько апартаментов.
• Каждый поток живет в определенном апартаменте.
• Каждый объект живет в определенном апартаменте.
Имеется три типа апартаментов:
• STA (Single-Threaded Apartment)
В одном процессе может иметься несколько апартаментов этого типа. Один из них (созданный первым) носит название главного STA. В STA живут объекты, не поддерживающие параллельный вызов своих методов. В каждом STA живет ровно один поток, причем это поток с окном. Именно этот поток выполняет все вызовы методов объектов, живущих в данном апартаменте.
• МТА (MultiThreaded Apartment)
В одном процессе может иметься только один МТА. В МТА живут объекты, поддерживающие параллельный вызов своих методов. С МТА связан пул потоков (рабочих потоков), из которого и выбирается новый поток для выполнения нового вызова метода объекта, живущего в данном апартаменте.
• NA (Neutral apartment)
В одном процессе может иметься только один NA. В NA живут объекты, поддерживающие параллельный вызов своих методов. Нет потоков, связанных с NA навечно. Любой поток из STA или МТА может временно покинуть свой апартамент и заняться выполнением некоторого метода некоторого объекта, живущего в NA.
Теперь рассмотрим процесс создания апартаментов, связь с ними потоков и объектов. Ограничимся случаем сервера, исполняющегося в процессе клиента.
Потоки создаются клиентом. Клиент имеет возможность связать данный поток с STA или МТА апартаментами. При этом либо создается новый апартамент, либо поток связывается с уже существующим апартаментом.
Если поток собирается работать с СОМ, он должен вызвать
CoInitialize(NULL), CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)
или
CoInitializeEx(NULL, COINIT_MULTITHREADED).
В первых двух случаях создается НОВЫЙ STA и данный поток связывается с этим STA. Первый из созданных STA объявляется главным. В третьем случае создается МТА, если он не был создан ранее. Данный поток связывается с МТА.
При создании апартамента в куче создается специальный объект апартамента, содержащий, в частности, такую информацию как идентификатор апартамента и его тип. Связь потока с апартаментом обеспечивается тем, что идентификатор апартамента сохраняется в локальной памяти потока (TLS — Thread Local Storage).
Создание объекта инициируется в определенном потоке, который уже приписан некоторому апартаменту. Но тип объекта, допустимость параллельного вызова его методов не определяется при создании объекта. Эта информация задается разработчиком класса и хранится в реестре системы.
Мы уже знаем, что если некоторый сервер предназначен для выполнения в процессе клиента, то