- Что такое секция windows
- Структура окна папки
- Файловая система персонального компьютера
- Операции с файлами и папками
- Работа с буфером обмена
- Операционная система Windows. Что это такое?
- Что такое Windows
- Как узнать операционную систему компьютера
- Эволюция синхронизации в Windows и C++
- Критическая секция
- Мьютекс
- Событие
- Тонкая блокировка, синхронизирующая доступ потоков-«читателей» и потоков-«писателей»
- Условная переменная
- Блокирующая очередь
Что такое секция windows
Современный Windows — это операционная система, управляющая работой персонального компьютера. Windows имеет удобный графический пользовательский интерфейс. В отличие от старой операционной системы DOS с текстовым интерфейсом, Windows не требует знания команд операционной системы и их точного ввода с клавиатуры. Подавляющее большинство операций по управлению работой персонального компьютера выполняются манипулятором мышь над графическими объектами Windows, либо короткими комбинациями клавиш (горячими клавишами) на клавиатуре.
Пользовательский интерфейс – это методы и средства взаимодействия человека с аппаратными и программными средствами компьютера.
Стартовый экран Windows представляет собой системный объект, называемый рабочим столом.
Рабочий стол — это графическая среда, на которой отображаются объекты и элементы управления Windows. На рабочем столе можно видеть значки (пиктограммы), ярлыки и панель задач (основной элемент управления). При запуске Windows на рабочем столе присутствуют, как минимум , три значка: Мой компьютер, Сетевое окружение, Корзина. На рабочем столе могут быть расположены и другие значки. Его можно использовать и как временное хранилище своих файлов, но по окончании работы в учебном классе они должны быть либо удалены, либо перемещены в собственные папки.
Значки являются графическим изображением объектов и позволяют управлять ими. Значок — это графическое представление объекта в свернутом виде, соответствующее папке, программе, документу, сетевому устройству или компьютеру. Значки, как правило имеют метки — надписи, которые располагаются под ними. Щелчок левой кнопкой мыши по значку позволяет выделить его, а двойной щелчок – открыть (запустить) соответствующее этому значку приложение.
Ярлык является указателем на объект. Ярлык – это специальный файл, в котором содержится ссылка на представленный им объект (информация о месте расположения объекта на жестком диске). Двойной щелчок мыши по ярлыку позволяет запустить (открыть) представляемый им объект. При его удалении сам объект не стирается, в отличие от удаления значка. Достоинство ярлыков в том, что они обеспечивают быстрый доступ к объекту из любой папки, не расходуя на это памяти. Отличить ярлык от значка можно по маленькой стрелке в левом нижнем углу пиктограммы.
Панель задач является инструментом для переключения между открытыми папками или приложениями. В левой части панели задач расположена кнопка «Пуск»; в правой — панель индикации. На самой панели изображаются все открытые в данный момент объекты.
Кнопка «Пуск» открывает Главное меню. С его помощью можно запустить все программы, зарегистрированные в операционной системе, получить доступ ко всем средствам настройки операционной системы, к поисковой и справочной системам и другим функциям.
Центральным понятием Windows является окно. Окно – структурный и управляющий элемент пользовательского интерфейса, представляющий собой ограниченную рамкой прямоугольную область экрана, в которой может отображаться приложение, документ или сообщение.
Выше на рисунке показан рабочий стол Windows с открытым Главным меню, окном текстового процессора Word, значками и ярлыками и некоторыми свернутыми на панели задач документами.
Из других понятий Windows следует отметить понятия каталога и папки.
Каталог – поименованная группа файлов, объединенных по какому-либо признаку.
Папка – понятие, которое используется в Windows вместо понятия каталог в более ранних операционных системах. Понятие папка имеет расширенное толкование, так как наряду с обычными каталогами папки представляют и такие объекты, как Мой компьютер, Проводник, Принтер, Модем и др.
Структура окна папки
Типовое окно папки показано на рисунке.
Окно содержит следующие обязательные элементы.
- Строка заголовка — в ней написано название папки. Служит для перетаскивания окна.
- Системный значок. Открывает служебное меню, с помощью которого можно управлять размером и расположением окна.
- Кнопки управления размером: разворачивающая (восстанавливающая), сворачивающая, закрывающая.
- Строка меню (ниспадающее меню). Гарантированно предоставляет доступ ко всем командам данного окна.
- Панель инструментов. Содержит командные кнопки для выполнения наиболее часто встречающихся операций. Часто пользователь может сам настраивать эту панель размещая на ней необходимые кнопки.
- Адресная строка. В ней указан путь доступа к текущей папке. Позволяет быстро перейти к другим разделам файловой структуры.
- Рабочая область. Отображает значки объектов, хранящихся в папке, причем способом отображения можно управлять.
- Полосы прокрутки – позволяют прокручивать содержимое окна в горизонтальном или вертикальном направлении если информация не умещается в окне.
- Строка состояния. Выводит дополнительную информацию об объектах в окне.
Файловая система персонального компьютера
Файловая система обеспечивает хранение и доступ к файлам на диске. Принцип организации файловой системы — табличный. Поверхность диска рассматривается как трехмерная матрица, измерениями которой являются номера поверхности, цилиндра и сектора. Под цилиндром подразумевается совокупность всех дорожек, принадлежащих разным поверхностям и равноудаленных от оси вращения. Данные о том, в каком месте записан тот или иной файл, хранятся в системной области диска в специальной таблице размещения файлов (FAT-таблица). FAT-таблица хранится в двух экземплярах, идентичность которых контролируется операционной системой.
ОС MS-DOS, OS/2, Windows-95/NT реализуют 16-разрядные поля в FAT-таблицах. Такая система называлась FAT-16. Такая система позволяет разместить не более 65536 записей о местоположении единиц хранения данных. Наименьшей единицей хранения данных является сектор. Размер сектора равен 512 байтам. Группы секторов условно объединяют в кластеры, которые являются наименьшей единицей адресации к данным. Размер кластера зависит от емкости диска: в Fat-16 для дисков от 1 до 2 Гбайт 1 кластер занимает 64 сектора или 32 Кбайта. Это нерационально, поскольку даже маленький файл занимает 1 кластер. У больших файлов, занимающих несколько кластеров, в конце образуется незаполненный кластер. Поэтому потери емкости для дисков в системе FAT-16 могут быть очень велики. С дисками свыше 2,1 Гбайт FAT-16 вообще не работает.
В Windows 98 и старших версиях реализована более совершенная файловая система — FAT-32 с 32-разрядными полями в таблице размещения файлов. Она обеспечивает маленький размер кластера для дисков большой емкости. Например, для диска до 8 Гбайт 1 кластер занимает 8 секторов (4 Кбайта).
Файл — это именованная последовательность байтов произвольной длины. До появления Windows-95 общепринятой схемой именования файлов была схема 8.3 (короткое имя) – 8 символов собственно имя файла, 3 символа – расширение его имени. Недостаток коротких имен — их низкая содержательность. Начиная с Windows-95 введено понятие длинного имени (до 256 символов). Оно может содержать любые символы, кроме девяти специальных: \ / : * ? » |.
Расширением имени считаются все символы после последней точки. В современных операционных ситемах расширение имени несет для системы важную информацию о типе файла. Типы файлов регистрируются и связывают файл с открывающей его программой. Например файл MyText.doc будет открыт текстовым процессором Word, поскольку расширение .doc обычно связывается именно с этим приложением. Обычно, если файл не связан ни с какой открывающей программой, то на его значке обозначен флаг — логотип Microsoft Windows, а открывающую программу пользователь может указать сам, выбрав ее из предоставленного ОС списка.
Логически структура файлов организована по иерархическому принципу: папки более низких уровней вкладываются в папки более высоких уровней. Верхним уровнем вложенности является корневой каталог диска. Термины «папка» и «каталог» равнозначны. Каждому каталогу файлов на диске соответствует одноименная папка операционной системы. Однако, понятие папки несколько шире. Так в Windows-95 существуют специальные папки, осуществляющие удобный доступ к программам, но которым не соответствует ни один каталог диска.
Атрибуты файлов — это параметры, определяющие некоторые свойства файлов. Для получения доступа к атрибутам файла, следует щелкнуть правой кнопкой мыши по его значку и выбрать меню Свойства. Основных атрибутов 4: «Только для чтения», «Скрытый», «Системный», Архивный». Атрибут «Только для чтения» предполагает, что файл не предназначен для внесения изменений. Атрибут «Скрытый» говорит о том, что данный файл не следует отображать на экране при проведении файловых операций. Атрибутом «Системный» помечаются наиболее важные файлы ОС (как правило они имеют и атрибут «Скрытый»). Атрибут «Архивный» связан с резервным копированием файлов и особого значения не имеет.
Операции с файлами и папками
Копирование и перемещение
1 способ. Разместить на рабочем столе два окна: источник и приемник копирования. Выделить в окне-источнике необходимые значки. Несколько значков выделяются при нажатой клавише Ctrl. Перетащить мышью выделенные значки в окно-приемник, указав на любой из выделенных значков. При одновременно нажатой клавише Ctrl происходит копирование, без нее — перемещение элементов (при условии, что папки находятся на одном диске).
2 способ. Выделить копируемые элементы. Выбрать меню Правка/Копировать (Вырезать). При выборе «Вырезать» произойдет перемещение. Открыть папку-приемник. Выбрать меню Правка/Вставить.
Удаление файлов и папок
Удаление файлов выполняется выбором элементов и нажатием клавиши Delete. При этом отмеченные элементы перемещаются в специальную папку — Корзину. При очистке корзины происходит уничтожение файлов. Существует еще операция стирания файлов, когда специальными служебными программами кластеры, в которых содержались стираемые файлы, заполняются случайными данными.
Групповые операции с файлами
Если требуется выполнить операцию копирования или удаления с большим количеством файлов одновременно, то выделять их удерживая Ctrl не очень удобно. Можно выделить целую группу подряд идущих значков, щелкнув по первому их них и при нажатой клавише Shift — по последнему. Однако, в этом случае требуется определенным образом упорядочить значки. Для этого следует открыть папку с файлами и обратиться к меню Вид/Упорядочить значки. Существует 4 способа упорядочивания значков в папке: по имени, по типу, по размеру, по дате. Например, необходимо скопировать все файлы с расширением .txt. В этом случае следует упорядочить значки по типу, после чего все файлы типа .txt будут сгруппированы вместе и использовать клавишу Shift для их выделения. Аналогичный прием применяется для выделения «старых» файлов (упорядочение по дате), «маленьких» (упорядочение по размеру) и в других стандартных ситуациях.
Если в окне не показана полная информация о файлах (расширение, объем и дата создания), следует обратиться к меню окна папки Вид/Таблица и в окне будут выведены все характеристики файлов.
Переименование файлов и папок.
Переименование файла или папки выполняется либо через меню Переименовать, вызываемого щелчком правой кнопки мыши на соответствующем значке, либо щелчком по имени выделенного значка.
Замечание. Удаление или переименование невозможно, если указанный файл уже открыт каким-либо приложением.
Работа с буфером обмена
ОС Windows создает и обслуживает специальную область памяти, называемую буфером обмена. Буфер обмена служит для обмена данными между приложениями Windows. Описанный выше второй способ копирования предполагает использование буфера обмена.
В меню Правка для операций с буфером обмена используются пункты Копировать, Вырезать и Вставить. Первые два помещают в буфер обмена объект, последний — копирует из буфера обмена. Если объект (часть текста, рисунок и т.д.) не выделен, то первые два пункта будут не активны. Если буфер обмена пуст, то не будет активен и третий пункт.
Операции с буфером обмена выполняются очень часто, поэтому на панель инструментов окна помещаются кнопки быстрого доступа.
Самый быстрый способ работы с буфером обмена — использование комбинаций клавиш: Ctrl+C — копировать; Ctrl+X — вырезать; Ctrl + V — вставить.
Операционная система Windows. Что это такое?
Что такое Windows
Windows (виндоуз) — это название операционной системы компьютера. У нее есть несколько версий, самые популярные из которых это XP, 7, 10.
Операционная система или сокращенно ОС — это самая важная программа компьютера. Без нее мы даже не смогли бы его включить. При помощи ОС мы управляем компьютером, то есть делаем за ним все то, что делаем – работаем, отдыхаем, пользуемся интернетом.
Когда мы включаем компьютер, то видим картинку и всякие значки, кнопки, окошки и прочее. Вся эта красота, которую вы видите и используете, возможна только благодаря операционной системе. Если бы не было ОС, то при включении был бы только черный экран с английскими буквами и цифрами.
Windows – это одна из разновидностей операционных систем. Так сказать, ее марка. Как, например, марка автомобиля – Ауди, Фольксваген, БМВ.
У систем компьютера есть свои «марки»: Linux, macOS и другие. Из них Windows — самая популярная в мире. Она установлена на большинстве компьютеров.
Существует несколько версий этой ОС:
- Устаревшие и малораспространенные: 95, 98, 2000, Me, XP, Vista и другие;
- Популярные: 7, 8, 10.
Между собой они отличаются датой выпуска. Сейчас самой новой версией является Windows 10. Но многие по-прежнему пользуются XP и 7. Эти ОС используют меньше ресурсов и подходят для старых и маломощных компьютеров.
Н а самом деле, не так важно, какая версия на вашем компьютере, ноутбуке. Все они друг на друга похожи – принцип работы один и тот же. Но самой стабильной на данный момент является Windows 10.
Как узнать операционную систему компьютера
Проще всего узнать версию операционной системы по Рабочему столу – той картинке и значкам, которые появляются при включении компьютера. Они отличаются на разных версиях.
Еще посмотреть какая у вас ОС можно через значок «Этот компьютер» или «Мой компьютер». Для этого щелкните по нему правой кнопкой мыши и из списка выберите пункт «Свойства».
Появится окно с указанием системы
Или откройте «Пуск», нажмите правой кнопкой мыши по надписи «Компьютер» и выберите «Свойства».
Эволюция синхронизации в Windows и C++
Посетителей: 1699 | Просмотров: 3433 (сегодня 0)
Когда я впервые занялся написанием ПО с параллельной обработкой, в C++ не было поддержки синхронизации. В самой Windows был лишь набор синхронизирующих примитивов, и все они были реализованы в ядре. В основном я использовал критические секции, если не возникало потребности в синхронизации между процессами, и тогда я применял мьютекс. В общих чертах, эти синхронизирующие примитивы были блокировками, или блокирующими объектами (lock objects).
Термин «мьютекс» (mutex) взят из концепции взаимоисключения (mutual exclusion); это еще одно название синхронизации. Мьютекс гарантирует, что в данный момент только один поток может получить доступ к некоему ресурсу. Название «критическая секция» связано с тем, что к такому ресурсу может обращаться только определенная секция кода. Чтобы обеспечить корректность, единовременно эту критическую секцию кода может выполнять только один поток. Эти два блокирующих объекта имеют разные возможности, но полезно помнить, что это всего лишь блокировки, оба гарантируют взаимоисключение и оба могут быть использованы для демаркации критических секций кода.
Сегодня ландшафт синхронизации изменился кардинально. Программисту на C++ предоставляется обширный выбор. Теперь Windows поддерживает намного больше синхронизирующих функций, а сам C++ наконец-то обладает интересным набором средств параллельной обработки и синхронизации для тех, кто использует компилятор, поддерживающий стандарт C++11.
В этой статье я намерен исследовать состояние синхронизации в Windows и C++ на данный момент. Начну с обзора синхронизирующих примитивов, предоставляемых самой Windows, а затем рассмотрю альтернативы, предлагаемые Standard C++ Library. Если вам важна портируемость, тогда новые библиотечные дополнения C++ будут для вас очень привлекательны. Однако, если портируемость для вас не столь значима и на первом месте стоит производительность, в таком случае гораздо важнее изучение того, что предлагается теперь операционной системой Windows. Давайте приступим прямо сейчас.
Критическая секция
Первым у нас будет объект «критическая секция». Эта блокировка интенсивно используется в бесчисленном множестве приложений, но имеет плохую репутацию. Когда я впервые начал использовать критические секции, они были по-настоящему простыми. Чтобы создать такую блокировку, достаточно было создать структуру CRITICAL_SECTION и вызвать функцию InitializeCriticalSection, чтобы подготовить эту структуру к использованию. Эта функция не возвращала никаких значений, и тем самым подразумевалось, что она не может завершиться неудачей. Однако в те дни этой функции требовалось создавать различные системные ресурсы, в том числе объект события режима ядра, и существовала вероятность, что в ситуациях с острой нехваткой памяти это не удастся, что приведет к появлению структурного исключения (structured exception). Тем не менее, эта вероятность была весьма мала, и большинство разработчиков игнорировало ее.
С распространением COM использование критических секций резко расширилось, потому что во многих COM-классах критические секции применялись для синхронизации, но в большинстве случаев конкуренции либо не было, либо она была очень низка. С появлением многоядерных процессоров внутреннее событие критической секции стало использоваться гораздо реже, так как критическая секция короткое время «крутилась» в цикле в пользовательском режиме, ожидая захвата блокировки. Малое число кручений в цикле означало, что во многих случаях кратковременной конкуренции можно было бы избавиться от перехода в режим ядра, что значительно повысило бы производительность.
Примерно в то же время некоторые разработчики ядра осознали, что они могли бы радикально улучшить масштабируемость Windows, если бы у них была возможность откладывать создание объектов событий критической секции до тех пор, пока их присутствие не будет оправдано нарастанием конкуренции. Идея казалась неплохой, пока эти разработчики не поняли, что, хотя InitializeCriticalSection теперь вряд ли могла бы завершиться неудачей, функция EnterCriticalSection (используемая для ожидания захвата блокировки) больше не является надежной. Игнорировать это было нельзя, потому что иначе появилось бы множество сбойных ситуаций, которые практически не позволили бы критическим секциям работать корректно, а это разрушило бы бесчисленную массу приложений. Тем не менее, преимущества масштабируемости были достаточно очевидны.
В конце концов, один из разработчиков ядра пришел к решению в форме нового, недокументированного объекта события режима ядра, который был назван событием с ключом (keyed event). Вы можете почитать о нем в книге «Windows Internals» за авторством Марка Руссиновича (Mark Russinovich), Дэвида Соломона (David Solomon) и Алекса Ионеску (Alex Ionescu) (Microsoft Press, 2012), но, если в двух словах, вместо захвата объекта события для каждой критической секции можно было бы использовать единственное событие с ключом для всех критических секций в системе. Это работало, потому что объект события с ключом был именно таков: он полагается на ключ, который является просто идентификатором с размером указателя, естественным образом локальным для адресного пространства.
Конечно, было искушение обновить критические секции так, чтобы они использовали только события с ключом, но из-за того, что многие отладчики и другие инструменты опирались на внутреннее устройство критических секций, к событию с ключом прибегали только как к последнему убежищу, если ядру не удалось создать обычный объект события.
Это может показаться историей, полной противоречий, но в течение цикла разработки Windows Vista работа событий с ключом была значительно улучшена, и это привело к введению совершенно нового блокирующего объекта, который был и проще, и быстрее, но об этом я расскажу немного позже.
Так как объект критической секции теперь защищен от сбоя из-за нехватки памяти, он стал прямолинеен в использовании. Простая оболочка показана на рис. 1.
Рис. 1. Блокировка «критическая секция»
Уже упоминавшаяся функция EnterCriticalSection дополнена функцией TryEnterCriticalSection, которая является неблокирующей альтернативой. Функция LeaveCriticalSection освобождает блокировку, а DeleteCriticalSection — любые ресурсы ядра, которые могли быть попутно выделены.
Поэтому критическая секция является вполне разумным выбором. Она работает весьма хорошо, так как старается избегать переходов в режим ядра и выделения ресурсов. Тем не менее, ей приходится сохранять немалый груз, связанный с историей ее развития и необходимый для совместимости с приложениями.
Мьютекс
Мьютекс является истинным синхронизирующим объектом режима ядра. В отличие от критических секций блокировка «мьютекс» всегда использует ресурсы, выделяемые ядром. Его преимущество, конечно же, заключается в том, что ядро в состоянии обеспечить синхронизацию между процессами благодаря своему знанию этой блокировки. Как объект ядра он предоставляет обычные атрибуты (в частности, имя), которые можно использовать для открытия этого объекта из других процессов или просто для идентификации блокировки в отладчике. Кроме того, вы можете указывать маску доступа для ограничения доступа к этому объекту. В качестве внутрипроцессной блокировки данный объект избыточен, несколько сложнее в использовании и намного медленнее. На рис. 2 приведена простая оболочка для безымянного мьютекса, который является локальным для процесса.
Рис. 2. Блокировка «мьютекс»
Функция CreateMutex создает блокировку, а общая функция CloseHandle закрывает описатель процесса, что в конечном счете уменьшает счетчик ссылок блокировки в ядре. Ожидание захвата блокировки осуществляется универсальной функцией WaitForSingleObject, которая проверяет (и при необходимости позволяет ожидать) переход в свободное состояние (signaled state) множества объектов ядра. Ее второй параметр указывает, сколько времени должен блокироваться вызвавший поток, ожидая захвата блокировки. Константа INFINITE (что не удивительно) означает бесконечное ожидание, тогда как нулевое значение вообще исключает ожидание потока, и захват блокировки будет происходить, только если она свободна. Наконец, функция ReleaseMutex освобождает блокировку.
Блокировка «мьютекс» обладает широкими возможностями, но за них приходиться расплачиваться производительностью и усложнением. Оболочка на рис. 2 утыкана выражениями проверки (assertions), указывающими возможные сбойные ситуации, но в большинстве случаев блокировка «мьютекс» отметается прежде всего по соображениям производительности.
Событие
Прежде чем рассказывать о высокопроизводительной блокировке, мне нужно ознакомить вас еще с одним синхронизирующим объектом ядра, который уже был упомянут. Хотя на деле это не блокировка (в том плане, что он не предоставляет механизма для прямой реализации взаимоисключения), объект события крайне важен для координации работы между потоками. По сути, это тот же объект, который на внутреннем уровне используется критической секцией, и, помимо этого, он полезен для эффективной и масштабируемой реализации всех видов шаблонов параллельной обработки.
Функция CreateEvent создает событие, а функция CloseHandle, как и в случае мьютекса, освобождает этот объект в ядре. Поскольку на самом деле это не блокировка, у этого объекта нет семантики захвата и освобождения. Скорее это именно то воплощение функциональности перевода в свободное/занятое состояние, которое предоставляется многими объектами ядра. Чтобы разобраться, как работает эта функциональность, нужно понять, что объект событие может быть создан в одном из двух состояний. Если вы передаете TRUE во второй параметр CreateEvent, то получаете объект события, который называют событием со сбросом вручную (manual-reset event); в ином случае создается событие с автоматическим сбросом (auto-reset event). Событие со сбросом вручную требует от вас самостоятельно устанавливать и сбрасывать состояние объекта (свободен/занят). Для этой цели предназначены функции SetEvent и ResetEvent. Событие с автоматическим сбросом само сбрасывается в исходное состояние (переходит из свободного [signaled] в занятое состояние [nonsignaled]) при освобождении ожидающего потока. Поэтому такое событие полезно, когда один из потоков должен координировать свои действия с еще одним потоком, тогда как событие со сбросом вручную удобно, когда один из потоков должен координировать свои действия с любым количеством других потоков. Вызов SetEvent для события с автоматическим сбросом освободит максимум один поток, а в случае события со сбросом вручную этот вызов освободит все ожидающие потоки. Подобно мьютексу ожидание перехода события в свободное состояние осуществляется через функцию WaitForSingleObject. На рис. 3 приведена простая оболочка безымянного события, которое может быть сконструировано в любом из двух режимов.
Рис. 3. Событие
Тонкая блокировка, синхронизирующая доступ потоков-«читателей» и потоков-«писателей»
Тонкая блокировка, синхронизирующая доступ потоков-«читателей» и потоков-«писателей» (Slim Reader/Writer, SRW), может оказаться труднопроизносимым термином, но ключевое слово здесь — «тонкая». Программисты могут недооценивать эту блокировку из-за ее способности различать общих «читателей» (shared readers) и монопольных «писателей» (exclusive writers), полагая, что это перебор, когда им достаточно критической секции. Однако на деле это простейшая в работе блокировка и на данный момент самая быстрая; при этом для ее использования вовсе не обязательно иметь общих «читателей». Свою репутацию в быстродействии она заслужила не только потому, что полагается на эффективный объект события с ключом, но и потому, что она в основном реализована в пользовательском режиме и переключается в режим ядра лишь при достижении такого уровня конкуренции, что потоку лучше «уснуть». И вновь объекты «критическая секция» и «мьютекс» предоставляют дополнительную функциональность, которая может вам потребоваться, например поддержку рекурсии и блокировок между процессами, но гораздо чаще достаточно быстрой и облегченной блокировки для внутреннего использования.
Эта блокировка опирается на уже упомянутые события с ключом и, как таковая, является чрезвычайно облегченной и несмотря на это предоставляет большой объем функциональности. Блокировка SRW требует памяти размером с указатель, которая выделяется вызывающим процессом, а не ядром. По этой причине инициализирующая функция InitializeSRWLock не может завершиться неудачей и просто обеспечивает, чтобы эта блокировка содержала подходящий битовый шаблон перед последующим использованием.
Ожидание захвата блокировки достигается использованием либо функции AcquireSRWLockExclusive для так называемой блокировки «писатель», либо функции AcquireSRWLockShared для блокировки «читатели». Однако термины «монопольный» и «общий» подходят больше. Для обоих режимов (монопольный/общий) имеются соответствующие функции освобождения и попытки захвата. На рис. 4 показана простая оболочка для блокировки SRW монопольного режима. Вам не составит труда самостоятельно добавить функции общего режима, если в том возникнет нужда. Но заметьте, что деструктора нет, так как нет ресурсов, которые нужно было бы освобождать.
Рис. 4. Блокировка SRW
Условная переменная
Последний синхронизирующий объект, с которым мне нужно ознакомить вас, — условная переменная (condition variable). Вероятно, это один из тех синхронизирующих объектов, которые неизвестны большинству программистов. Однако в последние месяцы я заметил возрождение интереса к условным переменным. Скорее всего это как-то связано с появлением C++11, но идея не нова и поддержка этой концепции в Windows существует уже некоторое время. Действительно Microsoft .NET Framework поддерживает шаблон условной переменной с момента первого выпуска, правда, эта поддержка включена в класс Monitor, что в некоторых отношениях ограничивает ее полезность. Но возрождение интереса к этому объекту также вызвано тем, что события с ключом позволили ввести условные переменные в Windows Vista, и с тех пор они только совершенствовались. Хотя условная переменная — это просто шаблон параллельной обработки и в связи с этим может быть реализована на основе других примитивов, ее включение в ОС означает, что она способна работать чрезвычайно быстро и освободить программиста от необходимости самому обеспечивать корректность такого кода. Действительно, если вы применяете синхронизирующие примитивы ОС, то почти невозможно гарантировать корректность некоторых шаблонов параллельной обработки без помощи самой ОС.
Если задуматься, шаблон условной переменной довольно распространен. Программе нужно ждать выполнения некоего условия, прежде чем она сможет продолжить работу. Оценка этого условия включает захват блокировки для проверки какого-либо общего состояния. Однако, если условие еще не выполнено, блокировка должна быть освобождена, чтобы другой поток мог выполнить условие. Далее оценивающий поток должен ждать до тех пор, пока условие не будет выполнено, и только потом снова захватывать блокировку. Как только блокировка захвачена повторно, необходимо снова проверить условие, чтобы избежать усиления конкуренции. Реализовать это труднее, чем кажется, поскольку здесь, по сути, требуется позаботиться о других подводных камнях, а реализовать эффективно — еще сложнее. Следующий псевдокод иллюстрирует эту проблему:
Но даже в этом псевдокоде скрыта трудноуловимая ошибка. Чтобы функция работала корректно, ожидание на условии должно осуществляться до выхода из блокировки, но в этом случае блокировка никогда не будет освобождена. Возможность атомарного освобождения одного объекта и ожидания на другом настолько важна, что в Windows предусмотрена функция SignalObjectAndWait, которая именно это и делает для определенных объектов ядра. Но, поскольку блокировка SRW, в основном находится в пользовательском режиме, требуется какое-то другое решение. И здесь мы приходим к условным переменным.
Как и блокировка SRW, условная переменная занимает единственный участок памяти размером с указатель и инициализируется функцией InitializeConditionVariable, которая никогда не завершается неудачей. По аналогии с SRW освобождение каких-либо ресурсов не требуется, поэтому, когда условная переменная больше не нужна, память может быть просто возвращена системе.
Поскольку само условие специфично для конкретной программы, написание шаблона в виде цикла while с единственным вызовом функции SleepConditionVariableSRW возлагается на вызывающую сторону. Эта функция автоматически освобождает блокировку SRW, ожидая пробуждения на момент выполнения условия. Также имеется соответствующая функция SleepConditionVariableCS, если вы хотите использовать условные переменные с критической секцией.
Функция WakeConditionVariable вызывается для пробуждения одного ждущего, или спящего, потока. Пробудившийся поток снова захватит блокировку перед возвратом управления. В качестве альтернативы можно использовать функцию WakeAllConditionVariable для пробуждения всех ждущих потоков. На рис. 5 показана простая оболочка с необходимым циклом while. Заметьте, что существует возможность непредсказуемого пробуждения спящего потока, и цикл while гарантирует, что условие всегда будет заново проверяться после повторного захвата блокировки потоком. Также важно отметить, что предикат всегда оценивается при удерживании блокировки.
Рис. 5. Условная переменная
Блокирующая очередь
Для дальнейших пояснений я воспользуюсь блокирующей очередью (blocking queue) как примером. Позвольте подчеркнуть, что я в принципе не советую применять блокирующие очереди. Гораздо эффективнее использовать порт завершения ввода-вывода (I/O completion port) или пул потоков Windows, который является просто абстракцией этого порта, или даже класс concurrent_queue в Concurrency Runtime. В целом, предпочтительнее любые неблокирующие средства. Тем не менее, блокирующая очередь — простая в понимании концепция, и слишком многие разработчики считают ее полезной. Согласен, не в каждой программе требуется масштабирование, но каждая программа должна работать корректно. Блокирующая очередь предоставляет широкие возможности в применении синхронизации как для обеспечения корректности, так и для совершенно неправильного ее использования.
Рассмотрим реализацию блокирующей очереди на основе какой-нибудь блокировки и события. Блокировка защищает общую очередь, а событие уведомляет потребителя о том, что источник что-то поместил в очередь. На рис. 6 дан простой пример с использованием события с автоматическим сбросом. Я задействовал этот режим события потому, что метод push ставит в очередь только один элемент и я хочу, чтобы при этом пробуждался лишь один потребитель для его извлечения из очереди. Метод push захватывает блокировку, ставит элемент в очередь, а затем переводит событие в свободное состояние, чтобы пробудить любого из ждущих потребителей. Метод pop захватывает блокировку, а затем ждет, пока в очереди что-то не появится, прежде чем извлечь элемент и вернуть его. Оба метода используют класс lock_block. Для краткости я не включил его в этот код, но он просто вызывает в конструкторе метод enter блокировки, а в деструкторе — метод exit.
Рис. 6. Блокирующая очередь с автоматическим сбросом
Но обратите внимание на весьма вероятную взаимоблокировку (deadlock) из-за того, что вызовы exit и wait не являются атомарными. Если бы блокировка была мьютексом, я мог бы использовать функцию SignalObjectAndWait, но тогда сильно пострадала бы производительность блокирующей очереди.
Другой вариант — применение события со сбросом вручную. Вместо перехода в свободное состояние всякий раз, когда в очередь ставится какой-либо элемент, просто определяем два состояния. Событие может находиться в свободном состоянии, пока в очереди есть элементы, и в занятом состоянии, если она пуста. Этот вариант будет работать гораздо лучше, так как потребует меньше вызовов ядра для перевода события в свободное состояние. Пример показан на рис. 7. Обратите внимание на то, как метод push устанавливает событие, если в очереди есть один элемент. Благодаря этому мы избегаем лишних вызовов функции SetEvent. Метод pop послушно очищает событие, если обнаруживает, что очередь пуста. Пока в очереди имеется несколько элементов, любое количество потребителей может извлекать элементы из очереди без участия объекта события, а это улучшает масштабируемость.
Рис. 7. Блокирующая очередь со сбросом вручную
В этом случае вероятности потенциальной взаимоблокировки в последовательности exit-wait-enter нет, так как другой потребитель не сможет украсть событие, учитывая, что это событие со сбросом вручную. Этот вариант гораздо производительнее. Тем не менее, альтернативное решение (и, возможно, более естественное) — использовать условную переменную вместо события. Это легко делается с помощью класса condition_variable на рис. 5 и подобно блокирующей очереди со сбросом вручную, но немного проще. Пример представлен на рис. 8. Заметьте, насколько четче стала семантика и ваши намерения в параллельной обработке благодаря применению более высокоуровневых синхронизирующих объектов. Эта четкость помогает избегать ошибок параллельной обработки, которым подвержен более туманный код.
Рис. 8. Блокирующая очередь с условной переменной
Наконец, я должен упомянуть, что C++11 теперь предоставляет блокировку mutex, а также condition_variable. Мьютекс из C++11 не имеет ничего общего с мьютексом из Windows. То же самое относится и к condition_variable. Это хорошие новости в плане портируемости. Их можно использовать где угодно, где есть подходящий компилятор C++. С другой стороны, реализация C++11 в Visual C++ 2012 работает заметно хуже по сравнению с Windows-блокировкой SRW и условной переменной. На рис. 9 показан пример блокирующей очереди, реализованной на основе типов из Standard C++11 Library.
Рис. 9. Блокирующая очередь, реализованная средствами C++11
Реализация на основе Standard C++ Library, несомненно, будет со временем усовершенствована, как и поддержка этой библиотекой параллельной обработки в целом. Комитет по C++ предпринял некоторые шаги довольно консервативного характера в направлении поддержки параллельной обработки, но работа пока не закончена. Как я обсуждал в последних трех статьях, будущее параллельной обработки в C++ все еще находится под вопросом. На данный момент для создания компактных и масштабируемых программ с параллельной обработкой лучше всего сочетать некоторые превосходные синхронизирующие примитивы Windows и средства современного компилятора C++.