Vvmebel.com

Новости с мира ПК
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, C# и Java

Уроки программирования, алгоритмы, статьи, исходники, примеры программ и полезные советы

ОСТОРОЖНО МОШЕННИКИ! В последнее время в социальных сетях участились случаи предложения помощи в написании программ от лиц, прикрывающихся сайтом vscode.ru. Мы никогда не пишем первыми и не размещаем никакие материалы в посторонних группах ВК. Для связи с нами используйте исключительно эти контакты: vscoderu@yandex.ru, https://vk.com/vscode

Потоки в C# для начинающих: разбор, реализация, примеры

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

Что такое потоки в C#

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

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

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

Читать еще:  Принципы объектно ориентированного программирования

Стандартно в проектах Visual Studio существует только один основной поток – в методе Main. Всё, что в нём выполняется – выполняется последовательно строка за строкой. Но при необходимости можно “распараллелить” выполняемые процессы при помощи потоков.

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

Например, если один работник будет собирать шкаф час, то вдвоём они могут управиться уже за полчаса. Однако не стоит переусердствовать в количестве работников (потоков). Математически, если нанять 4 работника, то шкаф соберется за 15 минут, если нанять 60 работников – за 1 минуту, а если нанять 3600, то вообще за секунду, но ведь на деле это неверно. Работники будут только мешать друг другу, толкаться, отнимать друг у друга детали, и процесс сборки шкафа может затянуться очень надолго.

Так же и с потоками. Чем больше потоков, тем выше вероятность, что они будут мешать друг другу выполнять свою работу. Например, если заставить работать огромное количество потоков с одними и теми же данными, потокам придётся выстраиваться в очередь для их обработки (например, если тем же 3600 рабочим дать какое-либо письменное задание, но предоставить им для этого дела всего одну ручку, то работникам, естественно, придётся становиться друг за другом в очередь за ручкой, чтобы после её получения выполнить поставленную задачу. Времени это займёт довольно много).

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

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

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

Реализация потоков в C#

Как создавать потоки в C#

Перво-наперво для работы с потоками в C# необходимо подключить специальную директиву:

Многопоточность в C

Что такое тема?
Поток — это отдельный поток последовательности внутри процесса. Поскольку потоки имеют некоторые свойства процессов, их иногда называют.

Каковы различия между процессом и потоком?
Потоки не являются независимыми друг от друга, как процессы, в результате потоки делятся с другими потоками своими разделами кода, данными и ресурсами ОС, такими как открытые файлы и сигналы. Но, как и процесс, поток имеет свой собственный программный счетчик (ПК), набор регистров и пространство стека.

Почему многопоточность?
Потоки — это популярный способ улучшить приложение с помощью параллелизма. Например, в браузере несколько вкладок могут быть разными потоками. MS word использует несколько потоков, один поток для форматирования текста, другой поток для обработки ввода и т. Д.
Потоки работают быстрее, чем процессы по следующим причинам:
1) Создание темы намного быстрее.
2) Переключение контекста между потоками происходит намного быстрее.
3) темы могут быть легко прекращены
4) Связь между потоками быстрее.

rmuhamma/OpSystems/Myos/threads.htm для получения более подробной информации.

Можем ли мы написать многопоточные программы на C?
В отличие от Java, многопоточность не поддерживается стандартом языка. POSIX Threads (или Pthreads) — это стандарт POSIX для потоков. Реализация pthread доступна с компилятором gcc.

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

#include
#include
#include //Header file for sleep(). man 3 sleep for details.
#include

// Обычная функция C, которая выполняется как поток
// когда его имя указано в pthread_create ()

void *myThreadFun( void *vargp)

printf ( «Printing GeeksQuiz from Thread n» );

printf ( «Before Threadn» );

printf ( «After Threadn» );

