Компилятор Microsoft C/C++ (MSVC) обеспечивает поддержку создания многопоточных приложений. Рассмотрите возможность использования нескольких потоков, если приложению необходимо выполнять ресурсоемкие операции, которые могут привести к тому, что пользовательский интерфейс перестанет отвечать на запросы.
В MSVC существует несколько способов программирования с несколькими потоками: можно использовать C++/WinRT и библиотеку среда выполнения Windows, библиотеку Microsoft Foundation Class (MFC), C++/CLI и среду выполнения .NET, библиотеку времени выполнения C и API Win32. Эта статья посвящена многопоточности в C. Пример кода см . в разделе Пример многопоточной программы на языке C.
Многопоточные программы
Поток — это, по сути, путь к выполнению через программу. Это также наименьшая единица выполнения, которую планирует Win32. Поток состоит из стека, состояния регистров ЦП и записи в списке выполнения системного планировщика. Каждый поток совместно использует все ресурсы процесса.
[C++11] STL: Thread — Многопоточные программы
Процесс состоит из одного или нескольких потоков, а также кода, данных и других ресурсов программы в памяти. Типичными ресурсами программы являются открытые файлы, семафоры и динамически выделяемая память. Программа выполняется, когда системный планировщик предоставляет управление выполнением одного из своих потоков.
Планировщик определяет, какие потоки должны выполняться и когда они должны выполняться. Потокам с более низким приоритетом может потребоваться ждать, пока потоки с более высоким приоритетом выполняют свои задачи. На многопроцессорных компьютерах планировщик может перемещать отдельные потоки на разные процессоры, чтобы сбалансировать нагрузку на ЦП.
Каждый поток в процессе работает независимо. Если вы не сделаете их видимыми друг для друга, потоки выполняются по отдельности и не знают о других потоках в процессе. Однако потоки, совместно использующие общие ресурсы, должны координировать свою работу с помощью семафоров или другого метода межпроцессного взаимодействия. Дополнительные сведения о синхронизации потоков см. в статье Написание многопоточной программы Win32.
Поддержка многопоточности библиотеками
Все версии CRT теперь поддерживают многопоточность, за исключением версий некоторых функций без блокировки. Дополнительные сведения см. в разделе Производительность многопоточных библиотек. Сведения о версиях CRT, доступных для связывания с кодом, см. в разделе Функции библиотеки CRT.
Включаемые файлы для многопоточности
Стандартные файлы CRT объявляют функции библиотеки времени выполнения C по мере их реализации в библиотеках. Если параметры компилятора указывают соглашения о вызовах __fastcall или __vectorcall , компилятор предполагает, что все функции должны вызываться с помощью соглашения о вызовах регистра. Функции библиотеки времени выполнения используют соглашение о вызовах C, а объявления в стандартных файлах включения предписывают компилятору создавать правильные внешние ссылки на эти функции.
Многопоточность | Потоки | thread | Многопоточное программирование | Уроки | C++ #1
Функции CRT для управления потоками
Все программы Win32 имеют по крайней мере один поток. Любой поток может создавать дополнительные потоки. Поток может быстро завершить свою работу, а затем завершить работу, или он может оставаться активным в течение всего срока действия программы.
Библиотеки CRT предоставляют следующие функции для создания и завершения потоков: _beginthread, _beginthreadex, _endthread и _endthreadex.
Функции _beginthread и _beginthreadex создают новый поток и возвращают идентификатор потока, если операция выполнена успешно. Поток завершается автоматически после завершения выполнения. Кроме того, он может завершиться вызовом _endthread или _endthreadex .
При вызове подпрограмм времени выполнения C из программы, созданной с помощью libcmt.lib, необходимо запустить потоки с _beginthread помощью функции или _beginthreadex . Не используйте функции ExitThread Win32 и CreateThread . Использование SuspendThread может привести к взаимоблокировке, когда несколько потоков блокируются в ожидании завершения доступа приостановленного потока к структуре данных среды выполнения C.
Функции _beginthread и _beginthreadex
Функции _beginthread и _beginthreadex создают новый поток. Поток совместно использует сегменты кода и данных процесса с другими потоками в процессе, но имеет собственные уникальные значения регистра, пространство стека и текущий адрес инструкции. Система предоставляет время ЦП каждому потоку, чтобы все потоки в процессе могли выполняться одновременно.
_beginthread и _beginthreadex похожи на функцию CreateThread в API Win32, но имеют следующие отличия:
- Они инициализируют определенные переменные библиотеки времени выполнения C. Это важно, только если в потоках используется библиотека времени выполнения C.
- CreateThread помогает обеспечить контроль над атрибутами безопасности. Эту функцию можно использовать для запуска потока в приостановленном состоянии.
_beginthread и _beginthreadex возвращают дескриптор в новый поток в случае успешного выполнения или код ошибки, если произошла ошибка.
Функции _endthread и _endthreadex
Функция _endthread завершает поток, созданный с помощью _beginthread (и аналогично завершает поток, _endthreadex созданный ). _beginthreadex Потоки завершаются автоматически по завершении. _endthread и _endthreadex полезны для условного завершения из потока. Например, поток, выделенный для обработки данных, может завершить работу, если ему не удается получить контроль над портом связи.
Написание многопоточной программы Win32
При написании программы с несколькими потоками необходимо координировать их поведение и использование ресурсов программы. Кроме того, убедитесь, что каждый поток получает собственный стек.
Совместное использование общих ресурсов между потоками
Каждый поток имеет собственный стек и собственную копию регистров ЦП. Другие ресурсы, такие как файлы, статические данные и память кучи, совместно используются всеми потоками в процессе. Потоки, использующие эти общие ресурсы, должны быть синхронизированы. Win32 предоставляет несколько способов синхронизации ресурсов, включая семафоры, критические разделы, события и мьютексы.
Если несколько потоков обращаются к статическим данным, программа должна обеспечить возможные конфликты ресурсов. Рассмотрим программу, в которой один поток обновляет статическую структуру данных, содержащую координаты x,y для элементов, отображаемых другим потоком. Если поток обновления изменяет координату x и вытесняется перед изменением координаты y , поток отображения может быть запланирован до обновления координаты y . Элемент будет отображаться в неправильном расположении. Эту проблему можно избежать, используя семафоры для управления доступом к структуре.
Мьютекс (сокращение от mutual exclusion) — это способ взаимодействия между потоками или процессами, которые асинхронно выполняются друг с другом. Такое взаимодействие можно использовать для координации действий нескольких потоков или процессов, обычно путем управления доступом к общему ресурсу путем блокировки и разблокировки ресурса.
Чтобы решить эту проблему с обновлением координат x,y, поток обновления устанавливает мьютекс, указывающий, что структура данных используется перед выполнением обновления. Он очищает мьютекс после обработки обеих координат. Поток отображения должен дождаться очистки мьютекса перед обновлением дисплея. Этот процесс ожидания мьютекса часто называется блокировкой для мьютекса, так как процесс блокируется и не может продолжаться до тех пор, пока мьютекс не будет очищен.
Программа Bounce.c, показанная в примере многопоточной программы C, использует мьютекс с именем ScreenMutex для координации обновлений экрана. Каждый раз, когда один из потоков отображения готов к записи на экран, он вызывает WaitForSingleObject с дескриптором ScreenMutex и константой INFINITE, чтобы указать, что WaitForSingleObject вызов должен блокироваться на мьютексе, а не истекать время ожидания. Если ScreenMutex параметр не задан, функция ожидания задает мьютекс, чтобы другие потоки не могли вмешиваться в отображение, и продолжает выполнение потока. В противном случае поток блокируется до тех пор, пока мьютекс не будет очищен. Когда поток завершает обновление дисплея, он освобождает мьютекс, вызывая ReleaseMutex .
Экранные дисплеи и статические данные — это только два ресурса, требующих тщательного управления. Например, программа может иметь несколько потоков, обращаюющихся к одному файлу. Так как другой поток мог переместить указатель на файл, каждый поток должен сбросить указатель на файл перед чтением или записью. Кроме того, каждый поток должен убедиться, что он не вытеснен между временем, когда он размещает указатель, и временем доступа к файлу. Эти потоки должны использовать семафор для координации доступа к файлу, закрепив каждый доступ к файлу с WaitForSingleObject помощью вызовов и ReleaseMutex . Следующий пример кода иллюстрирует этот метод:
HANDLE hIOMutex = CreateMutex (NULL, FALSE, NULL); WaitForSingleObject( hIOMutex, INFINITE ); fseek( fp, desired_position, 0L ); fwrite( data, sizeof( data ), 1, fp ); ReleaseMutex( hIOMutex);
Стеки потоков
Все пространство стека приложения по умолчанию выделяется первому потоку выполнения, который называется потоком 1. В результате необходимо указать, сколько памяти будет выделено для отдельного стека для каждого дополнительного потока, необходимого вашей программе. При необходимости операционная система выделяет дополнительное пространство стека для потока, но необходимо указать значение по умолчанию.
Первый аргумент в вызове _beginthread является указателем на функцию BounceProc , которая выполняет потоки. Второй аргумент указывает размер стека по умолчанию для потока. Последний аргумент — это идентификатор, который передается в BounceProc . BounceProc использует номер идентификатора для заполнения генератора случайных чисел и выбора атрибута цвета потока и отображаемого символа.
Потоки, которые вызывают библиотеку времени выполнения C или API Win32, должны иметь достаточно места в стеке для библиотеки и функций API, которые они вызывают. Для функции C printf требуется более 500 байт пространства стека, а при вызове процедур API Win32 должно быть доступно 2 КБ пространства стека.
Так как каждый поток имеет собственный стек, можно избежать потенциальных конфликтов между элементами данных, используя как можно меньше статических данных. Разработайте программу так, чтобы использовать автоматические переменные стека для всех данных, которые могут быть закрытыми для потока. Единственными глобальными переменными в программе Bounce.c являются либо мьютексы, либо переменные, которые никогда не изменяются после инициализации.
Win32 также предоставляет локальное хранилище потоков (TLS) для хранения данных для каждого потока. Дополнительные сведения см. в разделе Локальное хранилище потоков (TLS).
Устранение потенциальных проблем при работе с многопоточными программами
Существует несколько проблем, которые могут возникнуть при создании, связывании или выполнении многопоточной программы C. Некоторые из наиболее распространенных проблем описаны в следующей таблице. (Аналогичное обсуждение с точки зрения MFC см. в разделе Многопоточность: советы по программированию.)
Источник: learn.microsoft.com
Многопоточность
Одним из ключевых аспектов в современном программировании является многопоточность . Ключевым понятием при работе с многоопоточностью является поток. Поток предствляет некоторую часть кода программы. При выполнении программы каждому потоку выделяется определенный квант времени.
И при помощи многопоточности мы можем выделить в приложении несколько потоков, которые будут выполнять различные задачи одновременно. Если у нас, допустим, графическое приложение, которое посылает запрос к какому-нибудь серверу или считывает и обрабатывает огромный файл, то без многопоточности у нас бы блокировался графический интерфейс на время выполнения задачи. А благодаря потокам мы можем выделить отправку запроса или любую другую задачу, которая может долго обрабатываться, в отдельный поток. Поэтому, к примеру, клиент-серверные приложения (и не только они) практически не мыслимы без многопоточности.
Основной функционал для использования потоков в приложении сосредоточен в пространстве имен System.Threading . В нем определен класс, представляющий отдельный поток — класс Thread .
Класс Thread определяет ряд методов и свойств, которые позволяют управлять потоком и получать информацию о нем. Основные свойства класса:
- ExecutionContext : позволяет получить контекст, в котором выполняется поток
- IsAlive : указывает, работает ли поток в текущий момент
- IsBackground : указывает, является ли поток фоновым
- Name : содержит имя потока
- ManagedThreadId : возвращает числовой идентификатор текущего потока
- Priority : хранит приоритет потока — значение перечисления ThreadPriority :
- Lowest
- BelowNormal
- Normal
- AboveNormal
- Highest
По умолчанию потоку задается значение Normal. Однако мы можем изменить приоритет в процессе работы программы. Например, повысить важность потока, установив приоритет Highest. Среда CLR будет считывать и анализировать значения приоритета и на их основании выделять данному потоку то или иное количество времени.
- Aborted : поток остановлен, но пока еще окончательно не завершен
- AbortRequested : для потока вызван метод Abort, но остановка потока еще не произошла
- Background : поток выполняется в фоновом режиме
- Running : поток запущен и работает (не приостановлен)
- Stopped : поток завершен
- StopRequested : поток получил запрос на остановку
- Suspended : поток приостановлен
- SuspendRequested : поток получил запрос на приостановку
- Unstarted : поток еще не был запущен
- WaitSleepJoin : поток заблокирован в результате действия методов Sleep или Join
В процессе работы потока его статус многократно может измениться под действием методов. Так, в самом начале еще до применения метода Start его статус имеет значение Unstarted . Запустив поток, мы изменим его статус на Running . Вызвав метод Sleep, статус изменится на WaitSleepJoin .
Кроме того статическое свойство CurrentThread класса Thread позволяет получить текущий поток
В программе на C# есть как минимум один поток — главный поток, в котором выполняется метод Main.
Например, используем вышеописанные свойства для получения информации о потоке:
using System.Threading; // получаем текущий поток Thread currentThread = Thread.CurrentThread; //получаем имя потока Console.WriteLine($»Имя потока: «); currentThread.Name = «Метод Main»; Console.WriteLine($»Имя потока: «); Console.WriteLine($»Запущен ли поток: «); Console.WriteLine($»Id потока: «); Console.WriteLine($»Приоритет потока: «); Console.WriteLine($»Статус потока: «);
В этом случае мы получим примерно следующий вывод:
Имя потока: Имя потока: Метод Main Запущен ли поток: True Id потока: 1 Приоритет потока: Normal Статус потока: Running
Так как по умолчанию свойство Name у объектов Thread не установлено, то в первом случае мы получаем в качестве значения этого свойства пустую строку.
Также класс Thread определяет ряд методов для управления потоком. Основные из них:
- Статический метод GetDomain возвращает ссылку на домен приложения
- Статический метод GetDomainID возвращает id домена приложения, в котором выполняется текущий поток
- Статический метод Sleep останавливает поток на определенное количество миллисекунд
- Метод Interrupt прерывает поток, который находится в состоянии WaitSleepJoin
- Метод Join блокирует выполнение вызвавшего его потока до тех пор, пока не завершится поток, для которого был вызван данный метод
- Метод Start запускает поток
Например, применим метод Sleep для задания задержки выполнения приложения:
using System.Threading; for(int i = 0; i < 10; i++) < Thread.Sleep(500); // задержка выполнения на 500 миллисекунд Console.WriteLine(i); >
Источник: metanit.com
[C++] часть 1: многопоточность, конкурентность и параллелизм: ОСНОВЫ

