Что такое потоки windows

Ниже представлена не простая расшифровка доклада с семинара CLRium, а переработанная версия для книги .NET Platform Architecture. Той её части, что относится к потокам.

Потоки и планирование потоков

Что такое поток? Давайте дадим краткое определение. По своей сути поток это:

  • Средство параллельного относительно других потоков исполнения кода;
  • Имеющего общий доступ ко всем ресурсам процесса.

Очень часто часто слышишь такое мнение, что потоки в .NET — они какие-то абсолютно свои. И наши .NET потоки являются чем-то более облегчённым чем есть в Windows. Но на самом деле потоки в .NET являются самыми обычными потоками Windows (хоть Windows thread id и скрыто так, что сложно достать). И если Вас удивляет, почему я буду рассказывать не-.NET вещи в хабе .NET, скажу вам так: если нет понимания этого уровня, можно забыть о хорошем понимании того, как и почему именно так работает код. Почему мы должны ставить volatile, использовать Interlocked и SpinWait. Дальше обычного lock дело не уйдёт. И очень даже зря.

Давайте посмотрим из чего они состоят и как они рождаются. По сути поток — это средство эмуляции параллельного исполнения относительно других потоков. Почему эмуляция? Потому, что поток как бы странно и смело это ни звучало — это чисто программная вещь, которая идёт из операционной системы. А операционная система создаёт этот слой эмуляции для нас. Процессор при этом о потоках ничего не знает вообще.

Задача процессора — просто исполнять код. Поэтому с точки зрения процессора есть только один поток: последовательное исполнение команд. А задача операционной системы каким-либо образом менять поток т.о. чтобы эмулировать несколько потоков.

Поток в физическом понимании

«Но как же так?», — скажите вы, — «во многих магазинах и на различных сайтах я вижу запись «Intel Xeon 8 ядер 16 потоков». Говоря по-правде это — либо скудность в терминологии либо — чисто маркетинговый ход. На самом деле внутри одного большого процессора есть в данном случае 8 ядер и каждое ядро состоит из двух логических процессоров. Такое доступно при наличии в процессоре технологии Hyper-Threading, когда каждое ядро эмулирует поведение двух процессоров (но не потоков). Делается это для повышения производительности, да. Но по большому счёту если нет понимания, на каких потоках идут расчёты, можно получить очень не приятный сценарий, когда код выполняется со скоростью, ниже чем если бы расчёты шли на одном ядре. Именно поэтому раздача ядер идёт +=2 в случае Hyper-Threading. Т.е. пропуская парные ядра.

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

Возникает блокировка совместного доступа: хоть и идёт эмуляция двух ядер, на самом-то деле оно одно. Поэтому в наихудшем сценарии эти потоки исполняются по очереди, а не параллельно.

Так если процессор ничего не знает о потоках, как же достигается параллельное исполнение потоков на каждом из его ядер? Как было сказано, поток — средство операционной системы выполнять на одном процессоре несколько задач одновременно. Достигается параллелизм очень быстрым переключением между потоками в течение очень короткого промежутка времени. Последовательно запуская на выполнение код каждого из потоков и делая это достаточно часто, операционная система достигает цели: делает их исполнение псевдопараллельным, но параллельным с точки зрения восприятия человека. Второе обоснование существования потоков — это утверждение, что программа не так часто срывается в математические расчёты. Чаще всего она взаимодействует с окружающим её миром: различным оборудованием. Это и работа с жёстким диском и вывод на экран и работа с клавиатурой и мышью. Поэтому чтобы процессор не простаивал, пока оборудование сделает то, чего хочет от него программа, поток можно на это время установить в состояние блокировки: ожидания сигнала от операционной системы, что оборудование сделало то, что от него просили. Простейший пример этого — вызов метода Console.ReadKey().

Если заглянуть в диспетчер задач Windows 10, то можно заметить, что в данный момент в вашей системе существует около 1,5 тысячи потоков. И если учесть, что квант на десктопе равен 20 мс, а ядер, например, 4, то можно сделать вывод, что каждый поток получает 20 мс работы 1 раз в 7,5 сек… Ну конечно же, нет. Просто почти все потоки чего-то ждут. То ввода пользователя, то изменения ключей реестра… В операционной системе существует очень много причин, чтобы что-либо ждать.

Так что пока одни потоки в блокировке, другие — что-то делают.

Создание потоков

Простейшая функция создания потоков в пользовательском режиме операционной системы — CreateThread. Эта функция создаёт поток в текущем процессе. Вариантов параметризации CreateThread очень много и когда мы вызываем new Thread(), то из нашего .NET кода вызывается данная функция операционной системы.

В эту функцию передаются следующие атрибуты:

1) Необязательная структура с атрибутами безопасности:

  • Дескриптор безопасности (SECURITY_ATTRIBUTES) + признак наследуемости дескриптора.

    В .NET его нет, но можно создать поток через вызов функции операционной системы;

2) Необязательный размер стека:

  • Начальный размер стека, в байтах (система округляет это значение до размера страницы памяти)

    Т.к. за нас размер стека передаёт .NET, нам это делать не нужно. Это необходимо для вызовов методов и поддержки памяти.

3) Указатель на функцию — точка входа нового потоками
4) Необязательный аргумент для передачи данных функции потока.

Из того, что мы не имеем в .NET явно — это структура безопасности с атрибутами безопасности и размер стэка. Размер стэка нас мало интересует, но атрибуты безопасности нас могут заинтересовать, т.к. сталкиваемся мы с ними впервые. Сейчас мы рассмотривать их не будем. Скажу только, что они влияют на возможность изменения информации о потоке средствами операционной системы.

Если мы создаём любым способом: из .NET или же вручную, средствами ОС, мы как итог имеем и ManageThreadId и экземпляр класса Thread.

Также у этой функции есть необязательный флаг: CREATE_SUSPENDED — поток после создания не стартует. Для .NET это поведение по умолчанию.

Помимо всего прочего существует дополнительный метод CreateRemoteThread, который создаёт поток в чужом процессе. Он часто используется для мониторинга состояния чужого процесса (например программа Snoop). Этот метод создаёт в другом процессе поток и там наш поток начинает исполнение. Приложения .NET так же могут заливать свои потоки в чужие процессы, однако тут могут возникнуть проблемы. Первая и самая главная — это отсутствие в целевом потоке .NET runtime. Это значит, что ни одного метод фреймворка там не будет: только WinAPI и то, что вы написали сами. Однако, если там .NET есть, то возникает вторая проблема (которой не было раньше). Это — версия runtime. Необходимо: понять, что там запущено (для этого необходимо импортировать не-.NET методы runtime, которые написаны на C/C++ и разобраться, с чем мы имеем дело). На основании полученной информации подгрузить необходимые версии наших .NET библиотек и каким-то образом передать им управление.

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

Планирование потоков

Для того чтобы понимать, в каком порядке исполнять код различных потоков, необходима организация планирования тих потоков. Ведь система может иметь как одно ядро, так и несколько. Как иметь эмуляцию двух ядер на одном так и не иметь такой эмуляции. На каждом из ядер: железных или же эмулированных необходимо исполнять как один поток, так и несколько. В конце концов система может работать в режиме виртуализации: в облаке, в виртуальной машине, песочнице в рамках другой операционной системы. Поэтому мы в обязательном порядке рассмотрим планирование потоков Windows. Это — настолько важная часть материала по многопоточке, что без его понимания многопоточка не встанет на своё место в нашей голове никоим образом.

