Green-sell.info

Новые технологии
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

C многопоточное программирование

Многопоточное программирование

1. Введение

К счастью, все может быть совсем не так — выбор за вами!

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

Не знаю стоит ли отдельно упоминать, но статья развивает излагавшиеся в предыдущем материале Интерфейсы и Сообщения базовые принципы, так что если для вас это является новостью, то имеет смысл предварительно ознакомиться.

С уважением, Сергей Деревяго.

2. Многопоточное программирование

К сожалению, с точки зрения абсолютного большинства программистов и авторов (а порою и весьма уважаемых и компетентных в своей области авторов) MT — это все то же самое обычное программирование, разве что щедро усыпанное невообразимым количеством mutex-ов «для корректности». В типичной настольной книжке вам приведут стандартный пример с двумя потоками (т.е. thread-ами), одновременно изменяющими значение одной и той же переменной, лихо вылечат ошибку посредством пресловутого mutex-а, а заодно и растолкуют что такое deadlock и почему много mutex-ов тоже плохо.

М-да. «И куды крестьянину податься?»

К счастью, не все так плохо: в природе встречаются авторы действительно понимающие MT. Авторы, стоявшие у истоков Стандарта POSIX Threads и умеющие доходчиво объяснять суть дела. Самой важной, классической книгой по теме, для нас безусловно является David R. Butenhof «Programming with POSIX Threads» — вряд ли вы в полной мере постигнете C/C++ MT не прочитав данной книги. Ну а классический FAQ по теме — это сообщения в ньюсгруппе comp.programming.threads того же автора. Скажем, если вам не вполне понятно использование некоторой функции и/или термина, то один из наиболее простых и эффективных способов — это поиск сообщений автора Butenhof с заданным ключевым словом, например попробуйте: priority inversion group:comp.programming.threads author:Butenhof.

А теперь к делу. Итак, что же такое правильное MT приложение? Правильное MT приложение — это прежде всего правильный дизайн! Вы никогда не сможете превратить серьезное однопоточное приложение в хорошее многопоточное приложение, сколько бы mutex-ов вы в него не добавили! Butenhof определяет его следующим образом: Multithreading is defined by an application design that ALLOWS FOR concurrent or simultaneous execution. Т.е.

    Многопоточное программирование определяется дизайном приложения, который РАЗРЕШАЕТ параллельное одновременное исполнение.

Вдумайтесь! Не «приложение, исполняющее несколько потоков», а прежде всего «дизайн, разрешающий параллельное исполнение». Даже не требующий! Кстати, использование блокировок не разрешает параллельное исполнение, т.е. использование mutex-ов, вообще говоря, не дает возможности получить хороший MT дизайн!

Не удержусь и отмечу еще один архитектурный изъян многострадальной Java (и ее недалеких клонов), содержащей по mutex-у в каждом объекте и даже предоставляющей ключевое слово synchronized , позволяющее вам легко и удобно создавать методы, параллельное одновременное исполнение которых невозможно. Да-да, все именно так и есть, как оно выглядит: языки и технологии программирования зачастую проектируются технически некомпетентными в этой области специалистами!

«За что же досталось mutex-у?!» — спросит меня опешивший читатель. «Неужели примитивы синхронизации вроде mutex-ов, semaphore-ов и т.п. вообще не нужны в MT приложениях?!» Ну, что же: они безусловно нужны для реализации некоторых, крайне низкоуровневых интерфейсов, но в обычном коде им просто нет места. Типичным примером правильного MT дизайна является приложение, в котором потоки извлекают из очереди сообщения для обработки и помещают в нее свои сообщения, обработка которых может быть произведена параллельно. Вот для реализации подобного рода очереди они и предназначены, а обычный код имеет дело только лишь с ее интерфейсом. К тому же, для случая ST приложения класс-реализация данного интерфейса никаких mutex-ов, очевидно, не должен использовать.

Судя по всему, идеальным MT приложением является приложение, в котором потоки вообще избегают какой бы то ни было синхронизации и, следовательно, могут исполняться без всяких взаимных задержек. На первый взгляд такая ситуация кажется абсурдной, но если в качестве некоего «логически единого» приложения представить себе два ST Web-сервера, работающих на двух разных машинах и отдающих пользователям один и тот же статический контент из собственных локальных копий, то мы имеем дело как раз с тем самым идеальным случаем, когда добавление второго, абсолютно независимого потока (факт. запуск на другой машине зеркального сервера) в буквальном смысле удваивает общую производительность комплекса, без оговорок.

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

