- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть II
- •Интерфейсы не изменяются
- •Полиморфизм
- •Что за интерфейсом
- •Таблица виртуальных функций
- •Указатели vtbl и данные экземпляра
- •Множественные экземпляры
- •Разные классы, одинаковые vtbl
- •Запрос интерфейса
- •IUnknown
- •Получение указателя на IUnknown
- •Знакомство с QueryInterface
- •Использование QueryInterface
- •Реализация QueryInterface
- •А теперь все вместе
- •Правила и соглашения QueryInterface
- •Вы всегда получаете один и тот же IUnknown
- •Вы можете получить интерфейс снова, если смогли получить его раньше
- •Вы можете снова получить интерфейс, который у Вас уже есть
- •Вы всегда можете вернуться туда, откуда начали
- •Если Вы смогли попасть куда-то хоть откуда-нибудь, Вы можете попасть туда откуда угодно
- •QueryInterface определяет компонент
- •Вы не можете воспользоваться всеми знаниями сразу
- •Работа с новыми версиями компонентов
- •Когда нужно создавать новую версию
- •Имена версий интерфейсов
- •Неявные соглашения
- •Управление временем жизни
- •Подсчет ссылок
- •Подсчет ссылок на отдельные интерфейсы
- •Реализация AddRef и Release
- •Когда подсчитывать ссылки
- •Оптимизация подсчета ссылок
- •Правила подсчета ссылок
- •Амуниция пожарного, резюме
- •Создание компонента
- •Экспорт функции из DLL
- •Загрузка DLL
- •Разбиваем монолит
- •Тексты программ
- •Связки объектов
- •Негибкое связывание, резюме
- •HRESULT
- •Поиск HRESULT
- •Использование HRESULT
- •Определение собственных кодов ошибки
- •GUID
- •Зачем нужен GUID?
- •Объявление и определение GUID
- •Сравнение GUID
- •Передача GUID по ссылке
- •Реестр Windows
- •Организация Реестра
- •Редактор Реестра
- •Необходимый минимум
- •Другие детали Реестра
- •ProgID
- •Саморегистрация
- •Категории компонентов
- •OleView
- •Некоторые функции библиотеки COM
- •Инициализация библиотеки COM
- •Управление памятью
- •Преобразование строк в GUID
- •Резюме
- •CoCreateInstance
- •Прототип CoCreateInstance
- •Использование CoCreateInstance
- •Контекст класса
- •Листинг кода клиента
- •Но CoCreateInstance недостаточно гибка
- •Фабрики класса
- •Использование CoGetClassObject
- •IClassFactory
- •CoCreateInstance vs. CoGetClassObject
- •Фабрики класса инкапсулируют создание компонентов
- •Реализация фабрики класса
- •Использование DllGetClassObject
- •Общая картина
- •Листинг кода компонента
- •Последовательность выполнения
- •Регистрация компонента
- •Несколько компонентов в одной DLL
- •Повторное применение реализации фабрики класса
- •Выгрузка DLL
- •Использование DllCanUnloadNow
- •LockServer
- •Резюме
- •Включение и агрегирование
- •Включение
- •Агрегирование
- •Сравнение включения и агрегирования
- •Реализация включения
- •Расширение интерфейсов
- •Реализация агрегирования
- •Магия QueryInterface
- •Неверный IUnknown
- •Интерфейсы IUnknown для агрегирования
- •Создание внутреннего компонента
- •Законченный пример
- •Слепое агрегирование
- •Агрегирование и включение в реальном мире
- •Предоставление информации о внутреннем состоянии
- •Моделирование виртуальных функций
- •Резюме
- •Упрощения на клиентской стороне
- •Smart-указатели на интерфейсы
- •Классы-оболочки C++
- •Упрощения на серверной стороне
- •Базовый класс CUnknown
- •Базовый класс CFactory
- •Использование CUnknown и CFactory
- •Резюме
- •Разные процессы
- •Локальный вызов процедуры
- •Маршалинг
- •DLL заместителя/заглушки
- •Введение в IDL/MIDL
- •Примеры описаний интерфейсов на IDL
- •Компилятор MIDL
- •Реализация локального сервера
- •Работа примера программы
- •Нет точек входа
- •Запуск фабрик класса
- •Изменения в LockServer
- •Удаленный сервер
- •Что делает DCOMCNFG.EXE?
- •Но как это работает?
- •Другая информация DCOM
- •Резюме
- •Новый способ общения
- •Старый способ общения
- •Использование IDispatch
- •Параметры Invoke
- •Примеры
- •Тип VARIANT
- •Тип данных BSTR
- •Тип данных SAFEARRAY
- •Библиотеки типа
- •Создание библиотеки типа
- •Библиотеки типа в Реестре
- •Реализация IDispatch
- •Генерация исключений
- •Маршалинг
- •Что Вы хотите сделать сегодня?
- •Потоковые модели COM
- •Потоки Win32
- •Подразделение
- •Разделенные потоки
- •Свободные потоки
- •Маршалинг и синхронизация
- •Реализация модели разделенных потоков
- •Автоматический маршалинг
- •Ручной маршалинг
- •Настало время написать программу
- •Пример с разделенным потоком
- •Реализация модели свободных потоков
- •Пример со свободным потоком
- •Оптимизация маршалинга для свободных потоков
- •Информация о потоковой модели в Реестре
- •Резюме
- •Программа Tangram
- •Tangram в работе
- •Детали и составные части
- •Клиентский EXE-модуль
- •Компонент TangramModel
- •Компоненты TangramGdiVisual и TangramGLVisual
- •Компоненты TangramGdiWorld и TangramGLWorld
- •Что демонстрирует пример
- •Файлы IDL
- •Файл DLLDATA.C
- •Циклический подсчет ссылок
- •Не вызывайте AddRef
- •Используйте явное удаление
- •Используйте отдельный компонент
- •События и точки подключения
- •IEnumXXX
12 глава
Многопоточность
Входящие в мой офис посетители постоянно бьются лбом о прикрепленный к потолку черный предмет сантиметров 30 длиной. Это копия вертолета Bell 206B-III Jet Ranger в масштабе 1:32. Копия не абсолютно точная — вместо хвостового винта сзади толкающий пропеллер благодаря которому модель может летать по кругу.
Выглядит это так. Вы включаете вертолет, и небольшой электромотор начинает вращать пропеллер. Мощности пропеллера не хватает, чтобы запустить движение, — надо слегка подтолкнуть. После этого вертолет начинает раскачиваться на подвесном шнуре. Постепенно пропеллер его разгоняет, угол между подвеской и потолком становится все меньше, и наконец, модель начинает быстро кружиться под потолком.
У этого маленького вертолета своя история. Его подарил мне Рёдигер Эш (Ruediger Asche), с которым мы вместе писали статьи для Microsoft Developer Network. Он знаток мрачных глубин ядра Windows NT, куда никогда не проникает свет GUI. Одна из областей специализации Рёдигера — многопоточное программирование. Вот мы и добрались до темы этой главы.
Если бы мы хотели написать программу моделирования моего вертолета, то могли бы использовать несколько потоков. Один из них отвечал бы за пользовательский интерфейс, дающий пользователю возможность управлять трехмерным изображением вращающегося вертолета. Другой бы вычислял положение вертолета при движении по кругу и вверх.
Однако для моделирования летающего по кругу вертолета многопоточность необязательна. По-настоящему она бывает полезна при построении пользовательского интерфейса с малым временем отклика. Интерфейс можно сделать живее и «доступнее», если переложить вычисления на фоновый поток. Наиболее это заметно в программах просмотра Web. Большинство из них перекачивают страницу данных в рамках одного потока, а выводят на экран в рамках другого; третий же дает возможность пользователю работать со страницей во время ее загрузки. Лично я не выношу сидеть и ждать, пока загружается куча ненужных картинок, — так что обычно щелкаю мышью на следующей гиперссылке еще до окончания загрузки и отрисовки. Эта удобная возможность обеспечивается многопоточностью.
Поскольку потоки так важны для быстрого отклика приложений, есть основания ожидать, что доступ к компоненту СОМ будет осуществляться несколькими потоками. Однако с использованием компонента несколькими потоками связан ряд специфических проблем, которые мы рассмотрим в данной главе. Эти проблемы незначительны и несопоставимы по масштабу с более общей проблемой многопоточного программирования. Мы не будем подробно рассматривать многопоточное программирование; посмотрим лишь, как многопоточность влияет на разработку и использование компонентов СОМ. Более подробно о многопоточном программировании можно прочитать в статьях Рёдигера Эша в MSDN.
Потоковые модели COM
COM использует потоки Win32 и не вводит новых типов потоков или процессов. В СОМ нет своих примитивов синхронизации, для создания и синхронизации потоков просто используется API Win32. Использование потоков в СОМ, кроме некоторых нюансов, не отличается от их использования в приложениях Win32. Мы рассмотрим эти нюансы, но сначала позвольте мне привести общий обзор потоков Win32.
Потоки Win32
В обычном приложении Win32 имеются потоки двух типов: потоки пользовательского интерфейса (userinterface threads) и рабочие потоки (worker threads). С потоком пользовательского интерфейса связаны одно или несколько окон. Такие потоки имеют циклы выборки сообщений, которые обеспечивают работу окон и реакцию на действия пользователя. Рабочие потоки используются для фоновой обработки и не связаны с окнами; в них обычно нет циклов выборки сообщений. В каждом процессе может быть несколько потоков пользовательского интерфейса и несколько рабочих потоков.
200
У потоков пользовательского интерфейса есть интересная особенность поведения. Как я только что сказал, у каждого потока пользовательского интерфейса есть одно или несколько окон. Оконная процедура данного окна вызывается только потоком, который владеет этим окном, — т.е. потоком, создавшим окно. Таким образом, оконная процедура всегда выполняется в одном и том же потоке, независимо от того, какой поток послал сообщение этой процедуре на обработку. Следовательно, все посланные данному окну сообщения синхронизированы, и окно с гарантией будет получать сообщения упорядоченно.
Преимущества для Вас, программиста, — в том, что нет нужды писать «потокобезопасные» оконные процедуры (а их писать не просто и, возможно, небыстро). Поскольку синхронизацию сообщений гарантирует Windows, Вам не нужно беспокоиться о том, что оконную процедуру могут вызвать одновременно несколько потоков. Эта синхронизация весьма полезна потокам, управляющим пользовательским интерфейсом. В конце концов, мы хотим, чтобы информация о действиях пользователя достигала окна в той же последовательности, в какой эти действия производились.
Потоки СОМ
СОМ использует те же два типа потоков, хотя и называет их по-другому. Вместо «поток пользовательского интерфейса» в СОМ говорят разделенный поток (apartment thread). Термин свободный поток (free thread)
используют вместо термина «рабочий поток». Самая сложная часть потоковой модели СОМ — терминология. Основная же сложность в ней состоит в несогласованности документации. Набор терминов, используемых в Win32 SDK, отличается от набора, используемого спецификацией СОМ. Я буду максимально избегать этой терминологии либо вводить термины как можно раньше. В этой главе я буду использовать термин разделенный поток для обозначения потока, подобного потоку пользовательского интерфейса, а термин свободный поток — для обозначения потока, подобного рабочему потоку.
Почему в СОМ вообще рассматривается потоковая модель, если она ничем не отличается от Win32? Причин две: маршалинг и синхронизация. Более подробно мы рассмотрим маршалинг и синхронизацию после того, как разберемся, что такое подразделение (apartment), модель разделенных потоков (apartment threading) и модель свободных потоков (free threading).
Подразделение
Хотя мне всерьез хотелось бы избежать новой терминологии, сейчас я определю термин подразделение (apartment). Подразделение — это концептуальный конгломерат, состоящий из потока в стиле пользовательского интерфейса (так называемый разделенный поток) и цикла выборки сообщений.
Возьмем типичное приложение Win32, которое состоит из процесса, цикла выборки сообщений и оконной процедуры. У каждого процесса есть как минимум один поток. Схематически приложение Windows представлено на рис. 12-1. Рамка пунктирными краями обозначает процесс. Рамка, внутри которой изображен цикл, представляет циклю выборки сообщений Windows. Две другие рамки изображают оконную процедуру и код программы. Все они расположены поверх линии, обозначающей поток управления.
Кроме процесса, рис. 12-1 иллюстрирует и подразделение. Один поток — это разделенный поток.
На рис. 12-2 та же схема иллюстрирует организацию типичного приложения СОМ, состоящего из клиента и двух компонентов внутри процесса. Программа работает внутри одного процесса и имеет единственный поток управления. У компонентов внутри процесса нет своих циклов выборки сообщений — они используют тот же цикл, что и клиентский EXE. И снова рисунок иллюстрирует одно подразделение.
Цикл выборки сообщений
Граница процесса
Код программы
Оконная
процедура
Поток управления
Рис. 12-1 Приложение Windows. Показаны: поток управления, цикл выборки сообщений, граница процесса и код программы
201
CoInitialize |
Компонент |
|
внутри |
|
процесса |
Клиент |
Компонент |
|
Компонент
CoUninitialize
Рис. 12-2 Клиент и два компонента внутри процесса. Имеется только один поток, и компоненты используют цикл выборки сообщений клиента совместно с клиентом
Использование компонентов внутри процесса не изменяет базовой структуры приложения Windows. Самой существенное различие между двумя изображенными процессами — в том, что процесс с компонентами обязан, прежде чем использовать какие-либо функции библиотеки СОМ, вызвать CoInitialize, а перед завершением вызывать CoUninitialize.
Добавим компонент вне процесса
Когда клиент подсоединяется к компоненту вне процесса, картина меняется. Такой клиент показан на рис. 12-3. Компонент находится в процессе, отдельном от процесса клиента. У каждого процесса свой поток управления. Цикл выборки сообщений предоставляется компоненту его сервером вне процесса. Если вернуться к примеру гл. 10, код такого цикла можно найти в OUTPROC.CPP. Другое существенное отличие от случая с компонентом внутри процесса — необходимость маршалинга вызовов между процессами. На рисунке такой вызов представлен «молнией». В гл. 10 мы узнали, как создать DLL заместителя/заглушки, которая используется для маршалинга данных между клиентом и компонентом вне процесса.
На рис. 12-3 изображены два подразделения. В одном из них находится клиент, а в другом — компонент. Может показаться, что подразделение — то же самое, что и процесс, но это неверно. В одном процессе может быть несколько подразделений.
Сервер компонента |
|
|
|
|
вне процесса |
|
|
CoInitialize |
|
CoInitialize |
Для вызовов внутри |
|
|
|
|
|
|
Компонент |
процесса маршалинг |
|
|
не выполняется |
|
|
|
|
|
Клиент |
Для вызовов |
|
|
|
между |
Компонент |
|
|
процессами |
|
|
|
|
|
|
|
необходим |
|
|
|
маршалинг |
|
|
CoUninitialize |
Цикл выборки |
CoUninitialize |
|
|
сообщений |
|
|
|
компонента вне |
|
|
|
процесса |
|
|
Рис. 12-3 У компонента вне процесса есть собственный цикл выборки сообщений и поток
На рис. 12-4 я превратил компонент рис. 12-3 из компонента вне процесса в компонент внутри процесса, расположенный в другом подразделении.
Штриховыми линиями изображены подразделения. Пунктирная линия по-прежнему обозначает границу процесса.
Обратите внимание, как похожи два рисунка. По существу, я нарисовал вокруг старой картинки новую рамку и объявил, что теперь объекты находятся в одном процессе. Из этого должна стать очевидной моя точка зрения — подразделения аналогичны (однопоточным) процессам в следующих моментах. И у процесса, и у подразделения есть собственный цикл выборки сообщений. Маршалинг вызовов функций внутри (однопоточного) процесса и
202
внутри подразделения не нужен. Имеет место естественная синхронизация, так как и у процесса, и у подразделения только один поток. Синхронизация вызовов функций между процессами и между подразделениями производится при помощи цикла выборки сообщений. И последняя деталь — каждый процесс должен инициализировать библиотеку СОМ. Точно так же и каждое подразделение должно инициализировать библиотеку СОМ. Теперь, если вернуться к рис. 12-2, Вам станет понятно, как клиент и два компонента сосуществуют внутри одного подразделения.
Граница |
Для вызовов между |
|
Компонент внутри |
|||
подразделениями |
|
процесса находится в |
||||
процесса |
необходим маршалинг |
|
другом подразделении |
|||
|
|
|
|
|
|
|
|
|
|
|
|
||
|
CoInitialize |
|
CoInitialize |
|
||
|
|
|
|
|
||
|
|
|
|
Компонент |
|
|
|
Клиент |
|
||||
|
|
|
|
|
||
|
|
|
|
|
||
|
|
|
|
Компонент |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CoUninitialize |
|
|
CoUninitialize |
|
|||
|
|
|
|
|
|
|
|
|
|
Границы подразделений |
|
Цикл выборки сообщений |
|||
|
|
|
|||||
|
|
||||||
|
|
|
|
|
|
используется процедурой |
|
|
|
|
|
|
|
|
потока |
Рис. 12-4 Клиент взаимодействует внутри процесса с компонентом, расположенным в другом подразделении
Разделенные потоки
Разделенный поток — это единственный поток внутри подразделения. Но когда Вы слышите термин «разделенный поток», представляйте себе поток пользовательского интерфейса. Вспомните, что поток пользовательского интерфейса владеет созданным им окном. Оконная процедура вызывается только им. Между разделенным потоком и созданным им компонентом существуют такие же отношения. Разделенный поток владеет созданным им компонентом. Компонент внутри подразделения будет вызываться только соответствующим разделенным потоком.
Если поток посылает сообщение окну, принадлежащему другому потоку, Windows помещает это сообщение в очередь сообщений соответствующего окна. Цикл выборки сообщений этого окна выполняется потоком, создавшим окно. Когда цикл выбирает очередное сообщение и вызывает оконную процедуру, для вызова процедуры используется тот же поток, который создал окно.
То же самое верно для компонента внутри подразделения. Предположим, что метод Вашего компонента, находящегося в подразделении, вызывается другим потоком. СОМ автоматически помещает этот вызов в очередь подразделения. Цикл выборки сообщений извлекает этот вызов и вызывает метод с помощью потока подразделения.
Таким образом, компонент внутри подразделения вызывается только потоком подразделения, и ему нет нужды заботиться о синхронизации. Так как СОМ гарантирует, что все вызовы такого компонента будут упорядочены, компоненту не требуется быть «потокобезопасным». Это значительно облегчает написание кода компонента. Ни один из компонентов, которые мы написали в этой книге, не был «потокобезопасным». Но, пока их создают разделенный потоки, мы можем быть уверены, что их методы никогда не будут вызваны разными потоками одновременно.
Именно в этом состоит отличие свободных потоков от разделенных.
Свободные потоки
СОМ упорядочивает вызовы компонентов для разделенных потоков. Однако синхронизация не выполняется для компонентов, созданных свободными потоками. Если компонент создан свободным потоком, он может вызываться любым потоком и в любой момент времени. Разработчик должен гарантировать, что его компонент сам синхронизирует доступ к себе. Такой компонент должен быть «потокобезопасным». Модель свободных потоков переносит заботу о синхронизации с СОМ на компонент.