Итак, начнём. Организация планирования в операционной системе Windows является: гибридной. С одной стороны моделируются условия вытесняющей многозадачности, когда операционная система сама решает, когда и на основе каких условия вытеснить потоки. С другой стороны — кооперативной многозадачности, когда потоки сами решают, когда они всё сделали и можно переключаться на следующий (UMS планировщик). Режим вытесняющей многозадачности является приоритетным, т.к. решает, что будет исполняться на основе приоритетов. Почему так? Потому что у каждого потока есть свой приоритет и операционная система планирует к исполнению более приоритетные потоки. А вытесняющей потому, что если возникает более приоритетный поток, он вытесняет тот, который сейчас исполнялся. Однако во многих случаях это бы означало, что часть потоков никогда не доберется до исполнения. Поэтому в операционной системе есть много механик, позволяющих потокам, которым необходимо время на исполнение его получить несмотря на свой более низкий по сравнению с остальными, приоритет.

Уровни приоритета

Windows имеет 32 уровня приоритета (0-31)

  • 1 уровень (00 — 00) — это Zero Page Thread;
  • 15 уровней (01 — 15) — обычные динамические приоритеты;
  • 16 уровней (16 — 31) — реального времени.

Самый низкий приоритет имеет Zero Page Thread. Это — специальный поток операционной системы, который обнуляет страницы оперативной памяти, вычищая тем самым данные, которые там находились, но более не нужны, т.к. страница была освобождена. Необходимо это по одной простой причине: когда приложение освобождает память, оно может ненароком отдать кому-то чувствительные данные. Личные данные, пароли, что-то ещё. Поэтому как операционная система так и runtime языков программирования (а у нас — .NET CLR) обнуляют получаемые участки памяти. Если операционная система понимает, что заняться особо нечем: потоки либо стоят в блокировке в ожидании чего-либо либо нет потоков, которые исполняются, то она запускает самый низко приоритетный поток: поток обнуления памяти. Если она не доберется этим потоком до каких-либо участков, не страшно: их обнулят по требованию. Когда их запросят. Но если есть время, почему бы это не сделать заранее?

Продолжая говорить о том, что к нам не относится, стоит отметить приоритеты реального времени, которые когда-то давным-давно таковыми являлись, но быстро потеряли свой статус приоритетов реального времени и от этого статуса осталось лишь название. Другими словами, Real Time приоритеты на самом деле не являются таковыми. Они являются приоритетами с исключительно высоким значением приоритета. Т.е. если операционная система будет по какой-то причине повышать приоритет потока с приоритетом из динамической группы (об этом — позже, но, например, потому, что потоку освободили блокировку) и при этом значение до повышения было равно 15, то повысить приоритет операционная система не сможет: следующее значение равно 16, а оно — из диапазона реального времени. Туда повышать такими вот «твиками» нельзя.

Уровень приоритетов процессов с позиции Windows API.

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

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

Существует 6 классов приоритетов процессов. Класс приоритетов процессов — это то, относительно чего будут создаваться приоритеты потоков. Все эти классы приоритетов можно увидеть в «Диспетчере задач», при изменении приоритета какого-либо процесса.

Другими словами класс приоритета — это то, относительно чего будут задаваться приоритеты потоков внутри приложения. Чтобы задать точку отсчёта, было введено понятие базового приоритета. Базовый приоритет — это то значение, чем будет являться приоритет потока с типом приоритета Normal:

  • Если процесс создаётся с классом Normal и внутри этого процесса создаётся поток с приоритетом Normal, то его реальный приоритет Normal будет равен 8 (строка №4 в таблице);
  • Если Вы создаёте процесс и у него класс приоритета Above Normal, то базовый приоритет будет равен 10. Это значит, что потоки внутри этого процесса будут создаваться с более повышенным приоритетом: Normal будет равен 10.

Для чего это необходимо? Вы как программисты знаете модель многопоточности, которая у вас присутствует.
Потоков может быть много и вы решаете, что один поток должен быть фоновым, так как он производит вычисления и вам
не столь важно, когда данные станут доступны: важно чтобы поток завершил вычисления (например поток обхода и анализа дерева). Поэтому, вы устанавливаете пониженный приоритет данного потока. Аналогично может сложится ситуация когда необходимо запустить поток с повышенным приоритетом.

Представим, что ваше приложение запускает пользователь и он решает, что ваше приложение потребляет слишком много процессорных ресурсов. Пользователь считает, что ваше приложение не столь важное в системе, как какие-нибудь другие приложения и понижает приоритет вашего приложения до Below Normal. Это означает, что он задаёт базовый приоритет 6 относительно которого будут рассчитываться приоритеты потоков внутри вашего приложения. Но в системе общий приоритет упадёт. Как при этом меняются приоритеты потоков внутри приложения?

Таблица 3

Normal остаётся на уровне +0 относительно уровня базового приоритета процесса. Below normal — это (-1) относительно уровня базового. Т.е. в нашем примере с понижением уровня приоритета процесса до класса Below Normal приоритет потока ‘Below Normal’ пересчитается и будет не 8 - 1 = 7 (каким он был при классе Normal), а 6 - 1 = 5. Lowest (-2) станет равным 4.

Idle и Time Critical — это уровни насыщения (-15 и +15). Почему Normal — это 0 и относительно него всего два шага: -2, -1, +1 и +2? Легко провести параллель с обучением. Мы ходим в школу, получаем оценки наших знаний (5,4,3,2,1) и нам понятно, что это за оценки: 5 — молодец, 4 — хорошо, 3 — вообще не постарался, 2 — это не делал ни чего, а 1 — это то, что можно исправить потом на 4. Но если у нас вводится 10-ти бальная система оценок (или что вообще ужас — 100-бальная), то возникает неясность: что такое 9 баллов или 7? Как понять, что вам поставили 3 или 4?

Тоже самое и с приоритетами. У нас есть Normal. Дальше, относительно Normal у нас есть чуть повыше
Normal (Normal above), чуть пониже Normal (Normal below). Также есть шаг на два вверх
или на два вниз (Higest и Lowest). Нам, поверьте, нет никакой необходимости в более подробной градации. Единственное, очень редко, может раз в жизни, нам понадобится сказать: выше чем любой приоритет в системе. Тогда мы выставляем уровень Time Critical. Либо наоборот: это надо делать, когда во всей системе делать нечего. Тогда мы выставляем уровень Idle. Это значения — так называемые уровни насыщения.

Как рассчитываются уровни приоритета?

У нас бал класс приоритета процесса Normal (Таблица 3) и приоритет потоков Normal — это 8. Если процесс Above Normal то поток Normal получается равен 9. Если же процесс выставлен в Higest, то поток Normal получается равен 10.

Поскольку для планировщика потоков Windows все потоки процессов равнозначны, то:

  • Для процесса класса Normal и потока Above-Normal
  • Для процесса класса Higest и потока Normal
    конечные приоритеты будут одинаковыми и равны 10.

Если мы имеем два процесса: один с приоритетом Normal, а второй — с приоритетом Higest, но при этом
первый имел поток Higest а второй Normal, то система их приоритеты будет рассматривать как одинаковые.