Описанный выше дизайн потоки+очередь является классическим примером правильного MT приложения, если конечно потоки не пытаются передавать друг другу настолько малые подзадачи, что операция по помещению/извлечению сообщения из очереди занимает больше времени, чем обработка подзадачи самим потоком «на месте». Дизайн потоки+очередь мы и будем использовать в нашем учебном примере mtftext, равно как и в следующих за ним приложениях.

3. Многопоточное программирование на C++

3.1. example1.exe: работа с памятью

Прямым решением данной проблемы является повсеместное явное использование собственного распределителя памяти, кэширующего полученные от глобального распределителя блоки. Т.е. своего объекта mem_pool для каждого отдельного потока (как минимум). Конечно, с точки зрения удобства кодирования повсеместное мелькание ссылок mem_pool& трудно назвать приемлемым — стоит ли овчинка выделки? Давайте разберемся с помощью следующего примера:

Программа запускает заданное в командной строке количество потоков и в каждом из них выполнят фиксированное количество вставок/удалений элементов в стандартный список. Отличие функции start_ders состоит в том, что вместо стандартного аллокатора по умолчанию lst использует аллокатор на основе mem_pool .

Многопоточное программирование в C++0x

В связи с появлением нового стандарта C++11, в котором появилось множество «новых» инструментов, которые раньше решались на уровне сторонних библиотеки и API ОС, все еще мало документации по использованию таких встроенных средств самого языка. Недавно набрел на одну интересную статью, в которой описывается организация многопоточности с использованием нового стандарта, появившегося еще с C++0x (до окончательного выхода C++11). Понравилось то, что в статье полностью охватывается данная тема с примерами и достаточно хорошо организованна, в связи с чем и выкладываю в помощь программистам.

Многопоточное программирование в C++0x

Стандарт C++ 1998-ого года не имел упоминаний о существовании потоков. Все написанное в старом стандарте относиться к абстрактной машине, которая выполняет все действия последовательно. По этим причинам программирование многопоточных приложений на C++ довольно сложное и проблематичное занятие. Программистам приходилось использовать нестандартные библиотеки и применять платформозависимые решения, но в новом стандарте все иначе. C++0x определяет как новую модель памяти, так и библиотеку для разработки многопоточных приложений C++ threading library, включающую в себя средства синхронизации, создания потоков, атомарные типы и операции а также много чего другого. Давайте рассмотрим возможности новой стандартной библиотеки потоков C++0x.

std::thread

