Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Роджерсон Д. - Основы COM - 2000.pdf
Скачиваний:
412
Добавлен:
13.08.2013
Размер:
2.4 Mб
Скачать

204

Разделенный — свободный

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

Как Вы видите, потоковые модели компонентов СОМ не слишком отличаются от обычных моделей потоков Win32. В процессе может быть любое число потоков. Эти потоки могут быть разделенными или свободными. С точки зрения программиста, в модели потоков СОМ есть только два интересных момента: синхронизация и маршалинг. СОМ синхронизирует вызовы компонентов в разделенных потоках. Разработчик синхронизирует вызовы компонентов в свободных потоках. Синхронизация компонентов в свободных потоках — это общая проблема многопоточности, а не специфика СОМ. Однако маршалинг специфичен для СОМ — и это единственное, что действительно уникально при работе с компонентами СОМ в многопоточной среде. Подробно мы рассмотрим ручной маршалинг интерфейсов позже, когда реализуем разделенный поток и поток клиента.

Реализация модели разделенных потоков

С компонентами внутри подразделений хорошо то, что им нет необходимости быть «потокобезопасными». Доступ к ним синхронизируется СОМ. При этом ни имеет значения, приходит ли вызов из потоков других подразделений или из свободных потоков. СОМ автоматически использует скрытую очередь сообщений Windows для синхронизации клиентских вызовов таких компонентов. Благодаря этому реализация компонентов в однопоточных подразделениях очень схожа с написанием оконных процедур. (Для синхронизации доступа к оконной процедуре используется цикл выборки сообщений; СОМ использует тот же механизм для синхронизации доступа к однопоточному подразделению.) Ниже следуют основные требования к подразделению:

!" Оно должно вызывать CoInitialize или OleInitialize.

!" В нем может быть только один поток.

!" У него должен быть цикл выборки сообщений.

!" Оно обязано выполнять маршалинг указателей на интерфейсы при передаче их другим подразделениям.

!" В случае компонента внутри процесса подразделение должно иметь «потокобезопасные» точки входа

DLL.

!" Ему может понадобиться «потокобезопасная» фабрика класса.

В следующих параграфах некоторые из этих требований рассматриваются подробнее.

Компонент может существовать только в одном потоке

Компонент в однопоточном подразделении должен выполняться в единственном потоке. Доступ к компоненту имеет только создавший его поток. Именно та работает оконная процедура — ее вызывает только поток, создавший данное окно. Так как доступ к компоненту возможен только из одного потока, такой компонент всегда выполняется в однопоточном подразделении и ему не нужно заботиться о синхронизации. Однако компонент должен защищать свои глобальные данные, поскольку он, как и оконная процедура, доступен для повторного входа (реентерабелен).

Необходим маршалинг интерфейсов через границы подразделений

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

Точки входа DLL должны быть «потокобезопасны»

Компоненту в однопоточном подразделении не нужно быть «потокобезопасным», так как доступ к нему возможен только из создавшего его потока. Однако «потокобезопасны» должны быть точки входа DLL, такие как

DllGetClassObject и DllCanUnloadNow. Функцию DllGetClassObject могут одновременно вызывать несколько клиентов из разных потоков. Чтобы сделать эти функции «потокобезопасными», убедитесь, что все совместно используемые данные защищены от параллельного доступа. В некоторых случаях это означает, что фабрика класса также должна быть «потокобезопасна».

205

Фабрикам класса может понадобиться «потокобезопасность»

Если для каждого компонента Вы создаете отдельную фабрику класса, такой фабрике «потокобезопасность» не требуется, поскольку доступ к ней возможен только для одного клиента. Но если DllGetClassObject создает одну фабрику класса, которая используется для порождения всех экземпляров компонента, Вы должны гарантировать «потокобезопасность» фабрики, поскольку к ней возможен одновременный доступ из разных потоков.

Компонент вне процесса может использовать один экземпляр фабрики класса для создания всех экземпляров компонента. Такая фабрика класса также должна быть «потокобезопасна». Обеспечение потокобезопасности большинства фабрик класса просто, так как они не изменяют никаких совместно используемых данных, кроме счетчика ссылок. Для защиты последних можно использовать InterlockedIncrement и InterlockedDecrement, что я и демонстрировал уже много лун тому назад в гл. 4.

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