Вначале, когда ещё только состоялось моё знакомство с многопоточностью в C++, многое было мне непонятным и сбивало с толку. Сложность программы расцветала буйным цветом (именно так: подобно прекрасному цветку), конкурентность и параллелизм с их недетерминированным поведением меня просто убивали, и всё было как в тумане. Так что мне легко понять всех приступающих к изучению этих понятий. Спешу избавить вас от мучений и предлагаю вашему вниманию это простое руководство по изучению конкурентности, параллелизма и многопоточности в C++ (в конце данной статьи расписан план, в соответствии с которым мы будем двигаться дальше).
А пока освежим в памяти основные понятия и попробуем на вкус код, выполняемый в многопоточной среде.
1. Что такое поток?
В любом процессе создаётся уникальный поток выполнения, который называется основным потоком. Он может с помощью операционной системы запускать или порождать другие потоки, которые делят то же адресное пространство родительского процесса (сегмент кода, сегмент данных, а также другие ресурсы операционной системы, такие как открытые файлы и сигналы). С другой стороны, у каждого потока есть свой идентификатор потока, стек, набор регистров и счётчик команд. По сути, поток представляет собой легковесный процесс, в котором переключение между потоками происходит быстрее, а взаимодействие между процессами — легче.
2. Что такое конкурентность/параллелизм
Планировщик распределяет процессорное время между разными потоками. Это называется аппаратным параллелизмом или аппаратной конкурентностью (пока что считаем здесь параллелизм и конкурентность синонимами): когда несколько потоков выполняются на разных ядрах параллельно, причём каждый занимается конкретной задачей программы.
→ Примечание: чтобы определить количество задач, которые реально можно выполнять в многопоточном режиме на том или ином компьютере, используется функция std::thread::hardware_concurrency() . Если число потоков будет превышать этот лимит, может начаться настоящая чехарда с переключением задач (когда слишком частые переключения между задачами — много раз в секунду — создают лишь иллюзию многопоточности).