Первый класс, с которым стоит ознакомиться — это std::thread. Класс, необходимый для создание потоков в C++ находится в заголовочном файле thread (#include

Класс std::thread нельзя копировать, но его можно перемещать (std::move) и присваивать. Присваивать можно только те объекты, которые не связаны ни с каким потоком, тогда объекту будет присвоено только состояние, а при перемещении объекту передается состояние и право на управление потоком.

Каждый поток имеет свой идентификатор типа std::thread::id, который можно получить вызовом метода get_id или же вызовом статического метода std::thread::this_thread::get_id из функции самого потока.

В результате работы этого кода оба вывода должны быть одинаковыми, так-как выводится идентификатор одного и того же потока, но разными способами.

Класс std::thread также предоставляет статический метод hardware_concurrency, который возвращает количество потоков, которые могут быть выполнены действительно параллельно, но стандарт разрешает функции возвращать 0, если на данной системе это значение нельзя подсчитать или оно не определено. Класс также предоставляет пару статических методов для усыпления потоков (sleep_for и sleep_until) и функцию yield для возможности передачи управления другим потокам.

std::mutex

В идеальном мире любую задачу можно было разделить на N подзадач, которые могли бы быть выполнены параллельно, тем самым мы бы получили 100% прирост производительности на 2х процессорах, 400% на 4-х и так далее, но в реальном мире далеко не все задачи можно так разделить. В большинстве задач приходится иметь дело с общими данными, но доступ изменения общих данных разными потоками необходимо синхронизировать, чтобы потоки не мешали друг другу. Для этих целей служит объект синхронизации std::mutex (mutual exclusion). Перед тем, как обращаться к общим данным поток должен заблокировать mutex вызовом метода lock, и разблокировать его вызовом unlock когда работа с общими данными завершена.

Читать еще:  Политика безопасности организации это

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

Библиотека также содержит класс std::recursive_mutex, который можно блокировать более одного раза одним и тем же потоком. Если для обычного заблокированного mutex-а его повторное блокирование тем же потоком приведет к неопределенному поведению, то для рекурсивной версии придется вызывать unlock столько раз, сколько вызывался lock, чтобы его разблокировать.

std::lock_guard

Тем не менее не рекомендуется использовать класс std::mutex напрямую, так-как если между вызовами lock и unlock будет сгенерировано исключение — произойдет deadlock (т.е. заблокированный поток так и останется ждать). Проблема безопасности исключений в C++ threading library решена довольно обычным для C++ способом — применением техники RAII (Resource Acquisition Is Initialization). Оберткой служит шаблонный класс std::lock_guard . Это простой класс, конструктор которого вызывает метод lock для заданного объекта, а деструктор вызывает unlock. Также в конструктор класса std::lock_guard можно передать аргумент std::adopt_lock — индикатор, означающий, что mutex уже заблокирован и блокировать его заново не надо. std::lock_guard не содержит никаких других методов, его нельзя копировать, переносить или присваивать.

std::unique_lock

Еще одним классом, контролирующим блокировки mutex-а является std::unique_lock, который предоставляет немного больше возможностей, чем std::lock_guard. Помимо RAII, std::unique_lock предоставляет возможность ручной блокировки и разблокировки контролируемого mutex-а с помощью методов lock и unlock соответственно. std::unique_lock также можно перемещать с помощью вызова std::move, но наиболее важным отличием является то, что объект классa std::unique_lock может не владеть правами на mutex, который он контролирует. При создании объекта можно отложить блокирование mutex-а передачей аргумента std::defer_lock конструктору std::unique_lock и указать, что объект на владеет правами на mutex и вызывать unlock в деструкторе не надо. Права на mutex можно получить позже, вызвав метод lock для объекта. Функцией owns_lock можно проверить, владеет ли текущий объект правами на mutex.

std::lock

Вернемся к deadlock-у. Описанная ситуация с исключениями к сожалению не единственный источник deadlock-а. Deadlock также возможен в том случае, если потоки блокируют более одного mutex-a. — Представьте ситуацию, когда два mutex-а A и B защищают два разных ресурса, и, двум потокам, одновременно необходим доступ к этим двум ресурсам. Блокировка одного mutex-а — атомарна, но блокировка двух mutex-ов — это два отдельных действия, и, если первый поток заблокирует mutex A, в то время, как второй заблокирует mutex B, оба потока зависнут ожидая друг друга. Чтобы избежать подобных ситуаций всегда нужно блокировать mutex-ы в одной и той же последовательности. Если блокировать mutex A всегда до того, как блокировать mutex B — deadlock станет невозможным. К сожалению, этот принцип применять можно не всегда. Но, для решения этой проблемы, стандартная библиотека C++0x предоставляет функцию std::lock, которая блокирует переданные ей mutex-ы без опасности deadlock-а. Функция принимает бесконечное количество шаблонных аргументов, которые должны иметь методы lock и unlock.

В этом примере, мы блокируем два mutex-а, с помощью функции std::lock. std::unique_lock нам необходим для того, чтобы контролировать mutex в случае исключения, а именно: std::unique_lock (а не std::lock_guard) нам необходим для того, чтобы его можно было передать в функцию std::lock. После вызова функции std::lock, объекты la и lb станут владельцами mutex-а, и разблокируют его в деструкторе.

std::call_once

std::call_once создан для того, чтобы защищать общие данные во время инициализации. По сути, это техника, позволяющая вызвать некий участок кода один раз, независимо от количества потоков, которые пытаются выполнить этот участок кода. std::call_once — быстрый и удобный механизм для создания потокобезопасных singleton-ов. Рассмотрим пример использования

Здесь создается 2 потока, которые пытаются создать объект класса x, но конструктор класса будет вызван лишь один раз.

std::condition_variable и std::condition_variable_any

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

std::condition_variable — это объект синхронизации, предназначенный для блокирования одного потока, пока он не будет оповещен о наступлении некоего события из другого.

Рассмотрим, для начала, работу функции thread_func2. Сперва, mutex блокируется, и, дальше, вызывается метод wait, который с блокированным mutex-ом вызывает предикат (в данном случае лямбда-выражение), который должен проверить условие наступления события. Если предикат возвращает false, метод разблокирует mutex и перейдет в режим ожидания до тех пор, пока не получит оповещение. Блокирование mutex-а при проверке необходимо для того, чтобы проверка могла затрагивать общие ресурсы.

Функция thread_func1, сперва, вставит число 10 в вектор, и, только после этого, отправит оповещение одному потоку (можно отправить всем, вызовом notify_all, если ожидающих больше одного и нужно разбудить всех).

Единственное отличие между std::condition_variable и std::condition_variable_any заключается в том, что std::condition_variable работает только с блокировками типа std::unique_lock , в то время, как std::condition_variable_any может работать с любыми блокировками, поддерживающими соответствующий интерфейс.

std::async и std::future

Представьте, что Вам нужно вызвать функцию в отдельном потоке, которая, после долгих подсчетов, вернет значение. Мы можем создать новый поток с помощью std::thread, но, тогда, нам придется позаботиться о возвращении результата вызывающему. std::thread не дает прямой возможности это сделать, но комитет по стандартизации позаботился и об этом. Поток запускается вызовом функции std::async и передачей ей функции/функтора для вызова в потоке. std::async возвращает объект типа std::future , где T — тип, возвращаемый переданной в std::async функцией.

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

В стандартной библиотеке содержатся, также, классы std::unique_future и std::shared_future , построенные по тому же принципу, что и std::unique_ptr и std::shared_ptr — соответственно. А std::atomic_future работает так же, как и std::shared_future , но он, к тому же, может использоваться одновременно из разных потоков.

std::promise

std::promise — это базовый механизм, позволяющий передавать значение между потоками. Каждый объект std::promise связан с объектом std::future . Это пара классов, один из которых (std::promise ) отвечает за установку значения, а другой (std::future ) — за его получение. Первый поток может ожидать установки значения с помощью вызова метода std::future ::wait или std::future ::get, в то время, как второй поток установит это значение с помощью вызова метода std::promise ::set_value, или передаст первому исключение вызовом метода std::promise ::set_exception.

std::packaged_task

Класс std::packaged_task связывает результат функции с объектом типа std::future . Объект класса std::future будет готов тогда, когда функция завершит свою работу и вернет значение. Этот класс позволяет реализовать нечто подобное функции std::async, но он дает возможность пользователю самому управлять потоками.

Task запускается в одном из потоков, в котором вызывается функция foo и связывает его возвращаемое значение с объектом типа std::future . В то время, другой поток может получить из этого task-а связанный с ним объект std::future вызовом метода get_future, и получить ассоциированное с ним значение или исключение вызовом метода get.

std::atomic и другие атомарные типы

Атомарность операции — означает ее неделимость, т.е. ни один поток не может увидеть ее промежуточное состояние. Стандарт C++0x гарантирует, что все операции с типами std::atomic и std::atomic_* — атомарны. Ниже перечислены атомарные типы, входящие в состав стандартной библиотеки C++.

Записки программиста

Написание многопоточных приложений на C++

В более ранних постах было рассказано про многопоточность в Windows при помощи CreateThread и прочего WinAPI, а также многопоточность в Linux и других *nix системах при помощи pthreads. Если вы пишите на C++11 или более поздних версиях, то вам доступны std::thread и другие многопоточные примитивы, появившиеся в этом стандарте языка. Далее будет показано, как с ними работать. В отличие от WinAPI и pthreads, код, написанный на std::thread, является кроссплатформенным.

Читать еще:  Курсы по языку программирования си

Примечание: Приведенный код был проверен на GCC 7.1 и Clang 4.0 под Arch Linux, GCC 5.4 и Clang 3.8 под Ubuntu 16.04 LTS, GCC 5.4 и Clang 3.8 под FreeBSD 11, а также Visual Studio Community 2017 под Windows 10. CMake до версии 3.8 не умеет говорить компилятору использовать стандарт C++17, указанный в свойствах проекта. Как установить CMake 3.8 в Ubuntu 16.04 описано здесь. Чтобы код компилировался при помощи Clang, в *nix системах должен быть установлен пакет libc++. Для Arch Linux пакет доступен на AUR. В Ubuntu есть пакет libc++-dev, но вы можете столкнуться с непофикшенным багом, из-за которого код так просто собираться не будет. Воркэраунд описан на StackOverflow. Во FreeBSD для компиляции проекта нужно установить пакет cmake-modules.

Мьютексы

Ниже приведен простейший пример использования трэдов и мьютексов:

std :: mutex mtx ;
static int counter = 0 ;
static const int MAX_COUNTER_VAL = 100 ;

void thread_proc ( int tnum ) <
for ( ;; ) <
<
std :: lock_guard lock ( mtx ) ;
if ( counter == MAX_COUNTER_VAL )
break ;
int ctr_val = ++ counter ;
std :: cout threads ;
for ( int i = 0 ; i
#include
#include

// std::shared_mutex mtx; // will not work with GCC 5.4
std :: shared_timed_mutex mtx ;

static int counter = 0 ;
static const int MAX_COUNTER_VAL = 100 ;

void thread_proc ( int tnum ) <
for ( ;; ) <
<
// see also std::shared_lock
std :: unique_lock lock ( mtx ) ;
if ( counter == MAX_COUNTER_VAL )
break ;
int ctr_val = ++ counter ;
std :: cout threads ;
for ( int i = 0 ; i
#include
#include

std :: mutex io_mtx ;
thread_local int counter = 0 ;
static const int MAX_COUNTER_VAL = 10 ;

void thread_proc ( int tnum ) <
for ( ;; ) <
counter ++ ;
if ( counter == MAX_COUNTER_VAL )
break ;
<
std :: lock_guard lock ( io_mtx ) ;
std :: cout threads ;
for ( int i = 0 ; i
#include
#include

static std :: atomic_int atomic_counter ( 0 ) ;
static const int MAX_COUNTER_VAL = 100 ;

std :: mutex io_mtx ;

void thread_proc ( int tnum ) <
for ( ;; ) <
<
int ctr_val = ++ atomic_counter ;
if ( ctr_val >= MAX_COUNTER_VAL )
break ;

<
std :: lock_guard lock ( io_mtx ) ;
std :: cout threads ;

int nthreads = std :: thread :: hardware_concurrency ( ) ;
if ( nthreads == 0 ) nthreads = 2 ;

for ( int i = 0 ; i < nthreads ; i ++ ) <
std :: thread thr ( thread_proc, i ) ;
threads. emplace_back ( std :: move ( thr ) ) ;
>

Обратите внимание на использование процедуры hardware_concurrency. Она возвращает оценку количества трэдов, которое в текущей системе может выполняться параллельно. Например, на машине с четырехядерным процессором, поддерживающим hyper threading, процедура возвращает число 8. Также процедура может возвращать ноль, если сделать оценку не удалось или процедура попросту не реализована.

Кое-какую информацию о работе атомарных переменных на уровне ассемблера можно найти в заметке Шпаргалка по основным инструкциям ассемблера x86/x64.

Заключение

Насколько я вижу, все это действительно неплохо работает. То есть, при написании кроссплатформенных приложений на C++ про WinAPI и pthreads можно благополучно забыть. В чистом C начиная с C11 также существуют кроссплатформенные трэды. Но они все еще не поддерживаются Visual Studio (я проверил) , и вряд ли когда-либо будут поддерживаться. Не секрет, что Microsoft не видит интереса в развитии поддержки языка C в своем компиляторе, предпочитая концентрироваться на C++.

За кадром осталось еще немало примитивов: std::condition_variable(_any), std::(shared_)future, std::promise, std::sync и другие. Для ознакомления с ними я рекомендую сайт cppreference.com. Также может иметь смысл прочитать книгу C++ Concurrency in Action. Но должен предупредить, что она уже не новая, содержит многовато воды, и в сущности пересказывает десяток статей с cppreference.com.

Полная версия исходников к этой заметке, как обычно, лежит на GitHub. А как вы сейчас пишите многопоточные приложения на C++?

Введение в многопоточность

Введение

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

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

Несинхронизированные потоки

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

Как видно из результата работы процесса, основной поток (сама программа) и поток Thread действительно работают параллельно (красным цветом обозначено состояние, когда основной поток выводит массив во время его заполнения потоком Thread):

Запустите программу, затем нажмите \»Pause\» для остановки вывода на дисплей (т.е. приостанавливаются операции ввода/вывода основного потока, но поток Thread продолжает свое выполнение в фоновом режиме) и любую другую клавишу для возобновления выполнения.

Критические секции

А что делать, если основной поток должен читать данные из массива после его обработки в параллельном процессе? Одно из решений этой проблемы — использование критических секций.

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

Мьютексы (взаимоисключения)

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

Два (или более) процесса могут создать мьютекс с одним и тем же именем, вызвав метод CreateMutex . Первый процесс действительно создает мьютекс, а следующие процессы получают хэндл уже существующего объекта. Это дает возможность нескольким процессам получить хэндл одного и того же мьютекса, освобождая программиста от необходимости заботиться о том, кто в действительности создает мьютекс. Если используется такой подход, желательно установить флаг bInitialOwner в FALSE, иначе возникнут определенные трудности при определении действительного создателя мьютекса.

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

  • Дочерний процесс, созданный при помощи функции CreateProcess может наследовать хэндл мьютекса в случае, если при его (мьютекса) создании функией CreateMutex был указан параметр lpMutexAttributes.
  • Процесс может получить дубликат существующего мьютекса с помощью функции DuplicateHandle .
  • Процесс может указать имя существующего мьютекса при вызове функций OpenMutex или CreateMutex .

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

События

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

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

Обзор многопоточности

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

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

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

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

Поток (thread) представляет собой независимую последовательность инструкций в программе. Потоки играют важную роль как для клиентских, так и для серверных приложений. К примеру, во время ввода какого-то кода C# в окне редактора Visual Studio проводится анализ на предмет различных синтаксических ошибок. Этот анализ осуществляется отдельным фоновым потоком. То же самое происходит и в средстве проверки орфографии в Microsoft Word. Один поток ожидает ввода данных пользователем, а другой в это время выполняет в фоновом режиме некоторый анализ. Третий поток может сохранять записываемые данные во временный файл, а четвертый — загружать дополнительные данные из Интернета.

В приложении, которое функционирует на сервере, один поток всегда ожидает поступления запроса от клиента и потому называется потоком-слушателем (listener thread). При получении запроса он сразу же пересылает его отдельному рабочему потоку (worker thread), который дальше сам продолжает взаимодействовать с клиентом. Поток-слушатель после этого незамедлительно возвращается к своим обязанностям по ожиданию поступления следующего запроса от очередного клиента.

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

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

Основы многопоточной обработки

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

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

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

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

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

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

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

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

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

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

В языке C# и среде .NET Framework поддерживаются обе разновидности многозадачности: на основе процессов и на основе потоков. Поэтому средствами C# можно создавать как процессы, так и потоки, а также управлять и теми и другими. Для того чтобы начать новый процесс, от программирующего требуется совсем немного усилий, поскольку каждый предыдущий процесс совершенно обособлен от последующего.

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

Классы, поддерживающие многопоточное программирование, определены в пространстве имен System.Threading. Поэтому любая многопоточная программа на C# включает в себя следующую строку кода:

Пространство имен System.Threading содержит различные типы, позволяющие создавать многопоточные приложения. Пожалуй, главным среди них является класс Thread, поскольку он представляет отдельный поток. Чтобы программно получить ссылку на поток, выполняемый конкретным его экземпляром, просто вызовите статическое свойство Thread.CurrentThread:

На платформе .NET не существует прямого соответствия «один к одному» между доменами приложений (AppDomain) и потоками. Фактически определенный AppDomain может иметь несколько потоков, выполняющихся в каждый конкретный момент времени. Более того, конкретный поток не привязан к одному домену приложений на протяжении своего времени существования. Потоки могут пересекать границы доменов приложений, когда это вздумается планировщику Windows и CLR.

Хотя активные потоки могут пересекать границы AppDomain, каждый поток в каждый конкретный момент времени может выполняться только внутри одного домена приложений (другими словами, невозможно, чтобы один поток работал в более чем одном домене приложений сразу). Чтобы программно получить доступ к AppDomain, в котором работает текущий поток, вызовите статический метод Thread.GetDomain():

Единственный поток также в любой момент может быть перемещен в определенный контекст, и он может перемещаться в пределах нового контекста по прихоти CLR. Для получения текущего контекста, в котором выполняется поток, используйте статическое свойство Thread.CurrentContext (которое возвращает объект System.Runtime.Remoting.Contexts.Context):

Еще раз: за перемещение потоков между доменами приложений и контекстами отвечает CLR. Как разработчик .NET, вы всегда остаетесь в счастливом неведении относительно того, когда завершается каждый конкретный поток (или куда именно он будет помещен после перемещения). Тем не менее, полезно знать о различных способах получения лежащих в основе примитивов.

Ссылка на основную публикацию
ВсеИнструменты
Adblock
detector
×
×