Как уже обсуждалось, группа приоритетов Real-Time на самом деле не является таковой, поскольку настоящий Real-Time — это гарантированная доставка сообщения за определённое время либо обработка его получения. Т.е., другими словами, если на конкретном ядре есть такой поток, других там быть не должно. Однако это ведь не так: система может решить, что низко приоритетный поток давно не работал и дать ему время, отключив real-time. Вернее его назвать классом приоритетов который работает над обычными приоритетами и куда обычные приоритеты не могут уйти, попав под ситуации, когда Windows временно повышает им приоритет.

Но так как поток повышенным приоритетом исполняется только один на группе ядер, то получается,
что если у вас даже Real-Time потоки, не факт, что им будет выделено время.

Если перевести в графический вид, то можно заметить, что классы приоритетов пересекаются. Например, существует пересечение Above-Normal Normal Below-Normal (столбик с квадратиками):

Это значит, что для этих трех классов приоритетов процессов существуют такие приоритеты потоков внутри этих классов, что реальный приоритет будет равен. При этом, когда вы задаёте приоритет процессу вы просто повышаете или понижаете все его внутренние приоритеты потоков на определённое значение (см. Таблица 3).

Поэтому, когда процессу выдаётся более высокий класс приоритета, это повышает приоритет потоков процесса относительно обычных – с классом Normal.

Кстати говоря, мы стартовали продажи на CLRium #7, в котором мы с огромным удовольствием будем говорить про практику работы с многопоточным кодом. Будут и домашние задания и даже возможность работы с личным ментором.

Загляните к нам на сайт: мы сильно постарались, чтобы его было интересно изучить.

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

Что такое процесс в операционной системе

Процессом называют некий независимый объект, потребляющий системные ресурсы при выполнении заданной программы. То есть это исполнительная единица, служащая для запуска программы и ее исполнения.

Пример. На одной и той же вычислительной системе запускается программа по решению двух логарифмов: log25x и log(7x+9). Несмотря на то что программа одна, система запустит два разных вычислительных процесса, так как в них используются разные исходные данные.

В программировании принято выделять два типа процессов:

  • основные, используемые процессором;
  • дочерние, созданные под основным процессом.

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

К элементам процесса относят:

  • открытые файлы;
  • файлы, отображенные в памяти;
  • дескрипторы;
  • адресное пространство;
  • дочерние процессы;
  • потоки — в одном процессе их может быть несколько.

При запуске и исполнении процесса с ним можно взаимодействовать через ряд операций.

  • Создание и завершение. Эти операции для каждого процесса выполняются один раз.
  • Запуск — процесс переводится в состояние исполнения.
  • Блокировка и разблокировка. Процесс может быть заблокирован, например, в результате ожидания входных данных, которые ему недоступны в конкретный момент времени. После получения требуемой информации происходит выход временно остановленного процесса из режима ожидания.
  • Изменение приоритета. Текущий процесс может быть приостановлен ОС, так как согласно новому алгоритму, в первую очередь время центрального процессора выделяется на другую задачу.

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

Что такое поток и его особенности

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

Каждый поток состоит из трех основных элементов:

  • стек — представляет собой способ хранения данных с упорядоченными элементами, где удаление и добавление элементов выполняется по принципу «последним пришел, первым ушел»;
  • регистр процессора — определенный участок памяти внутри процессора, предназначенный для промежуточного хранения информации и имеющий длину от 8 до 32 бит;
  • счетчик команд — регистр процессора, который определяет последовательность выполнения команд.

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

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

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

Модели потоков в современных операционных системах

В зависимости от способа классификации выделяют три типа моделирования потоков:

  • по отображению в ядро;
  • по уровню реализации;
  • по виду многозадачности.

Модели отображения в ядро

  • 1:1. Такая модель считается наиболее простой, используется в Windows и Linux с ядром от 2.6 и выше. Основной принцип модели 1:1 — за управление любым потоком внутри любого процесса напрямую отвечает планировщик ядра.
  • N:M. Относится к гибридным системам, обладает высокой производительностью, но сложна в реализации. Основной принцип модели: часть пользовательских потоков (N) выполняется за счет команд из планировщика ОС, потоки режима ядра (M) управляются планировщиком потоков процесса или библиотекой потоков. Например, это может быть GNU Portable Threads — библиотека пользовательского пространства на базе POSIX/ANSI-C для UNIX-платформ.
  • N:1. Все пользовательские потоки управляются планировщиком ядра ОС. Примером такой модели могут служить волокна — облегченные пользовательские потоки, которые чаще всего применяются для упрощения портирования софта, разработанного для другой ОС.

Модели потоков в зависимости от уровня реализации

  • Поддержка потоков в режиме пользователя. В теории такая модель должна быть удобна, однако в реальности широкого распространения она не получила. В частности, данная модель реализована в GNU Portable Threads, благодаря чему обеспечивается простота переноса библиотеки с одной ОС на другую.
  • Поддержка потоков на уровне ядра. В эту группу входят все модели потоков, относящиеся к формату 1:1. Например, Native Posix Threads Library (NPTL), Win 32, Leight Weight Kernel Threads (LWKT) в DragonFlyBSD.
  • Гибридный режим. В таких моделях разработчики пытаются создать тандем из одновременной поддержки потоков в режиме пользователя и на уровне ядра. Например, гибридная схема, работающая по модели N:M, была реализована в стандарте Posix Threads в NetBSD. Однако достоинств у нее было меньше, чем недостатков, поэтому впоследствии система N:M была заменена моделью 1:1.

Проблемы многопоточности

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

  • Состояние гонки — так называют ошибку проектирования, в результате которой итоговое решение зависит от порядка реализации отдельных частей кода. Для решения проблемы можно предложить, например, использовать атомарные операции или ограничить доступ потоков к переменной так, чтобы в каждый отдельный момент времени ее мог изменять только один из потоков.
  • Взаимная блокировка — возникает в ситуации, когда параллельные потоки ожидают ресурсы, которые уже заняты «конкурентами», в результате чего ни один из них не может продолжить работу. 
  • Голодание потоков — отсутствие доступа потока к какому-либо общему ресурсу, так как он непрерывно занят другими потоками, имеющими первоочередную важность.
  • Livelock или живой замок — явление, при котором потоки непрерывно реагируют на действия друг друга, из-за чего у них не остается возможности возобновить работу.

Практическое использование потоков и процессов

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

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

Источник статьи

Автор24
— учеба по твоим правилам

Определение 1

Использование потоков в приложениях Windows — это одновременное исполнение ряда задач одним приложением.

Введение

Бывают ситуации, когда специалисту требуется одновременно исполнять несколько задач, используя одно приложение. Чтобы решить эту и другие подобные проблемы, можно использовать средства, которые дают возможность реализации многопоточности. Многопоточность может быть использована в следующих случаях:

  1. Чтобы обойти медленные процессы. При использовании лишь одного потока, приложение способно выполнить приостановку своего исполнения на тот период времени, пока им заканчивается один из медленных процессов, таких, например, как соединение с другим оборудованием через сеть. Компьютерный центральный процессор в это время входит в режим ожидания и фактически не исполняет никаких команд. При использовании многих потоков, программное приложение способно продолжить исполнение других потоков, во время ожидания одним из потоков окончания медленного процесса.
  2. Чтобы организовать поведение приложения. Благодаря применению потоков, имеется возможность организации исполнения компонентов приложения, как пожелает пользователь. К примеру, можно для всех задач приложения назначить приоритеты исполнения. То есть, задача, обладающая максимальным приоритетом, станет использовать больше процессорных ресурсов, что бывает важным при решении задач критического плана.
  3. Чтобы поддержать мультипроцессорную обработку. Когда в компьютере, на котором работает многопоточное приложение, установлено много процессоров, то имеется возможность существенно повысить скорость исполнения приложения, распределяя потоки между процессорами.