3. Основные операции с потоками с помощью std::thread
- Заголовочный файл| #include
- Запуск потока| std::thread t(callable_object, arg1, arg2, ..)
Создаёт новый поток выполнения, ассоциируемый с t, который вызывает callable_object(arg1, arg2) . Вызываемый объект (т.е. указатель функции, лямбда-выражение, экземпляр класса с вызовом функции operator ) немедленно выполняется новым потоком с (выборочно) передаваемыми аргументами. Они копируются по умолчанию. Если хотите передать по ссылке, придётся использовать метод warp к аргументу с помощью std::ref(arg) . Не забывайте: если хотите передать unique_ptr, то должны переместить его ( std::move(my_pointer) ), так как его нельзя копировать. - Жизненный цикл потока| t.join() и t.detach()
Если основной поток завершает выполнение, все второстепенные сразу останавливаются без возможности восстановления. Чтобы этого не допустить, у родительского потока имеются два варианта для каждого порождённого:
→ Блокирует и ждёт завершения порождённого потока, вызывая на нём метод join .
→ Прямо объявляет, что порождённый поток может продолжить выполнение даже после завершения родительского, используя метод detach . - Запомните: объект потока можно перенести, но нельзя копировать.
Здесь вы можете найти пример кода, иллюстрирующий практически всё, что написано выше.
4. Зачем нужна синхронизация?
Из-за того, что несколько потоков делят одно адресное пространство и ресурсы, многие операции становятся критичными, и тогда многопоточности требуются примитивы синхронизации. И вот почему:
- Память — дом с привидениями
Память никогда больше не будет обычным хранилищем данных — теперь это обитель привидений. Представьте: поток смотрит Netflix, уютно устроившись перед Smart TV, и тут вдруг экран мигает и выключается. В панике поток набирает 112, а в ответ… «Доставка пиццы, спасибо, что позвонили». Что происходит? А то, что в доме полно привидений (где в роли привидений другие потоки): они все в одной комнате и взаимодействуют с одними и теми же объектами (это называется гонка данных), но друг для друга они привидения.

