Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6250
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Г Л А В А 1 2

Волокна

Майкрософт добавила в Windows поддержку волокон (fibers), чтобы упростить портирование (перенос) существующих серверных приложений из UNIX в Windows. С точки зрения терминологии, принятой в Windows, такие серверные приложения следует считать однопоточными, но способными обслуживать множество клиентов. Иначе говоря, разработчики UNIX-приложений создали свою библиотеку для организации многопоточности и с ее помощью эмулируют истинные потоки. Она создает набор стеков, сохраняет определенные регистры процессора и переключает контексты при обслуживании клиентских запросов.

Разумеется, чтобы добиться большей производительности от таких UNIXприложений, их следует перепроектировать, заменив библиотеку, эмулирующую потоки, на настоящие потоки, используемые в Windows. Но переработка может занять несколько месяцев, и поэтому компании сначала просто переносят существующий UNIX-код в Windows — это позволяет быстро предложить новый продукт на рынке Windows-приложений.

Но при переносе UNIX-программ в Windows могут возникнуть проблемы. В частности, механизм управления стеком потока в Windows куда сложнее простого выделения памяти. В Windows стеки начинают работать, располагая сравнительно малым объемом физической памяти, и растут по мере необходимости (об этом я расскажу в разделе «Стек потока» главы 16). Перенос усложняется и наличием механизма структурной обработки исключений (см. главы 23,24 и 25).

Стремясь помочь быстрее (и с меньшим числом ошибок) переносить UNIXкод в Windows, Майкрософтдобавила в операционную систему механизм поддержки волокон. В этой главе мы рассмотрим концепцию волокон и функции, предназначенные для операций с ними. Кроме того, я покажу, как эффективнее работать с такими функциями. Но, конечно, при разработке новых приложений следует использовать настоящие потоки.

Глава 12. Волокна.docx 423

Работа с волокнами

Во-первых, потоки и Windows реализуются на уровне ядра операционной системы, которое отлично осведомлено об их существовании и «коммутирует» их в соответствии с созданным Майкрософт алгоритмом. В то же время волокна реализованы на уровне кода пользовательского режима, ядро ничего не знает о них, и процессорное время распределяется между волокнами по алгоритму, определяемому нами. А раз так, то о вытеснении волокон говорить не приходится — по крайней мере, когда дело касается ядра.

Второе, о чем следует помнить, — в потоке может быть одно или несколько волокон. Для ядра ноток — все то, что можно вытеснить и что выполняет код. Единовременно поток будет выполнять код лишь одного волокна — какого, решать вам (соответствующие концепции я поясню позже). Приступая к работе с волокнами, прежде всего преобразуйте существующий поток в волокно. Это де-

лает функция ConvertThreadToFiber:

PV0ID ConvertThreadToFiber(PVOID pvParam);

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

определенное программистом значение; оно получает значение параметра pvParam, передаваемого в ConvertThreadToFiber;

заголовок цепочки структурной обработки исключении;

начальный и конечный адреса стека волокна; при преобразовании потока в волокно оп служит и стеком потока;

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

По умолчанию, на компьютерах с архитектурой х86 сведения о состоянии вы-

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

давать в параметре dwFlags флаг FIBER_FLAG_FLOAT_SWITCH:

PV0ID ConvertThreadToFiberEx(

PV0ID pvParam,

DWORD dwFlags);

Создав и инициализировав контекст волокна, вы сопоставляете его адрес с потоком, преобразованным в волокно, и теперь оно выполняется в этом потоке. ConvertThreadToFiber(Ex) возвращает адрес, но которому расположен контекст волокна. Этот адрес еще понадобится вам, но ни считывать, ни записывать по нему напрямую ни в коем случае нельзя — с содержимым этой структуры работают только функции, управляющие волокнами. При вызове ExitThread завершаются и волокно, и ноток.

424 Часть II. Приступаем к работе

Нет смысла преобразовывать поток в волокно, если вы не собираетесь создавать дополнительные волокна в том же потоке. Чтобы создать другое волокно, поток (выполняющий в данный момент волокно), вызывает функцию CreateFiber:

PVOID CreateFiber(

DW0RD dwStackSize,

PFIBER_START_ROUTINE pfnStartAddress,

PVOID pvParam);

Сначала она пытается создать новый стек, размер которого задан в параметре dwStackSize. Обычно передают 0, и тогда максимальный размер стека ограничивается 1 Мб, а изначально ему передается две страницы памяти. Если вы укажете ненулевое значение, то для стека будет зарезервирован и передан именно такой объем памяти. При использовании множества волокон можно сэкономить память, необходимую для хранения их стеков. Для этого вместо CreateFiber воспользуйтесь следующей функцией:

PVOID CreateFiberEx(

SIZE_T dwStackCommitSize,

SIZE_T dwStackReserveSize,

DWORD dwFlags,

PFIBER_START_ROUTINE pStartAddress,

PVOID pvParam);

Параметр dwStackCommitSize устанавливает размер памяти, изначально выделяемой для стека; параметр dwStackReserveSize позволяет зарезервировать для стека некоторое количество виртуальной памяти; параметр dwFlags принимает то же значение FIBER_FLAG_FLOAT_SWITCH, что и ConvertThreadToEberEx, до-

бавляя состояние вычислений с плавающей точкой к контексту волокна. Остальные параметры — те же, что и для функции CreateFiber.

Функция CreateFtber(Ex) создает и инициализирует новую структуру, представляющую контекст исполнения волокна. При этом пользовательское значение устанавливается по значению параметрариРанш, сохраняются начальный и конечный адреса памяти нового стека, а также адрес функции волокна (переданный в параметре pfnStartAddress).

Аргумент pfnStartAddress задает адрес функции волокна, которую вам придется реализовать самостоятельно. Эта функция должна соответствовать следующему прототипу:

VOID WINAPI FiberFunc(PVOID pvParam);

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

Глава 12. Волокна.docx 425

завершаться, пока существует волокно! Как только функция волокна завершится, поток и все созданные в нем волокна тут же будут уничтожены.

Подобно ConvertThreadToFiber(Ex), функция CreateFiber(Ex) возвращает ад-

рес контекста исполнения волокна. Но, в отличие от ConvertThreadToFiber(Ex), исполнение созданного функцией CreateBber(Ex) волокна не начинается, пока исполняется текущее волокно. Дело в том, что исполняться может только одно волокно потока одновременно. Чтобы запустить новое волокно, вызовите функцию

SwitchToFiber.

VOID SwitchToFiber(PVOID pvFiberExecutionContext);

Эта функция принимает единственный параметр (pvFiberExecutionContext) — адрес контекста волокна, полученный в предшествующем вызове ConvertThreadToFiber(Ex) или CreateFiber(Ex). По этому адресу она определяет, какому волокну предоставить процессорное время. SwitchToFiber осуществляет такие операции:

1.Сохраняет в контексте выполняемого в данный момент волокна ряд текущих регистров процессора, включая указатели команд и стека.

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

3.Связывает контекст волокна с потоком, и тот выполняет указанное волокно.

4.Восстанавливает указатель команд. Поток (волокно) продолжает выполнение с того места, на каком волокно было прервано в последний раз.

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

Для уничтожения волокна предназначена функция DeieteFiber:

VOID DeleteFiber(PVOIO pvFiberExecutionContext);

Она удаляет волокно, чей адрес контекста определяется параметром pvFiberExecutionContext, освобождает память, занятую стеком волокна, и уничтожает его контекст. Но, если вы передаете адрес волокна, связанного в данный момент с потоком, DeleteFiber сама вызывает ExitThread — в результате поток и все созданные в нем волокна «погибают».

DeleteFiber обычно вызывается волокном, чтобы удалить другое волокно. Стек удаляемого волокна уничтожается, а его контекст освобождается.

426 Часть II. Приступаем к работе

И здесь обратите внимание на разницу между волокнами и потоками: потоки, как правило, уничтожают себя сами, обращаясь к ExitThread. Использование с этой целью TerminateThread считается плохим тоном — ведь тогда система не уничтожает стек потока. Так вот, способность волокна корректно уничтожать другие волокна нам еще пригодится — как именно, я расскажу, когда мы дойдем до про- граммы-примера.

После удаления всех волокон также можно удалить их состояние из потока,

исходного вызвавшего ConvertThreadToFiber(Ex), с помощью ConvertFiberToThread, — так удастся полностью освободить память, использованную для преобразования потока в волокно.

Для хранения информации, специфичной для волокна, используется локальная память волокна (Fiber Local Storage, FLS) и функции, предназначенные для работы с ней. Эти функции выполняют те же операции, что и аналогичные функции локальной памяти потока (см. главу 6). Сначала следует вызвать FlsAlloc, чтобы выделить слот FLS, доступный всем волокнам, работающим в данном процессе. Эта функция принимает единственный параметр: указатель на функцию обратного вызова, исполняемую при уничтожении волокна либо освобождении FLS-слота вызовом FlsFree. Записать специфичные для волокна данные в FLS-слот можно вызовом функции FlsSetValue, а прочитать их оттуда — вызовом FlsGetValue. Чтобы узнать, находитесь ли вы в контексте исполнения некоторого волокна, достаточно проверить булево значение, возвращаемое функцией IsThreadAFiber.