Использование потоков в приложениях Windows

Следует отметить, что не все версии Windows были способны в полной мере работать в полнопоточном режиме, даже при поддержке оборудованием такого режима. Например, Windows 95 была способна лишь имитировать режим многопоточности.

Потоком является объект операционной системы, который заключён в процесс и реализует определённую задачу. Все приложения Windows имеют как минимум один поток, именуемый основным. Любой процесс способен иметь несколько потоков.

Возможность задания многопоточности была обусловлена появлением вытесняющей многозадачности. Ранние 16-ти разрядные версии Windows применяли кооперативную многозадачность. Кооперативная многозадачность является поддержкой одновременной работы ряда приложений операционной системы, при которой операционная система получает управление от приложения. Такой тип многозадачности был достаточно малоэффективным. При зависании одного из приложений, повисала вся система.

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

«Использование потоков в приложениях Windows» 👇

Почти во всех приложениях пользователь может применять объект потока, который позволит ему использовать потоки в своих приложениях. Объекты потоков содержат в себе главные свойства и методы, требуемые для реализации многопоточных приложений.

Таким образом, поток является объектом, получающим заданное процессорное время. Все приложения Windows считаются процессами операционной системы. Все процессы имеют в своём составе по крайней мере один поток, именуемый главным. Windows не задаёт ограничений по количеству потоков для любого процесса.

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

Для использования объектов потоков в своём приложении, пользователь должен сформировать потомок класса TThread. Этот класс был спроектирован, чтобы облегчить формирование приложений, имеющих несколько потоков. Он обеспечивает совместимость при использовании библиотеки визуальных компонентов (VCL) Delphi.

При реализации приложений, имеющих много потоков, нужно выполнять следующие рекомендации:

  1. Избегать создания чрезмерно большого количества потоков, поскольку это ведёт к перегрузке операционной системы и процессора. Рекомендуется создавать для однопроцессорной системы не более шестнадцати активных потоков в одном процессе.
  2. Использовать синхронизацию в ситуации, когда сразу несколько потоков стремятся обеспечить себе доступ к одному и тому же ресурсу.
  3. Почти все методы, которые выполняют обращение к объектам VCL и меняют содержание формы, обязаны получать вызов из главного потока или применять объект синхронизации.

Если пользователю необходимо записать код инициализации для нового объекта потока, ему следует выполнить добавление нового конструктора в описание нового класса потока, а уже далее пользователь может прибавить код инициализации вместо кода реализации класса. Здесь же пользователь может задать требуемый приоритет для этого потока и необходимые действия по завершении его работы.

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

Приоритет всех потоков состоит из следующих компонентов:

  1. Класс приоритета, то есть приоритет процесса, который породил поток.
  2. Относительный приоритет, то есть приоритет самого потока.

Класс приоритета процесса может быть одним их четырёх значений, а именно, Idle, Normal, High и Realtime, отображаемых числами от четырёх до двадцати четырёх. Всем приложениям по умолчанию присваивается приоритет Normal.

Чтобы определить текущий и установить требуемый класс приоритета применяются соответственно функции Get Prioriry Class и Set Priority Class.

Written on . Posted in Win32

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

Процессы

Процессом обычно называют экземпляр выполняемой программы.

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

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

— Адресное пространство — диапазон адресов виртуальной памяти, которым может пользоваться процесс;

— Исполняемая программа и данные, проецируемые на виртуальное адресное пространство процесса.

Потоки

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

— Уникальный идентификатор потока;

— Содержимое набора регистров процессора, отражающих состояние процессора;

— Два стека, один из которых используется потоком при выполнении в режиме ядра, а другой — в пользовательском режиме;

— Закрытую область памяти, называемую локальной памятью потока (thread local storage, TLS) и используемую подсистемами, run-time библиотеками и DLL.

Планирование потоков

Чтобы все потоки работали, операционная система отводит каждому из них определенное процессорное время. Тем самым создается иллюзия одновременного выполнения потоков (разумеется, для многопроцессорных компьютеров возможен истинный параллелизм). В Windows реализована система вытесняющего планирования на основе приоритетов, в которой всегда выполняется поток с наибольшим приоритетом, готовый к выполнению. Выбранный для выполнения поток работает в течение некоторого периода, называемого квантом. Квант определяет, сколько времени будет выполняться поток, пока операционная система не прервет его. По окончании кванта операционная система проверяет, готов ли к выполнению другой поток с таким же (или большим) уровнем приоритета. Если таких потоков не оказалось, текущему потоку выделяется еще один квант. Однако поток может не полностью использовать свой квант. Как только другой поток с более высоким приоритетом готов к выполнению, текущий поток вытесняется, даже если его квант еще не истек.

Квант не измеряется в каких бы то ни было единицах времени, а выражается целым числом. Для каждого потока хранится текущее значение его кванта. Когда потоку выделяется квант процессорного времени, это значит, что его квант устанавливается в начальное значение. Оно зависит от операционной системы. Например, для Win2000 Professional начальное значение кванта равно 6, а для Win2000 Server — 36.

Это значение можно изменить вызвав Control Panel — > System -> Advanced -> Performance options. Значение «Applications» — как для Win2000 Professional; «Background Services» — как для Win2000 Server.
Или напрямую в ключе реестра HKLM Win32PrioritySeparation.

Всякий раз, когда возникает прерывание от таймера, из кванта потока вычитается 3, и так до тех пор, пока он не достигнет нуля. Частота срабатывания таймера зависит от аппаратной платформы. Например, для большинства однопроцессорных x86 систем он составляет 10мс, а на большинстве многопроцессорных x86 систем — 15мс.

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

Планирование в Windows осуществляется на уровне потоков, а не процессов. Это кажется понятным, так как сами процессы не выполняются, а лишь предоставляют ресурсы и контекст для выполнения потоков. Поэтому при планировании потоков, система не обращает внимания на то, какому процессу они принадлежат. Например, если процесс А имеет 10 готовых к выполнению потоков, а процесс Б — два, и все 12 потоков имеют одинаковый приоритет, каждый из потоков получит 1/12 процессорного времени.

Приоритеты

В Windows существует 32 уровня приоритета, от 0 до 31. Они группируются так: 31 — 16 уровни реального времени; 15 — 1 динамические уровни; 0 — системный уровень, зарезервированный для потока обнуления страниц (zero-page thread).

При создании процесса, ему назначается один из шести классов приоритетов:

Real time class (значение 24),

High class (значение 13),

Above normal class (значение 10),

Normal class (значение 8),

Below normal class (значение 6),

и Idle class (значение 4).

В Windows NT/2000/XP можно посмотреть приоритет процесса в Task Manager.

Above normal и Below normal появились начиная с Win2000.

Приоритет каждого потока (базовый приоритет потока) складывается из приоритета его процесса и относительного приоритета самого потока. Есть семь относительных приоритетов потоков:

Normal: такой же как и у процесса;