Когда компонент в разделенном потоке передает свой интерфейс компоненту в другом потоке, для этого интерфейса требуется маршалинг. Неважно, является ли другой поток разделенным или свободным, маршалинг всегда необходим.

Автоматический маршалинг

Во многих случаях СОМ автоматически выполняет маршалинг интерфейса. В гл. 10 мы рассматривали DLL заместителя/заглушки, которые осуществляют маршалинг интерфейсов между процессами. С точки зрения программиста, потоковая модель не влияет на использование этих DLL. Они автоматически позаботятся о маршалинге между процессами.

DLL заместителя/заглушки используются СОМ и для маршалинга интерфейсов между разделенным потоком и другими потоками в том же процессе. Таким образом, когда Вы обращаетесь к интерфейсу компонента в другом подразделении, СОМ автоматически выполняет этот вызов через заместитель, и происходит маршалинг интерфейса.

Ручной маршалинг

Итак, когда же программист должен выполнять маршалинг указателя интерфейса самостоятельно? В основном тогда, когда он пересекает границу подразделения без помощи СОМ.

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

Второй случай имеет место тогда, когда фабрика класса компонента внутри процесса создает его экземпляры в разных потоках. Этот сценарий похож на предыдущий, но теперь создание компонента выполняет в разных потоках сервер (в предыдущем случае это делал клиент). Клиент вызывает CoCreateInstance, в результате чего запускается фабрика класса компонента. Когда клиент вызывает IClassFactory::CreateInstance, фабрика класса создает новый разделенный поток. Этот новый поток создает компонент. IClassFactory::CreateInstance должна вернуть клиенту указатель на интерфейс компонента. Но CreateInstance не может непосредственно передать клиенту указатель на интерфейс, созданный в новом подразделении, так как клиент находится в другом потоке. Таким образом, поток подразделения должен выполнить маршалинг указателя на интерфейс для CreateInstance, которая затем выполняет демаршалинг указателя и возвращает его клиенту.

Самое длинное имя API Win32

Теперь, когда мы узнали, где нужен маршалинг интерфейса, нам нужно знать, как его осуществлять. Вы можете выполнить всю работу сами при помощи функций CoMarshalInterface и CoUnMarshalInterface. Но если у Вас есть более интересные занятия, используйте вспомогательные функции с самыми длинными именами в API Win32,

CoMarshalInterThreadInterfaceInStream и CoGetInterfaceAndReleaseStream. (Если так пойдет и дальше, скоро имя функции будет занимать целый абзац.)

Использовать эти функции просто. Маршалинг указателя на интерфейс IX выполняется так:

206

IStream* pIStream = NULL;

HRESULT hr = CoMarshalInterThreadInterfaceInStream(

IID_IX,

// ID интерфейса,

маршалинг которого нужно выполнить

pIX,

//

Интерфейс, для

которого выполняется маршалинг

&pIStream);

//

Поток, куда будут помещены результаты маршалинга

Демаршалинг выполняется следующим образом:

IX* pIXmarshaled;

HRESULT hr = CoGetInterfaceAndReleaseStream(

pIStream,

// Поток, содержащий

интерфейс

 

 

IID_IX,

// ID демаршализуемого интерфейса

интерфейс

(void**)&pIXmarshaled);

// Демаршализованный

указатель

на

Все очень просто, не так ли? Это так просто потому, что СОМ незаметно для программиста и автоматически использует DLL заместителя/заглушки.

Настало время написать программу

До этого места данная глава носила весьма концептуальный характер, и тому была основательная причина: концепции здесь сложнее реализации. Давайте рассмотрим простой пример. Предположим, Вы хотите в фоновом режиме изменять счетчик в компоненте, и иногда обновлять значение счетчика, выводимое на дисплей. Если бы Вы писали нормальную программу Win32, то создали бы рабочий поток, который бы «фоном» изменял счетчик.