В main () мы объявляем переменную с именем thread_id, которая имеет тип pthread_t, который является целым числом, используемым для идентификации потока в системе. После объявления thread_id мы вызываем функцию pthread_create () для создания потока.
pthread_create () принимает 4 аргумента.
Первый аргумент — это указатель на thread_id, который устанавливается этой функцией.
Второй аргумент определяет атрибуты. Если значение равно NULL, то должны использоваться атрибуты по умолчанию.
Третий аргумент — это имя функции, которая должна быть выполнена для создаваемого потока.
Четвертый аргумент используется для передачи аргументов функции, myThreadFun.
Функция pthread_join () для потоков является эквивалентом wait () для процессов. Вызов pthread_join блокирует вызывающий поток, пока поток с идентификатором, равным первому аргументу, не завершится.

Как скомпилировать вышеуказанную программу?
Чтобы скомпилировать многопоточную программу с использованием gcc, нам нужно связать ее с библиотекой pthreads. Ниже приведена команда, используемая для компиляции программы.

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

#include
#include
#include
#include

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

// Функция, выполняемая всеми потоками

void *myThreadFun( void *vargp)

// Сохраняем аргумент значения, переданный в этот поток

int *myid = ( int *)vargp;

// Давайте создадим статическую переменную для наблюдения за ее изменениями

static int s = 0;

// Изменить статические и глобальные переменные

// Распечатать аргумент, статические и глобальные переменные

printf ( «Thread ID: %d, Static: %d, Global: %dn» , *myid, ++s, ++g);

Многопоточное программирование в 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++.

Асинхронное программирование. Часть 1: Как работает процессор

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

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

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

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

ВНИМАНИЕ!

Информация в следующих разделах сильно упрощена.

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Как работает процессор

За единицу времени (она называется тик) процессор может выполнить только одну задачу — например, прочитать значение ячейки памяти. Поэтому на то, чтобы выполнить следующие операции, потребуется 4 тика:

  1. Прочитать данные из двух ячеек.
  2. Сложить их.
  3. Записать результат в другую ячейку.

Это касается только атомарных операций, то есть самых маленьких и неделимых. Более сложные задачи могут состоять из нескольких атомарных. Например, чтобы провести умножение числа A на число B, нужно будет прибавить к числу A само себя B — 1 раз.

5 * 5 = 5 + 5 + 5 + 5 + 5

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

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

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

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

Количество тиков измеряется в герцах (Гц) — это единица измерения частоты протекания периодических процессов. Например, если мимо вашего дома раз в секунду проезжает гоночный болид, то его частота будет равна 1 Гц. Если болид проезжает два раза в секунду, то его частота — 2 Гц; если он проезжает трижды, то давно пора подумать о переезде.

Процессор так быстро выполняет процессы, что его частота измеряется в гигагерцах.

1 ГГц = 1 000 000 000 Гц

Частота современных процессоров обычно равна 2-3 ГГц.

Как процессор выполняет программы

Каждая программа состоит из множества процессов: нужно 500 раз провести сложение, записать данные в 2000 ячеек, перемножить всё это, а потом, наконец, поделить.

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

Тогда код программы будет выглядеть примерно так:

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

Поэтому, чтобы компьютерами было удобно пользоваться, мы делим все выполняемые программы на потоки. Допустим, у нас запущено 10 программ, а процессор работает на скорости 100 тиков в секунду.

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

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

Когда нужна асинхронность

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

Если же всё это выполняется в одном потоке, то приложение будет подвисать, когда выполняется сложная инструкция. В ОС Windows часто можно заметить, что, когда приложение что-то делает, а вы кликаете на него, то в заголовке окна можно увидеть словосочетание «Не отвечает».

Это не всегда означает, что приложение зависло. Возможно, оно просто занято решением какой-то сложной задачи, которая выполняется в том же потоке.

Пример асинхронного приложения

Чтобы лучше закрепить тему, давайте напишем приложение с использованием асинхронности. Оно будет разделено на два потока (Thread): первый будет обновлять полосу загрузки, а второй — выполнять саму загрузку.

В следующих статьях вы узнаете, как всё устроено, а пока посмотрим, как это работает:

Здесь можно заметить, что курсор обновляется чаще (каждые 100 мс), чем проценты (каждые 500 мс), как и было записано в коде программы.

Заключение

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

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

Многое об АМП вы можете узнать на нашем курсе «Профессия C#-разработчик». Там вы освоите все инструменты, необходимые программисту на C#, чтобы постепенно стать профессионалом.

Ссылка на основную публикацию
Adblock
detector