Above normal: +1 к приоритету процесса;

Below normal: -1;

Highest: +2;

Lowest: -2;

Time critical: устанавливает базовый приоритет потока для Real time класса в 31, для остальных классов в 15.

Idle: устанавливает базовый приоритет потока для Real time класса в 16, для остальных классов в 1.

В следующей таблице показаны приоритеты процесса, относительный и базовый приоритеты потока.

Приоритет потока

Класс процесса

Класс процесса

   

Idle class

Below normal class

Normal class

Above normal class

High class

Real time class

1

 

Idle

Idle

Idle

Idle

Idle

 

2

 

Lowest

         

3

 

Below …

         

4

Idle class

Normal

Lowest

       

5

 

Above …

Below …

       

6

Below normal class

Highest

Normal

Lowest

     

7

   

Above …

Below …

     

8

Normal class

 

Highest

Normal

Lowest

   

9

     

Above …

Below …

   

10

Above normal class

   

Highest

Normal

   

11

       

Above …

Lowest

 

12

       

Highest

Below …

 

13

High class

       

Normal

 

14

         

Above …

 

15

         

Highest

 

15

 

Time critical

Time critical

Time critical

Time critical

Time critical

 

16

           

Idle

17

             

18

             

19

             

20

             

21

             

22

           

Lowest

23

           

Below …

24

Real time class

         

Normal

25

           

Above …

26

           

Highest

27

             

28

             

29

             

30

             

31

           

Time critical

Привязка к процессорам

Если операционная система выполняется на машине, где установлено более одного процессора, то по умолчанию, поток выполняется на любом доступном процессоре. Однако в некоторых случаях, набор процессоров, на которых поток может работать, может быть ограничен. Это явление называется привязкой к процессорам (processor affinity). Можно изменить привязку к процессорам программно, через Win32-функции планирования.

Память

Каждому процессу в Win32 доступно линейное 4-гигабайтное (2^32 = 4 294 967 296) виртуальное адресное пространство. Обычно верхняя половина этого пространства резервируется за операционной системой, а вторая половина доступна процессу.

Виртуальное адресное пространство процесса доступно всем потокам этого процесса. Иными словами, все потоки одного процесса выполняются в едином адресном пространстве.

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

Виртуальная память может вовсе не соответствовать структуре физической памяти. Диспетчер памяти транслирует виртуальные адреса на физические, по которым реально хранятся данные. Поскольку далеко не всякий компьютер в состоянии выделить по 4 Гбайт физической памяти на каждый процесс, используется механизм подкачки (swapping). Когда оперативной памяти не хватает, операционная система перемещает часть содержимого памяти на диск, в файл (swap file или page file), освобождая, таким образом, физическую память для других процессов. Когда поток обращается к странице виртуальной памяти, записанной на диск, диспетчер виртуальной памяти загружает эту информацию с диска обратно в память.

Создание процессов

Создание Win32 процесса осуществляется вызовом одной из таких функций, как CreateProcess, CreateProcessAsUser (для Win NT/2000) и CreateProcessWithLogonW (начиная с Win2000) и происходит в несколько этапов:

— Открывается файл образа (EXE), который будет выполняться в процессе. Если исполняемый файл не является Win32 приложением, то ищется образ поддержки (support image) для запуска этой программы. Например, если исполняется файл с расширением .bat, запускается cmd.exe и т.п.

В WinNT/2000 для отладки программ реализовано следующее. CreateProcess, найдя исполняемый Win32 файл, ищет в SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Option раздел с именем и расширением запускаемого файла, затем ищет в нем параметр Debugger, и если строка не пуста, запускает то, что в ней написано вместо данной программы.

— Создается объект Win32 «процесс».

— Создается первичный поток (стек, контекст и объект «поток»).

— Подсистема Win32 уведомляется о создании нового процесса и потока.

— Начинается выполнение первичного потока.

— В контексте нового процесса и потока инициализируется адресное пространство (например, загружаются требуемые DLL) и начинается выполнение программы.

Завершение процессов

Процесс завершается если:

— Входная функция первичного потока возвратила управление.

— Один из потоков процесса вызвал функцию ExitProcess.

— Поток другого процесса вызвал функцию TerminateProcess.

Когда процесс завершается, все User- и GDI-объекты, созданные процессом, уничтожаются, объекты ядра закрываются (если их не использует другой процесс), адресное пространство процесса уничтожается.

Пример 1. Программа создает процесс «Калькулятор».


#include <windows.h>
int main(int argc, char* argv[])
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
if( !CreateProcess( NULL, "c:/windows/calc.exe", NULL, NULL, FALSE,
0, NULL, NULL, &si, &pi))

return 0;
// Close process and thread handles.
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return 0;
}

Создание потоков

Первичный поток создается автоматически при создании процесса. Остальные потоки создаются функциями CreateThread и CreateRemoteThread (только в Win NT/2000/XP).

Завершение потоков

Поток завершается если

— Функция потока возвращает управление.

— Поток самоуничтожается, вызвав ExitThread.

— Другой поток данного или стороннего процесса вызывает TerminateThread.

— Завершается процесс, содержащий данный поток.

Если вы используете в своем приложении LibCMt C run-time библиотеку, Microsoft настоятельно рекомендует вместо Win32 API функций использовать их аналоги из C run-time: _beginthread, _beginthreadex, _endthread и _endthreadex.

Объекты ядра

Эти объекты используются системой и пользовательскими приложениями для управления множеством самых разных ресурсов: процессами, потоками, файлами и т.д. Windows позволяет создавать и оперировать с несколькими типами таких объектов, в том числе:

Kernel object

Объект ядра

Kernel object

Объект ядра

Access token

Маркер доступа

Module

Подгружаемый модуль (DLL)

Change notification

Уведомление об изменениях на диске

Mutex

Мьютекс

I/O completion ports

Порт завершения ввода-вывода

Pipe

Канал

Event

Событие

Process

Процесс

File

Файл

Semaphore

Семафор

File mapping

Проекция файла

Socket

Сокет

Heap

Куча

Thread

Поток

Job

Задание

Timer

Ожидаемый таймер

Mailslot

Почтовый слот

   

Объект ядра это, по сути, структура, созданная ядром и доступная только ему. В пользовательское приложение передается только описатель (handle) объекта, а управлять объектом ядра можно с помощью функций Win32 API.

Wait функции

Как можно приостановить работу потока? Существует много способов. Вот некоторые из них.

Функция Sleep() приостанавливает работу потока на заданное число миллисекунд. Если в качестве аргумента вы укажите 0 ms, то произойдет следующее. Поток откажется от своего кванта процессорного времени, однако тут же появится в списке потоков готовых к выполнению. Иными словами произойдет намеренное переключение потоков. (Вернее сказать, попытка переключения. Ведь следующим для выполнения потоком вполне может стать тот же самый.)

Функция WaitForSingleObject() приостанавливает выполнение потока до тех пор, пока не произойдет одно из двух событий:

— истечет таймаут ожидания;

— ожидаемый объект перейдет в сигнальное (signaled) состояние.

По возвращаемому значению можно понять, какое из двух событий произошло. Ожидать с помощью wait-функций можно большинство объектов ядра, например, объект «процесс» или «поток», чтобы определить, когда они завершат свою работу.

Функции WaitForMultipleObjects передается сразу массив объектов. Можно ожидать срабатывания сразу всех объектов или какого-то одного из них.