Здесь мы будем делать то же самое, но вместо рабочего потока используем разделенный поток. Главный поток создает разделенный поток. Разделенный поток создает компонент и периодически обновляет его счетчик. Этот поток будет передавать главному потоку указатель на интерфейс, чтобы главный поток мог получать и отображать значение счетчика. Все, как в обычном многопоточном программировании в Win32 — за исключением того, что поток подразделения:

!" Инициализирует библиотеку СОМ.

!" Имеет собственный цикл выборки сообщений.

!" Выполняет маршалинг интерфейса для передачи его обратно главному потоку. Компонент в точности похож на те, что мы писали ранее.

Теперь самая сложная часть в разработке однопоточного подразделения состоит в том, что у нас есть лишь концепция, а не код. Как обычно, Вы создаете поток. Как обычно, Вы создаете цикл выборки сообщений. Так как я хотел, чтобы подразделение выглядело более «настоящим», то создал для выполнения этих действий небольшой класс CSimpleApartment.

CSimpleApartment и CClientApartment

CSimpleApartment — это простой класс, инкапсулирующий создание компонента в другом потоке. Соответствующий код находится в файлах APART.H и APART.CPP в каталоге CHAP12\APT_THD на прилагающемся к книге диске. CSimpleApartment::StartThread запускает новый поток. CSimpleApartment::CreateComponent принимает CLSID компонента и создает его в потоке, запущенном

StartThread.

Именно здесь все становится интересным (или непонятным). CSimpleApartment охватывает оба потока. Часть CSimpleApartment вызывается первоначальным потоком, а другая часть — новым потоком. CSimpleApartment обеспечивает коммуникацию двух потоков. Поскольку CSimpleApartment::CreateComponent вызывается из первоначального потока, постольку она не может создать компонент непосредственно. Компонент надо создать в новом потоке. Поэтому CreateComponent использует событие, чтобы дать потоку нового подразделения сигнал к созданию компонента. Для собственно создания поток подразделения вызывает функцию

CreateComponentOnThread. CSimpleApartment::CreateComponentOnThread — это чисто виртуальная функция,

которую следует определить в производном классе. В этом первом примере производный класс CClientApartment реализует версию CreateComponentOnThread, которая создает компонент самым обычным способом — при помощи CoCreateInstance.

Пример с разделенным потоком

В табл. 12-1 показана структура вызовов функций в коде, который мы собираемся рассматривать. Весь код в правой части таблицы исполняется в разделенном потоке, созданном CSimpleApartment::StartThread.

207

Таблица 12-1 Структура вызовов функций в примере с разделенным потоком

Главный поток

Разделенный поток

 

 

 

WinMain

CSimpleApartment

CSimpleApartment

 

 

 

InitializeApartment

StartThread

RealThreadProc

 

 

ClassThreadProc

 

 

CClientApartment::WorkerFunction

 

CreateComponent

CreateComponentOnThread

 

 

CClientApartment::CreateComponentOnThread

 

 

 

CSimpleApartment::StartThread

Все самое интересное начинается в CLIENT.CPP с функции InitializeApartment. Она вызывает CSimpleApartment::StartThread, реализация которой приведена ниже:

BOOL CSimpleApartment::StartThread(DWORD WaitTime)

{

if (IsThreadStarted())

{

return FALSE;

}

 

// Создать поток

// Защита по умолчанию

m_hThread = ::CreateThread(NULL,

0,

// Размер стека по умолчанию

RealThreadProc,

 

(void*)this,

// Создать приостановленный

CREATE_SUSPENDED,

&m_ThreadId);

// поток

// Получить идентификатор

 

// потока

if (m_hThread == NULL)

{

trace("StartThread не может создать поток", GetLastError()); return FALSE;

}

trace("StartThread успешно создала поток");

//Создать событие для выдачи потоку команды на создание компонента m_hCreateComponentEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); if (m_hCreateComponentEvent == NULL)

{

return FALSE;

}

//Создать событие, которое сигнализируется потоком при завершении m_hComponentReadyEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); if (m_hComponentReadyEvent == NULL)

{

return FALSE;

}

trace("StartThread успешно создала события");

//Инициализировать время ожидания m_WaitTime = WaitTime;

//Поток был создан приостановленным; запустить его

DWORD r = ResumeThread(m_hThread); assert(r != 0xffffffff);

//Дождаться начала выполнения потока перед продолжением

WaitWithMessageLoop(m_hComponentReadyEvent);

return TRUE;

}

