Scientific journal
Научное обозрение. Педагогические науки
ISSN 2500-3402
ПИ №ФС77-57475

METHODS FOR SYNCHRONIZING THREADS IN MULTITHREADED APPLICATION IN C++

Razdobudov S.A. 1 Martyshkin A.I. 1
1 Penza State Technological University
This article provides methods for synchronizing threads in a multithreaded C++application. The main difficulty that arises when writing programs for parallel systems is synchronization of concurrently running threads. A detailed review of the publications on the subject of the study is provided. There are four common types of thread synchronization in one process or processes in one application: start-start, finish-start, start-finish and finish-finish. These basic types of relationships can be used to describe the coordination of tasks between threads and processes. There are a number of software tools that can help a programmer protect shared data and make code thread-safe. They are called synchronization primitives, among which the most common are mutexes, semaphores, conditional variables, and spin locks. A detailed description of these synchronization primitives is provided. Synchronization of threads plays an important role in multithreaded programming. In any program that operates with multiple threads, there are a number of resources that can only work with a single thread at a given time. It is impossible to avoid the use of such resources in the program, and therefore we have to think about how to most effectively organize the joint work of flows in order that they do not interfere with each other. The basic methods for synchronizing threads work like this: while one thread is working on a resource, other threads are denied access to that resource.
operating system
multithreading
programming language
multitasking
processor
thread
parallelism
compiler

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

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

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

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

На сегодняшний день, объем литературы, написанной по языку программирования C++, невероятно огромен. Каждый найдет для себя подходящий учебник по языку, вне зависимости от его уровня владения им. Стоит отметить книгу [1], написанную непосредственно автором языка С++ и являющуюся наиболее авторитетным и каноничным изложением возможностей, предоставляемых языком программирования. На страницах этой книги найдутся доказавшие свою эффективность подходы к решению разнообразных задач проектирования и программирования, а также примеры, демонстрирующие как высокий стиль программирования на ядре С++, так и современный объектно-ориентированный подход к созданию программных продуктов.

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

Книга [3] рассказывает о поддержке многопоточности в С++. Она включает в себя описания библиотеки потоков, atomics-библиотеки, модели памяти С++, блокировок и мьютексов (взаимных исключений) вместе с распространенными проблемами дизайна и отладки многопоточных приложений.

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

Так, в курсах «Принцип многопоточного программирования» и «Основы разработки на C++» рассматривается в контексте разработки сетевых и высоконагруженных систем. Главной целью данных курсов является обучение межпроцессному взаимодействию (IPC) и синхронизации потоков.

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

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

razd-1.tif

Возможные варианты синхронизации потоков/процессов

Синхронизация типа старт-старт. Одна задача может начаться раньше другой, но не позже.

Синхронизация типа финиш-старт задача A не может завершиться до тех пор, пока не начнется задача B. Этот тип отношений типичен для процессов типа родитель-потомок.

Синхронизация типа старт-финиш может считаться обратным вариантом синхронизации типа финиш-старт.

Синхронизация типа финиш-финиш. Одна задача не может завершиться до тех пор, пока не завершится другая, т.е. задача A не может финишировать до задачи B.

Существует ряд программных инструментов, которые могут помочь разработчику защитить разделяемые данные и сделать код потокобезопасным, их называют примитивами синхронизации, среди которых наиболее распространенные – мьютексы, семафоры, условные переменные и спин-блокировки [4–7]. Все они защищают часть кода, давая только определенному потоку право получать доступ к данным и блокируя остальные.

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

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

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

Задача мьютекса – обеспечить такой механизм, чтобы доступ к объекту в определенное время был только у одного потока. Если поток 1 захватил мьютекс объекта А, остальные потоки не получат к нему доступ, чтобы что-то в нем менять. До тех пор, пока мьютекс объекта А не освободится, остальные потоки будут вынуждены ждать.

С++ предоставляет нам 3 типа операций над базовыми мьютексами [8]:

1. lock – если мьютекс не принадлежит никакому потоку, тогда поток, вызвавший lock, становится его обладателем. Если же некий поток уже владеет мьютексом, то текущий поток (который пытается овладеть им) блокируется до тех пор, пока мьютекс не будет освобожден и у него не появится шанса овладеть им.

2. try_lock – если мьютекс не принадлежит никакому потоку, тогда поток, вызвавший try_lock, становится его обладателем и метод возвращает true. В противном случае возвращает false. try_lock не блокирует текущий поток.

3. unlock – освобождает ранее захваченный мьютекс.

Условные переменные, позволяют блокировать один или более потоков, пока либо не будет получено уведомление от другого потока, либо не произойдет «ложное/случайное пробуждение».

Есть две реализации условных переменных, доступных в заголовке:

1. condition_variable: требует от любого потока перед ожиданием сначала выполнить std::unique_lock;

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

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

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

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

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

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

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