Пример 2. Программа создает два одинаковых потока и ожидает их завершения.
Потоки просто выводят текстовое сообщение, которое передано им при инициализации.


#include <windows.h>
#include <process.h>

unsigned __stdcall ThreadFunc( void * arg) // Функция потока
{
char ** str = (char**)arg;
MessageBox(0,str[0],str[1],0);
_endthreadex( 0 );
return 0;
};
int main(int argc, char* argv[])
{
char * InitStr1[2] = {"First thread running!","11111"};// строка для первого потока
char * InitStr2[2] = {"Second thread running!","22222"};// строка для второго потока
unsigned uThreadIDs[2];

HANDLE hThreads[2];
hThreads[0] = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, InitStr1, 0,&uThreadIDs[0]);
hThreads[1] = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, InitStr2, 0,&uThreadIDs[1]);

// Ждем, пока потоки не завершат свою работу
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE ); // Set no time-out
// Закрываем дескрипторы
CloseHandle( hThreads[0] );
CloseHandle( hThreads[1] );
return 0;
}

Синхронизация потоков

Работая параллельно, потоки совместно используют адресное пространство процесса. Также все они имеют доступ к описателям (handles) открытых в процессе объектов. А что делать, если несколько потоков одновременно обращаются к одному ресурсу или необходимо как-то упорядочить работу потоков? Для этого используют объекты синхронизации и соответствующие механизмы.

Мьютексы

Мьютексы (Mutex) это объекты ядра, которые создаются функцией CreateMutex(). Мьютекс бывает в двух состояниях — занятом и свободном. Мьютексом хорошо защищать единичный ресурс от одновременного обращения к нему разными потоками.

Пример 3. Допустим, в программе используется ресурс, например, файл или буфер в памяти. Функция WriteToBuffer() вызывается из разных потоков. Чтобы избежать коллизий при одновременном обращении к буферу из разных потоков, используем мьютекс. Прежде чем обратиться к буферу, ожидаем <освобождения> мютекса.


HANDLE hMutex;

int main()
{
hMutex = CreateMutex( NULL, FALSE, NULL); // Создаем мьютекс в свободном состоянии
...
// Создание потоков, и т.д.
...
}
BOOL WriteToBuffer()
{
DWORD dwWaitResult;
// Ждем освобождения мьютекса перед тем как обратиться к буферу.
dwWaitResult = WaitForSingleObject( hMutex, 5000L); // 5 секунд на таймаут
 
if (dwWaitResult == WAIT_TIMEOUT) // Таймаут. Мьютекс за это время не освободился.
{
return FALSE;
}
else // Мьютекс освободился, и наш поток его занял. Можно работать.
{
Write_to_the_buffer().
...
ReleaseMutex(hMutex); // Освобождаем мьютекс.
}
return TRUE;
}

Семафоры

Семафор (Semaphore) создается функцией CreateSemaphore(). Он очень похож на мьютекс, только в отличие от него у семафора есть счетчик. Семафор открыт если счетчик больше 0 и закрыт, если счетчик равен 0. Семафором обычно «огораживают» наборы равнозначных ресурсов (элементов), например очередь, список и т.п.

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


class CMyQueue
{
HANDLE m_hSemaphore; // Семафор для учета элементов очереди
// Описание других объектов для хранения элементов очереди

public:
CMyQueue()
{
m_hSemaphore = CreateSemaphore(NULL, 0, 1000, NULL); //начальное значение счетчика = 0

//максимальное значение = 1000


// Инициализация других объектов
}
~CMyQueue()
{
CloseHandle( m_hSemaphore);
// Удаление других объектов
}
void AddItem(void * NewItem)
{
// Добавляем элемент в очередь
// Увеличиваем счетчик семафора на 1.
ReleaseSemaphore(m_hSemaphore,1, NULL);
}
void GetItem(void * Item)
{
// Если очередь пуста, то потоки, вызвавшие этот метод,
// будут находиться в ожидании...

WaitForSingleObject(m_hSemaphore,INFINITE);
 
// Удаляем элемент из очереди
}
};

Замечание. В этом примере мы считаем, что сами процедуры добавления элемента в очередь и удаления из очереди безопасны с точки зрения многопоточности. Не будем пока касаться деталей их реализации. Подробнее мы рассмотрим это в примере 9.

События

События (Event), также как и мьютексы имеют два состояния — установленное и сброшенное. События бывают со сбросом вручную и с автосбросом. Когда поток дождался (wait-функция вернула управление) события с автосбросом, такое событие автоматически сбрасывается. В противном случае событие нужно сбрасывать вручную, вызвав функцию ResetEvent(). Допустим, сразу несколько потоков ожидают одного и того же события, и событие сработало. Если это было событие с автосбросом, то оно позволит работать только одному потоку (ведь сразу же после возврата из его wait-функции событие сбросится автоматически!), а остальные потоки останутся ждать. Если же это было событие со сбросом вручную, то все потоки получат управление, а событие так и останется в установленном состоянии, пока какой-нибудь поток не вызовет ResetEvent().

Пример 5. Вот еще один пример многопоточного приложения. Программа имеет два потока; один готовит данные, а второй отсылает их на сервер. Разумно распараллелить их работу. Здесь потоки должны работать по очереди. Сначала первый поток готовит порцию данных. Потом второй поток отправляет ее, а первый тем временем готовит следующую порцию и т.д. Для такой синхронизации понадобится два event’а с автосбросом.


unsigned __stdcall CaptureThreadFunc( void * arg) // Поток, готовящий данные
{
while (bSomeCondition)
{
WaitForSingleObject(m_hEventForCaptureTh,INFINITE); // Ждем своего события
... // Готовим данные
SetEvent(hEventForTransmitTh); // Разрешаем работать второму потоку
}
_endthreadex( 0 );
return 0;
};

unsigned __stdcall TransmitThreadFunc( void * arg) // Поток, отсылающий данные.
{
while (bSomeCondition)
{
WaitForSingleObject(m_hEventForTransmitTh,INFINITE); // Ждем своего события
... // Данные готовы, формируем из них пакет для отправки
SetEvent(hEventForCaptureTh); // Разрешаем работать первому потоку, а сами...
... // отправляем пакет
}
_endthreadex( 0 );
return 0;
};

int main(int argc, char* argv[]) // Основной поток
{
// Создаем два события с автосбросом, со сброшенным начальным состоянием
hEventForCaptureTh = CreateEvent(NULL,FALSE,FALSE,NULL);
hEventForTransmitTh = CreateEvent(NULL,FALSE,FALSE,NULL);

// Создаем потоки
hCaptureTh = (HANDLE)_beginthreadex( NULL, 0, &CaptureThreadFunc, 0, 0,&uTh1);
hTransmitTh = (HANDLE)_beginthreadex( NULL, 0, &TransmitThreadFunc, 0, 0,&uTh2);
// Запускаем первый поток
SetEvent(hEventForCaptureTh);

....
}

Пример 6. Другой пример. Программа непрерывно в цикле производит какие-то вычисления. Нужно иметь возможность приостановить на время ее работу. Допустим, это просмотрщик видео файлов, который в цикле, кадр за кадром отображает информацию на экран. Не будем вдаваться в подробности видео функций. Реализуем функции Pause и Play для программы. Используем событие со сбросом вручную.