208

CSimpleApartment::StartThread создает новый поток при помощи ::CreateThread. Она также создает два события для синхронизации двух потоков. Функция CSimpleApartment::ClassThreadProc, выполняющаяся в потоке подразделения, использует m_hComponentReadyEvent дважды — сначала для сигнализации о том, что новый поток начал выполняться, и в конце для сигнализации о том, что он остановлен. Функция

CSimpleApartment::CreateComponent использует событие m_hCreateComponentEvent, чтобы выдать потоку подразделения команду на вызов CSimpleApartment::CreateComponentOnThread для создания компонента. После создания компонента CreateComponentOnThread устанавливает m_hCreateComponentEvent, чтобы уведомить об окончании создания CreateComponent.

CSimpleApartment::WaitWithMessageLoop — это вспомогательная функция, которая ожидает события. Она не просто ждет, а обрабатывает события Windows. Если Вы будете ждать события без обработки сообщений, пользователю покажется, что программа «зависла». Пользовательский интерфейс должен всегда обрабатывать сообщения в процесса ожидания. WaitWithMessageLoop использует функцию API Win32

MsgWaitForMultipleObjects, которую мы рассмотрим ниже.

CSimpleApartment::ClassThreadProc

При запуске потока вызывается статическая функция RealThreadProc, которая вызывает ClassThreadProc. Windows не может вызывать функции С++, поэтому функции обратного вызова Win32 обязаны быть статическими. При создании потока его процедуре передается указатель нашего класса, чтобы она могла вызвать

ClassThreadProc. Код ClassThreadProc приведен ниже:

DWORD CSimpleApartment::ClassThreadProc()

{

// Инициализировать библиотеку СОМ

HRESULT hr = CoInitialize(NULL); if (SUCCEEDED(hr))

{

//Сигнализировать, что поток запущен

SetEvent(m_hComponentReadyEvent);

//Ждать команды на создание компонента

BOOL bContinue = TRUE; while (bContinue )

{

switch(::MsgWaitForMultipleObjects(

1, &m_hCreateComponentEvent, FALSE,

m_WaitTime, QS_ALLINPUT))

{

//Создать компонент case WAIT_OBJECT_0:

CreateComponentOnThread();

break;

//Обработать сообщения Windows case (WAIT_OBJECT_0 + 1):

MSG msg;

while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))

{

if (msg.message == WM_QUIT)

{

bContinue = FALSE; break;

}

DispatchMessage(&msg);

}

break;

// Выполнить фоновую обработку case WAIT_TIMEOUT:

WorkerFunction();

break;

default:

trace("Ошибка ожидания", GetLastError());

}

209

}

// Освободить библиотеку COM CoUninitialize();

}

// Сигнализировать о завершении потока

SetEvent(m_hComponentReadyEvent); return 0;

}

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

GetMessage/DispatchMessage, ClassThreadProc использует MsgWaitForMultipleObjects, которая ожидает, пока не произойдет одно из трех событий: m_hCreateComponentEvent, сообщение Windows или истечение времени тайм-

аута. Если устанавливается событие m_hCreateComponentEvent, то MsgWaitForMultipleObjects прекращает ожидание и ClassThreadProc вызывает CreateComponentOnThread. Если потоку посылается сообщение Windows, то цикл PeekMessage/DispatchMessage извлекает и распределяет это сообщение (а также любые другие, находящиеся в очереди). Если истекает время тайм-аута, вызывается CSimpleApartment::WorkerFunction. Эта функция реализована производным классом CClientApartment, о котором мы поговорим ниже.

При желании Вы можете использовать GetMessage/DispatchMessage в чистом виде. Для выдачи команды на создание компонента вместо события можно использовать PostThreadMessage. Однако MsgWaitForMultipleObjects более эффективна.

CSimpleApartment::CreateComponent