Для удобства предусмотрено еще две функции, управляющие волокнами. В каждый момент потоком выполняется лишь одно волокно, и операционная система всегда знает, какое волокно связано сейчас с потоком. Чтобы получить адрес контекста текущего волокна, вызовите GetCurrentFiber.

PVOID GetCurrentFiber();

Другая полезная функция — GetHberData:

PV0ID GetFiberData();

Как я уже говорил, контекст каждого волокна содержит определяемое программистом значение. Оно инициализируется значением параметра pvParam, пе-

редаваемого функции ConvertThreadToFiber(Ex) или CreateFiber(Ex), и служит ар-

гументом функции волокна. GetFiberData просто «заглядывает» в контекст текущего волокна и возвращает хранящееся там значение.

Обе функции — GetCurrentEber и GetFiberData — работают очень быстро и обычно реализуются компилятором как встраиваемые (т. е. вместо вызовов этих функций он подставляет их код).

Программа-пример Counter

Эта программа, «12 Counter.exe», демонстрирует применение волокон для реализации фоновой обработки. Запустив ее, вы увидите диалоговое окно,

Глава 12. Волокна.docx 427

показанное ниже. (Настоятельно советую запустить программу Counter; тогда вам будет легче понять, что происходит в пей и как она себя ведет.)

Рис. 12-1. Окно программы Counter

Считайте эту программу сверхминиатюрной электронной таблицей, состоящей всего из двух ячеек. В первую из них можно записывать — она реализована как поле, расположенное за меткой Count To. Вторая ячейка доступна только для чтения и реализована как статический элемент управления, размещенный за меткой Answer. Изменив число в поле, вы заставите программу пересчитать значение в ячейке Answer. В этом простом примере пересчет заключается в том, что счетчик, начальное значение которого равно 0, постепенно увеличивается до максимума, заданного в ячейке Count To. Для наглядности статический элемент управления, расположенный в нижней части диалогового окна, показывает, какое из волокон — пользовательского интерфейса или расчетное — выполняется в данный момент.

Чтобы протестировать программу, введите в поле число 5 — строка Currently Running Fiber будет заменена строкой Recalculation, а значение в поле Answer постепенно возрастет с 0 до 5. Когда пересчет закончится, текущим волокном вновь станет интерфейсное, а поток заснет. Теперь введите число 50 и вновь понаблюдайте за пересчетом — на этот раз, перемещая окно по экрану. При этом вы заметите, что расчетное волокно вытесняется, а интерфейсное вновь получает процессорное время, благодаря чему программа продолжает реагировать на действия пользователя. Оставьте окно в покое, и вы увидите, что расчетное волокно снова получило управление и возобновило работу с того значения, на котором было прервано.

Остается проверить лишь одно. Давайте изменим число в поле ввода в момент пересчета. Заметьте: интерфейс отреагировал на ваши действия, но после ввода данных пересчет начинается заново. Таким образом, программа ведет себя как настоящая электронная таблица.

Обратите внимание и на то, что в программе не задействованы ни критические секции, ни другие объекты, синхронизирующие потоки, — все сделано на основе двух волокон в одном потоке.

Теперь обсудим внутреннюю реализацию программы Counter. Когда первичный поток процесса приступает к выполнению _tWinMain, вызывается функция ConvertThreadToFiber, преобразующая поток в волокно, которое впоследствии позволит нам создать другое волокно. Затем мы создаем немодальное диалоговое окно, выступающее в роли главного окна программы. Далее инициализируем переменную — индикатор состояния фоновой обработки (background processing state, BPS). Она реализована как элемент bps в

428 Часть II. Приступаем к работе

глобальной переменной g_FiberInfo. Ее возможные состояния описываются в следующей таблице.

Табл. 12-1. Варианты состояния программы Counter

Состояние

Описание

BPS_DONE

Пересчет завершен, пользователь ничего не изменял, новый пересчет не

 

нужен

BPS_STARTOVER

Пользователь внес изменения, требуется пересчет с самого начала

BPS_CONTFNUE

Пересчет еще продолжается, пользователь ничего не изменял, пересчет

 

заново не нужен

Индикатор bps проверяется внутри цикла обработки сообщений потока, который здесь сложнее обычного. Вот что делает этот цикл.