// Главная функция потока, которая в цикле отображает кадры
unsigned __stdcall VideoThreadFunc( void * arg)
{
while (bSomeCondition)
{
WaitForSingleObject(m_hPauseEvent,INFINITE); // Если событие сброшено, ждем
... // Отображаем очередной кадр на экран
}
_endthreadex( 0 );
return 0;
};

void Play()
{
SetEvent(m_hPauseEvent);
};

void Pause()
{
ResetEvent(m_hPauseEvent);
};

Функция PulseEvent() устанавливает событие и тут же переводит его обратно в сброшенное состояние; ее вызов равнозначен последовательному вызову SetEvent() и ResetEvent(). Если PulseEvent вызывается для события со сбросом в ручную, то все потоки, ожидающие этот объект, получают управление. При вызове PulseEvent для события с автосбросом пробуждается только один из ждущих потоков. А если ни один из потоков не ждет объект-событие, вызов функции не дает никакого эффекта.

Пример 7. Реализуем функцию NextFrame() для предыдущего примера для промотки файла вручную по кадрам.


void NextFrame()
{
PulseEvent(m_hPauseEvent);
};

Ожидаемые таймеры

Пожалуй, ожидаемые таймеры — самый изощренный объект ядра для синхронизации. Появились они, начиная с Windows 98. Таймеры создаются функцией CreateWaitableTimer и бывают, также как и события, с автосбросом и без него. Затем таймер надо настроить функцией SetWaitableTimer. Таймер переходит в сигнальное состояние, когда истекает его таймаут. Отменить «тиканье» таймера можно функцией CancelWaitableTimer. Примечательно, что можно указать callback функцию при установке таймера. Она будет выполняться, когда срабатывает таймер.

Пример 8. Напишем программу-будильник используя WaitableTimer’ы. Будильник будет срабатыват раз в день в 8 утра и «пикать» 10 раз. Используем для этого два таймера, один из которых с callback-функцией.


#include <process.h>
#include <windows.h>
#include <stdio.h>
#include <conio.h>

#define HOUR (8) // время, когда срабатывает будильник (только часы)
#define RINGS (10) // сколько раз пикать

HANDLE hTerminateEvent ;

// callback функция таймера
VOID CALLBACK TimerAPCProc(LPVOID, DWORD, DWORD)
{
Beep(1000,500); // звоним!
};
 
// функция потока
unsigned __stdcall ThreadFunc(void *)
{
HANDLE hDayTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE hAlarmTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE h[2]; // мы будем ждать эти объекты
h[0] = hTerminateEvent; h[1] = hDayTimer;
int iRingCount=0; // число "звонков"
int iFlag;
DWORD dw;

// немного помучаемся со временем,
//т.к. таймер принимает его только в формате FILETIME

LARGE_INTEGER liDueTime, liAllDay;
liDueTime.QuadPart=0;
// сутки в 100-наносекундных интервалах = 10000000 * 60 * 60 * 24 = 0xC92A69C000
liAllDay.QuadPart = 0xC9;
liAllDay.QuadPart=liAllDay.QuadPart << 32;
liAllDay.QuadPart |= 0x2A69C000;
SYSTEMTIME st;
GetLocalTime(&st); // узнаем текущую дату/время
iFlag = st.wHour > HOUR; // если назначенный час еще не наступил,
//то ставим будильник на сегодня, иначе - на завтра

st.wHour = HOUR;
st.wMinute = 0;
st.wSecond =0;
FILETIME ft;
SystemTimeToFileTime( &st, &ft);
if (iFlag)
((LARGE_INTEGER *)&ft)->QuadPart =
((LARGE_INTEGER *)&ft)->QuadPart +liAllDay.QuadPart ;

LocalFileTimeToFileTime(&ft,&ft);
// Устанавливаем таймер,


// он будет срабатывать раз в сутки (24*60*60*1000ms),
// начиная со следующего "часа пик" - HOUR

SetWaitableTimer(hDayTimer, (LARGE_INTEGER *) &ft, 24*60*60000, 0, 0, 0);
do {
dw = WaitForMultipleObjectsEx(2,h,FALSE,INFINITE,TRUE);
if (dw == WAIT_OBJECT_0 +1) // сработал hDayTimer
{
// Устанавливаем таймер, он будет вызывать callback ф-ию раз в секунду,
// начнет с текущего момента

SetWaitableTimer(hAlarmTimer, &liDueTime, 1000, TimerAPCProc, NULL, 0);
iRingCount=0;
}
if (dw == WAIT_IO_COMPLETION) // закончила работать callback ф-ия
{
iRingCount++;
if (iRingCount==RINGS)
CancelWaitableTimer(hAlarmTimer);
}
}while (dw!= WAIT_OBJECT_0); // пока не сработало hTerminateEvent крутимся в цикле

// закрывае handles, выходим


CancelWaitableTimer(hDayTimer);
CancelWaitableTimer(hAlarmTimer);
CloseHandle(hDayTimer);
CloseHandle(hAlarmTimer);
_endthreadex( 0 );
return 0;
};

int main(int argc, char* argv[])
{
// это событие показывае потоку когда надо завершаться
hTerminateEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
unsigned uThreadID;
HANDLE hThread;
// создаем поток
hThread = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, 0, 0,&uThreadID);
puts("Press any key to exit.");
// ждем any key от пользователя для завершения программы
getch();
// выставляем событие
SetEvent(hTerminateEvent);
// ждем завершения потока
WaitForSingleObject(hThread, INFINITE );
// закрываем handle
CloseHandle( hThread );
return 0;
}

Критические секции. Синхронизация в пользовательском режиме

Критическая секция гарантирует вам, что куски кода программы, огороженные ей, не будут выполняться одновременно. Строго говоря, критическая секция не является объектом ядра. Она представляет собой структуру, содержащую несколько флагов и какой-то (не важно) объект ядра. При входе в критическую секцию сначала проверяются флаги, и если выясняется, что она уже занята другим потоком, то выполняется обычная wait-функция. Критическая секция примечательна тем, что для проверки, занята она или нет, программа не переходит в режим ядра (не выполняется wait-функция) а лишь проверяются флаги. Из-за этого считается, что синхронизация с помощью критических секций наиболее быстрая. Такую синхронизацию называют «синхронизация в пользовательском режиме«.

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


typedef ... ItemData;

// Элемент очереди: данные и два указателя на предыдущий и следующий элементы
typedef struct _ItemStruct
{
ItemData data;
struct _ItemStruct * prev,*next;
} Item;