Теперь, когда мы создали поток, пришла пора создавать компонент. Создание начинается с вызова главным потоком CSimpleApartment::CreateComponent. Код этой функции приведен ниже:

HRESULT CSimpleApartment::CreateComponent(const CLSID& clsid, const IID& iid, IUnknown** ppI)

{

// Инициализировать совместно используемые данные m_pIStream = NULL;

m_piid = &iid; m_pclsid = &clsid;

//Выдать потоку команду на создание компонента

SetEvent(m_hCreateComponentEvent);

//Ожидать завершения создания компонента

trace("Ожидать завершения создания компонента "); if (WaitWithMessageLoop(m_hComponentReadyEvent))

{

trace("Ожидание закончилось успешно");

if (FAILED(m_hr))

// Ошибка GetClassFactory?

{

 

return m_hr;

 

}

 

if (m_pIStream == NULL)

// Ошибка при маршалинге?

{

 

return E_FAIL;

 

}

 

trace("Демаршалинг указателя на интерфейс"); // Выполнить демаршалинг интерфейса

HRESULT hr = ::CoGetInterfaceAndReleaseStream(m_pIStream, iid, (void**)ppI);

m_pIStream = NULL; if (FAILED(hr))

{

trace("Ошибка CoGetInterfaceAndReleaseStream", hr); return E_FAIL;

}

return S_OK;

}

210

trace("Что случилось?"); return E_FAIL;

}

Функция CreateComponent выполняет четыре основных действия. Во-первых, она копирует свои параметры в переменные-члены. Во-вторых, она выдает потоку команду на создание компонента. В-третьих, она ждет завершения создания компонента. И в-четвертых, она выполняет демаршалинг запрошенного интерфейса компонента.

CSimpleApartment::CreateComponentOnThread

Когда CreateComponent устанавливает m_hCreateComponentEvent, ClassThreadProc вызывает приватную внутреннюю версию CreateComponentOnThread, а та выполняет два основных действия. Во-первых, она вызывает чисто виртуальную версию CreateComponentOnThread с параметрами, которые были переданы CreateComponent. Непосредственная передача параметров CreateComponentOnThread упрощает ее реализацию в производном классе. Во-вторых, она выполняет маршалинг интерфейса:

void CSimpleApartment::CreateComponentOnThread()

{

IUnknown* pI = NULL;

// Вызвать производный класс для фактического создания компонента m_hr = CreateComponentOnThread(*m_pclsid, *m_piid, &pI);

if (SUCCEEDED(m_hr))

{

trace("Компонент создан успешно");

// Выполнить маршалинг интерфейса для основного потока

HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(*m_piid, pI,

&m_pIStream);

assert(SUCCEEDED(hr));

// Освободить указатель pI pI->Release();

}

else

{

trace("Ошибка CreateComponentOnThread", m_hr);

}

trace("Сигнализировать главному потоку, что компонент создан");

SetEvent(m_hComponentReadyEvent);

}

CreateComponentOnThread использует функцию CoMarshalInterThreadInterfaceInStream для маршалинга указателя на интерфейс в другой поток. Код CreateComponent выполняет демаршалинг интерфейса.

CClientApartment

В этом примере CClientApartment реализует две виртуальные функции: CreateComponentOnThread и WorkerFunction. CClientApartment предназначена для использования клиентами, которые хотят создавать компоненты в разных потоках. Она переопределяет CreateComponentOnThread, чтобы вызвать CoCreateInstance:

HRESULT CClientApartment::CreateComponentOnThread(const CLSID& clsid, const IID& iid, IUnknown** ppI)

{

HRESULT hr = ::CoCreateInstance(clsid, NULL,

CLSCTX_INPROC_SERVER, iid,

(void**)ppI);

if (SUCCEEDED(hr))

{

// Запросить интерфейс IX, который используется в WorkerFunction hr = (*ppI)->QueryInterface(IID_IX, (void**)&m_pIX);

if (FAILED(hr))

{

// Если мы не можем с этим работать, на дадим и другим

(*ppI)->Release(); return E_FAIL;

Соседние файлы в предмете Программирование на C++