Если поступает оконное сообщение (активен пользовательский интерфейс), обрабатываем именно его. Своевременная обработка действий пользователя всегда приоритетнее пересчета.

Если пользовательский интерфейс простаивает, проверяем, не нужен ли пересчет (т. е. не присвоено ли переменной bps значение BPS_STARTOVER или

BPS_CONTINUE).

Если вычисления не нужны (BPS_DONE), приостанавливаем поток, вызывая WaitMessage, — только событие, связанное с пользовательским интерфейсом, может потребовать пересчета.

Если диалоговое окно закрывается, он останавливает расчетное волокно вызовом DeleteFiber, после чего вызывается ConvertFuberToThread для освобождения ресурсов интерфейсного волокна и возврата потока в обычный режим до завершения функции _WinMain.

Если интерфейсному волокну делать нечего, а пользователь только что изменил значение в поле ввода, начинаем вычисления заново (BPS_STARTOVER). Главное, о чем здесь надо помнить, — волокно, отвечающее за пересчет, может уже работать. Тогда это волокно следует удалить и создать новое, которое начнет все с начала. Чтобы уничтожить выполняющее пересчет волокно, интерфейсное вызывает DeleteFiber. Именно этим и удобны волокна. Удаление волокна, занятого пересчетом, — операция вполне допустимая, стек волокна и его контекст корректно уничтожаются. Если бы мы использовали потоки, а не волокна, интерфейсный поток не смог бы корректно уничтожить поток, занятый пересчетом, — нам пришлось бы задействовать какой-нибудь механизм межпоточного взаимодействия и ждать, пока поток пересчета не завершится сам. Зная, что волокна, отвечающего за пересчет, больше нет, мы вправе создать новое волокно для тех же целей, присвоив переменной bps значение BPS_CONTINUE.

Когда пользовательский интерфейс простаивает, а волокно пересчета чем-то занято, мы выделяем ему процессорное время, вызывая SwitchToFiber.

Глава 12. Волокна.docx 429

Последняя не вернет упранление, пока волокно пересчета тоже не обратится к SwilchToFiber, передан ей адрес контекста интерфейсного волокна.

FiberFunc является функцией волокна и содержит код, выполняемый волокном пересчета. Ей передается адрес глобальной структуры g_FiberInfo, и поэтому она знает описатель диалогового окна, адрес контекста интерфейсного волокна и текущее состояние индикатора фоновой обработки. Конечно, раз это глобальная переменная, то передавать eе адрес как параметр необязательно, но я решил показать, как в функцию волокна передаются параметры. Кроме того, передача адресов позволяет добиться того, чтобы код меньше зависел от конкретных переменных, — именно к этому и следует стремиться.

Первое, что делает функция волокна, — обновляет диалоговое окно, сообщая, что сейчас выполняется волокно пересчета. Далее функция получает значение, введенное в поле, и запускает цикл, считающий от 0 до указанного значения. Перед каждым приращением счетчика вызывается GetQueueStatus — эта функция проверяет, не появились ли в очереди потока новые сообщения. (Все волокна, работающие в рамках одного потока, делят его очередь сообщений.) Если сообщение появилось, значит, интерфейсному волокну есть чем заняться, и мы, считая его приоритетным по отношению к расчетному, сразу же вызываем SwitchToFiber, давая ему возможность обработать поступившее сообщение. Когда сообщение (или сообщения) будет обработано, интерфейсное волокно передаст управление волокну, отвечающему за пересчет, и фоновая обработка возобновится.

Если сообщений нет, расчетное волокно обновляет поле Answer диалогового окна и засыпает на 200 мс. В коде настоящей программы вызов Sleep надо, естественно, убрать — я поставил его, только чтобы «потянуть» время.

Когда волокно, отвечающее за пересчет, завершает свою работу, статус фоновой обработки устанавливается как BPS_DONE, и управление передается (через SxwitchToFiber) интерфейсному волокну. В этот момент ему делать нечего, и оно вызывает WaitMessage, которая приостанавливает поток, чтобы не тратить процессорное время понапрасну.

Заметьте, что каждое волокно записывает в FLS-слот строку-идентификатор («User interface» или «Computation»), которая выводится при регистрации различных событий, таких как удаление волокна или FLS-слота. Это делает функция обратного вызова, которая задается при создании FLS-слота; для проверки возможности использования FLS-слота эта функция использует функцию IsThreadAFiber.

Ч А С Т Ь I I I

УПРАВЛЕНИЕ ПАМЯТЬЮ

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