// Описание класса "Очередь"
class CMyQueue
{
CRITICAL_SECTION m_crisec; // Критическая секция
Item * m_Begin; // Указатель на первый элемент
Item * m_End; // Указатель на последний элемент
int m_Count; // Количество элементов


public:
CMyQueue()
{
// Инициализируем критическую секцию
InitializeCriticalSection(&m_crisec);
// Инициализируем переменные
m_Count = 0;
m_Begin = m_End = NULL;
}

~CMyQueue()
{
// Удаляем все элементы очереди
while(m_Count) GetItem();
// Удаляем критическую секцию
DeleteCriticalSection(&m_crisec);
}

void AddItem(ItemData data)
{
Item * NewItem;
Item * OldFirstItem;
NewItem = new Item(); // New item
NewItem->next = NULL;
NewItem->prev = NULL;
NewItem->data = data;
// ------------------------ Этот кусок не может выполняться параллельно
EnterCriticalSection(&m_crisec); // Заходим в к.с. (ждем входа)
OldFirstItem = m_Begin;
m_Begin = NewItem;
NewItem->next = OldFirstItem;
if (OldFirstItem)
OldFirstItem->prev = NewItem;
else
m_End = NewItem;
m_Count++;
LeaveCriticalSection(&m_crisec); // Выходим из к.с.
// ------------------------ Этот кусок не может выполняться параллельно
}

ItemData GetItem()
{
ItemData data;
// ------------------------ Этот кусок не может выполняться параллельно
EnterCriticalSection(&m_crisec); // Заходим в к.с. (ждем входа)
if (!m_End)
data = NULL;
else
{
data = m_End->data ;
if (m_End->prev )
{
m_End->prev ->next = NULL;
m_End = m_End->prev ;
}
else
{
m_End = NULL;
m_Begin = NULL;
}
m_Count --;
}
LeaveCriticalSection(&m_crisec); // Выходим из к.с.
// ------------------------ Этот кусок не может выполняться параллельно
return data;
};
};

Синхронизация процессов

Описатели объектов ядра зависимы от конкретного процесса (process specific). Проще говоря, handle объекта, полученный в одном процессе, не имеет смысла в другом. Однако существуют способы работы с одними и теми же объектами ядра из разных процессов.

Во-первых, это наследование описателя. При создании объекта можно указать будет ли его описатель наследоваться дочерними (порожденными этим процессом) процессами.

Во-вторых, дублирование описателя. Функция DuplicateHandle дублирует описатель объекта одного процесса в другой, т.е. по сути, берет запись в таблице описателей одного процесса и создает ее копию в таблице другого.

И, наконец, именование объекта ядра. При создании объекта ядра для синхронизации (мьютекса, семафора, ожидаемого таймера или события) можно задать его имя. Оно должно быть уникальным в системе. Тогда другой процесс может открыть этот объект ядра, указав в функции Open…(OpenMutex, OpenSemaphore, OpenWaitableTimer, OpenEvent) это имя.

На самом деле, при вызове функции Create… () система сначала проверяет, не существует ли уже объект ядра с таким именем. Если нет, то создается новый объект. Если да, ядро проверяет тип этого объекта и права доступа. Если типы не совпадают или же вызывающий процесс не имеет полных прав на доступ к объекту, вызов Create… функции заканчивается неудачно и возвращается NULL. Если все нормально, то просто создается новый описатель (handle) существующего уже объекта ядра. По коду возврата функции GetLastError() можно понять что произошло: создался новый объект или Create() вернула уже существующий.

Поэтому, синхронизировать потоки внутри разных процессов можно точно также как и в пределах одного. Нужно только правильно передать описатель синхронизирующего объекта от одного процесса к другому любым из перечисленных выше способов.

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


int main(int argc, char* argv[])
{
HANDLE Mutex;
Mutex = CreateMutex(NULL,FALSE,"MyMutex");
if (ERROR_ALREADY_EXISTS == GetLastError()) // Такой мьютекс уже кем-то создан...
{
MessageBox(0,"Приложение уже запущено","Error",0);
CloseHandle( Mutex);
exit(0);
}
...
}

Взаимодействие между процессами

Потоки одного процесса не имеют доступа к адресному пространству другого процесса. Однако существуют механизмы для передачи данных между процессами.

Разделяемая память

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

Такие объекты называются проекциями файлов (на оперативную память) (file-mapping object). Для создания проекции файла сначала вызывается функция CreateFileMapping(). Ей передается дескриптор (уже открытого) файла или указывается, что нужно использовать page file операционной системы. Кроме этого, в параметрах ей передается флаг защиты, максимальный размер проекции и имя объекта. Затем вызывается функция MapViewOfFile(). Она отображает представление файла (view of a file) в адресное пространство процесса. По окончании работы вызывается функция UnmapViewOfFile(). Она освобождает память и записывает данные в файл (если это не файл подкачки). Чтобы записать данные на диск немедленно, используется функция FlushViewOfFile(). Проекция файла, как и другие объекты ядра, может использоваться другими процессами через наследование, дублирование дескриптора или по имени.

Пример 11. Вот пример программы, которая создает проекцию в page file и записывает в нее данные.


#include <windows.h>

void main()
{
HANDLE hMapping;
char* lpData;
char* lpBuffer;
...
//Создание или открытие существующей проекции файла
hMapping = CreateFileMapping( (HANDLE)(-1), // используем page file
NULL, PAGE_READWRITE, 0, 0x0100, "MyShare");
if (hMapping == NULL) exit(0);
// Размещаем проекцию hMapping в адресном пространстве нашего процесса;
// lpData получает адрес размещения
lpData = (char*) MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS,0,0,0);
if (lpData == NULL) exit(0);
// Копируем в проекцию данные
memcpy ( lpData , lpBuffer );
...
// Заканчиваем работу. Освобождаем память.
UnmapViewOfFile(lpData);
// Закрываем объект ядра
CloseHandle(hMapping);
};

Прочие механизмы (сокеты, pipe)

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

Каналы используются для пересылки данных в одном направлении между дочерним и родительским процессами или между двумя дочерними процессами. Операции чтения/записи в канал похожи на подобные операции при работе с файлами.

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

Сокет — это абстрактный объект для обозначения одного из концов сетевого соединения, в том числе и через Internet. Сокеты Windows бывают двух типов: сокеты дейтаграмм и сокеты потоков. Интерфейс Windows Sockets (WinSock) основан на BSD-версии сокетов, но в нем имеются также расширения, специфические для Windows.

Сообщения в Windows (оконные сообщения)

Говоря о Windows нельзя не упомянуть о таких понятиях как windows (окна), messages (сообщения), message queue (очередь сообщений) и т.д.

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

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

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

Если описатели объектов ядра процессо-зависимы, то описатели окон уникальны в пределах Deskop. Поэтому одному процессу не составляет никакого труда получить и использовать описатель окна принадлежащему потоку другого процесса.

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

Пример 12. Программа находит окно с заголовком “Калькулятор” и закрывает его, посылая сообщение WM_CLOSE.


#include <windows.h>

int main(int argc, char* argv[])
{
HWND hwnd = FindWindow( NULL , "Калькулятор");
if (NULL != hwnd) PostMessage(hwnd, WM_CLOSE, 0, 0 );
return 0;
}

Тема оконных сообщений в Windows весьма обширна и требует для рассмотрения, по крайней мере, отдельной статьи.

Заключение

В этой статье коротко были рассмотрены основные моменты многозадачной работы в Windows: создание и завершение процессов, синхронизация и межпроцессное общение.

Статья ни в коем случае не претендует на полноту и предназначена дать лишь общие сведения или указать направление для поиска нужной информации в огромной документации. Чтобы узнать более подробно о приведенных в статье объектах и функциях, читайте первоисточники:

— Microsoft Platform SDK

— Jeffrey Richter. Programming Applications for Microsoft® Windows. ISBN 1-57231-996-8

— Соломон, Руссинович. Внутреннее устройство MS Windows 2000. ISBN 5-7502-0136-8

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Какой офис выбрать для windows 11
  • Редактор видеофильмов windows movie maker
  • Расшаренные папки windows server 2012 r2
  • Программа task host windows что это
  • Windows on arm 4pda