Поток должен объявить, что он использует. А затем, прежде чем трогать этот объект, проверить, не использует ли его кто-то ещё. Зелёный поток смотрит ТВ? Значит, никто не должен трогать ТВ (другие могут рядышком сесть и посмотреть, если что). Это можно сделать с помощью мьютекса.
- Нужны атомарные операции!
Большинство операций неатомарные. Если операция неатомарная, можно увидеть её промежуточное состояние, так как она не является неделимой. Например: запись 64 битов, 32 бита за один раз. Во время этой операции другой поток может увидеть 32 старых бита и 32 новых, получая совершенно неверный результат. По этой причине результаты таких операций должны казаться атомарными, даже если они такими не являются.
→ Примечание: даже инкремент не является атомарной операцией: int tmp = a; a = tmp + 1;
Самое простое решение здесь — использовать шаблон std::atomic , который разрешает атомарные операции разных типов. - Когерентность кеша и выполнение с изменением очерёдности
Каждое ядро пытается сохранить результаты какой-то работы, помещая недавние значения в локальный кеш. Но несколько потоков выполняются на разных ядрах, и значения, хранящиеся в кеше, больше не могут быть валидными, так что рано или поздно кеш должен обновляться. В то же время изменения не видны другим, пока кеш не очищен. Чтобы распространить изменения и обеспечить корректную видимость памяти, нужны определённые механизмы.
Кроме того, для повышения эффективности процессор и/или компилятор может поменять очерёдность выполнения команд. Это может привести к непредсказуемому поведению в параллельно выполняемой программе, в связи с чем необходимо гарантировать исполнение критически важных команд в первоначальном порядке.
Эта работа выполняется примитивами синхронизации, предполагающими использование барьеров доступа к памяти (строки кода, которые не вычеркнуть какими-то операциями) для обеспечения согласованности и предотвращения изменения очерёдности выполнения (инструкции внутри барьеров памяти нельзя вытащить оттуда).
Пример кода
Обратимся к коду. Теперь вы сами можете проверить это недетерминированное поведение многопоточности.
#include #include #include void run(std::string threadName) < for (int i = 0; i < 10; i++) < std::string out = threadName + std::to_string(i) + «n»; std::cout > int main()
B0 A0 A1 A2 B1 A3 B2 B3 ..
В отличие от однопоточной реализации, каждое выполнение даёт разный и непредсказуемый результат (единственное, что можно сказать определённо: строки А и B упорядочены по возрастанию). Это может вызвать проблемы, когда очерёдность команд имеет значение.
#include #include #include void runA(bool if(value) < //значение всегда должно быть равным 1 std::string out = «[ » + std::to_string(i) + » ] value » + std::to_string(value) + «n»; std::cout > void runB(bool value = false; >int main() < for(int i = 0; i < 20; i++) < bool value = true; //1 std::thread tA(runA, std::ref(value), i); std::thread tB(runB, std::ref(value)); tA.join(); tB.join(); >>
.. [ 12 ] value 0 [ 13 ] value 1 [ 14 ] value 0 [ 15 ] value 0 [ 16 ] value 0 [ 17 ] value 0 [ 18 ] value 1 [ 19 ] value 0 ..
Но что здесь происходит? После того как поток А оценивает «значение» как истинное, поток B меняет его. Теперь мы внутри блока if , даже если нарушены ограничения.
Если два потока имеют доступ к одним и тем же данным (один к записи, другой — к чтению), нельзя сказать наверняка, какая операция будет выполняться первой.
Доступ должен быть синхронизирован.
Вы можете сказать: «Батюшки! Сколько всего намешано в этой статье!» Просто помните, что не надо пытаться понять всё и сразу, важно ухватить основные идеи.
Предлагаю пока что поиграть с примерами и посмотреть, как в них проявляется многопоточность. Можете подумать над другими примерами, где нужна синхронизация, и протестировать их (подсказка: потоки, удаляющие элементы из начала очереди. Не забывайте: прежде чем удалять, надо проверить, не пуста ли очередь).
План статей
В будущих статьях будут освящены следующие темы:
- Теория + Простые примеры
→ Низкоуровневые подходы
1. Мьютекс
2. std::condition_variable
3. Атомарность
→ Высокоуровневые подходы
3. Future и async
4. Промисы
5. std::packeged_task - Практика + самостоятельная работа и упражнения
Библиотека C++11 представляет стандартный механизм для синхронизации, независимый от базовой платформы, так что говорить о потоках, выполняемых в Linux и Windows, мы не будем. Тем более, что основные принципы похожи.
В следующей статье рассмотрим примитив синхронизации мьютекса и как его задействовать по максимуму.
- Языки C и C++. Где их используют и зачем?
- Почему Go прекрасно подходит для DevOps
- Что такого в языке Go?
Источник: nuancesprog.ru