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

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 помещает это сообщение в очередь сообщений соответствующего окна. Цикл выборки сообщений этого окна выполняется потоком, создавшим окно. Когда цикл выбирает очередное сообщение и вызывает оконную процедуру, для вызова процедуры используется тот же поток, который создал окно.

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

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

Именно в этом состоит отличие свободных потоков от разделенных.

Свободные потоки

СОМ упорядочивает вызовы компонентов для разделенных потоков. Однако синхронизация не выполняется для компонентов, созданных свободными потоками. Если компонент создан свободным потоком, он может вызываться любым потоком и в любой момент времени. Разработчик должен гарантировать, что его компонент сам синхронизирует доступ к себе. Такой компонент должен быть «потокобезопасным». Модель свободных потоков переносит заботу о синхронизации с СОМ на компонент.

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