C++: параллельность, асинхронность, атомарность

Вопросы написания собственного программного кода (на любых языках)

Модератор: Olej

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

C++: параллельность, асинхронность, атомарность

Непрочитанное сообщение Olej » 07 фев 2021, 15:36

Очень интересная тема...
Про параллелизм написано во множестве ... лет за 50 последних (начиная с фундаментальной публикации Э.Дейкстра откуда всё началось).
Но параллелизм, грубо, это про то "как" организовать, а асинхронность - про то "зачем" организовать, базируясь на параллелизме.

Начинаю собирать интересные (то как мне показалось) публикации об асинхронности... Читайте - это интересно!

Асинхронность в программировании
3 апреля 2019 в 14:35
В основе материала — расшифровка доклада Ивана Пузыревского, преподавателя школы анализа данных Яндекса.
Изображение
Добро пожаловать в параллельный мир. Часть 2: Мир асинхронный
3 июня 2012
перейдём от простейшего способа использования потоков, к следующему, более продвинутому, способу мульти-поточного программирования - асинхронному программированию.
Синхронная асинхронность в C++
10.09.18 в 10:24
Изображение

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: С++ в относительно новых стандартах

Непрочитанное сообщение Olej » 17 фев 2021, 18:55

Следующая часть нашего марлизонского балета: <atomic>, атомарные операции ... это надолго и всерьёз!

Это формальные спецификации (документация) и если возникают какие-то неявности, то справляться нужно здесь:
std::atomic
std::atomic

А это более-менее внятные описания "на пальцах" ... из числа того что мне приходилось видеть:

1. Добро пожаловать в параллельный мир. Часть 3: Единый и Неделимый
28. августа 2012
Это довольно старая публикация ... но там я нашёл некоторые вещи, которых нет в других местах.

2. std::atomic. Модель памяти C++ в примерах
7 сентября 2020
А вот это объясняет почему "надолго и всерьёз" и почему недостаточно оказалось простого перенесения механизмов POSIX API, типа std::mutex и других, которые и так присутствуют...
Задача предоставить разные гарантии по памяти решалась по-разному для разных архитектур процессоров. Наиболее популярные архитектуры x86-64 и ARM имеют разные представления о том, как синхронизировать память.

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

Отсюда следует несколько важных выводов: модель синхронизации памяти C++ — это "искусственные" правила, которые учитывают особенности различных архитектур процессоров.
По крайней мере "помимо прочего" ;-) , но очень важно ...

Там есть один очень интересный пример как по-разному компилируются в ассемблер операции не атомарного и атомарного инкремента:

Код: Выделить всё

static int v1 = 0;
static std::atomic<int> v2{ 0 };

int add_v1() {
    return ++v1;
    /* Generated x86-64 assembly:
        mov     eax, DWORD PTR v1[rip]
        add     eax, 1
        mov     DWORD PTR v1[rip], eax
    */
}

int add_v2() {
    return v2.fetch_add(1);
    /* Generated x86-64 assembly:
        mov     eax, 1
        lock xadd       DWORD PTR _ZL2v2[rip], eax
    */
}

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: С++ в относительно новых стандартах

Непрочитанное сообщение Olej » 17 фев 2021, 19:02

Olej писал(а):
17 фев 2021, 18:55
Следующая часть нашего марлизонского балета: <atomic>
Пока только то, как это может выглядеть синтаксически ... в этом коде нет ни смысла, ни потоков вообще ... но это отработка синтаксиса флага такого типа в конкретном коде по проекту:

Код: Выделить всё

#include <iostream>
#include <atomic>
// https://www.cplusplus.com/reference/atomic/

class XXX
{
public:
	XXX(std::string s) : need_dump(false), content(s) {};
	bool change()
	{
		bool ret = need_dump.load();
		need_dump.store(!ret, std::memory_order_relaxed);
		return ret;
	}
	std::string dump(void)
	{
		if( need_dump.load() ) return content;
		else return "вывод содержимого не разрешён";
	}
private:
	std::atomic_bool need_dump;
	std::string content;
};

int main()
{
	XXX x("заполненная структура");
	for( int i = 0; i < 5; i++ )
		std::cout << (x.change() ? "T " : "F ");
	std::cout << std:: endl;
}

Код: Выделить всё

olej@nvidia:~/2021_WORK/OWN_TEST.codes/atomic$ make
g++ -Wall -pedantic -std=c++14 atomic_bool.cc -o atomic_bool

Код: Выделить всё

olej@nvidia:~/2021_WORK/OWN_TEST.codes/atomic$ ./atomic_bool 
F T F T F 
Вложения
atomic_bool.cc
(654 байт) 20 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: С++ в относительно новых стандартах

Непрочитанное сообщение Olej » 18 фев 2021, 00:15

Olej писал(а):
17 фев 2021, 18:55
Следующая часть нашего марлизонского балета: <atomic>, атомарные операции
Классический пример с многозадачностью (многопроцессорностью), непосредственно из руководства по C++, но с использованием атомарных переменных и операций:

Код: Выделить всё

#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::atomic_flag, ATOMIC_FLAG_INIT
#include <thread>         // std::thread, std::this_thread::yield
#include <vector>         // std::vector
// https://www.cplusplus.com/reference/atomic/atomic/atomic/

std::atomic<bool> ready (false);
std::atomic_flag winner = ATOMIC_FLAG_INIT;

void count1m (int id) {
        while (!ready) { std::this_thread::yield(); }  // wait for the ready signal
        for (int i=0; i<1000000; ++i) {}                    // go!, count to 1 million
        if (!winner.test_and_set()) {
                std::cout << "поток #" << id << " выиграл!" << std::endl;
        }
}

int main ()
{
        std::vector<std::thread> threads;
        std::cout << "создание 10 потоков, считающих до миллиона наперегонки..." << std::endl;
        for (int i=1; i<=10; ++i) threads.push_back(std::thread(count1m, i));
        ready = true;
        for (auto& th : threads) th.join();
}

Код: Выделить всё

olej@nvme:~/2021/OWN_TEST.codes/atomic$ make
clang++ -xc++ -pthread -std=c++20  -pthread thread1.cc -o thread1
Компиляция показана с Clang LLVM, но это не имеет никакого значения в данном случае:

Код: Выделить всё

olej@nvme:~/2021/OWN_TEST.codes/atomic$ ./thread1 
создание 10 потоков, считающих до миллиона наперегонки...
поток #9 выиграл!

Код: Выделить всё

olej@nvme:~/2021/OWN_TEST.codes/atomic$ ./thread1 
создание 10 потоков, считающих до миллиона наперегонки...
поток #1 выиграл!
И каждый запуск результат будет - разный. :-o
Такой вот своеобразный ... "генератор случайных чисел".
Вложения
thread1.cc
(955 байт) 20 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: С++ в относительно новых стандартах

Непрочитанное сообщение Olej » 18 фев 2021, 04:44

Olej писал(а):
17 фев 2021, 18:55
А вот это объясняет почему "надолго и всерьёз" и почему недостаточно оказалось простого перенесения механизмов POSIX API, типа std::mutex и других, которые и так присутствуют...
Интересная стать (перевод), хоть и не новенькая, которая показывает почему на самых разных процессорных архитектурах даже самые элементарные операции могут быть неатомарными - Атомарные и неатомарные операции:
3 декабря 2014 в 12:31
В этой статье я сравню атомарные загрузки и сохранения с их неатомарными аналогами на уровне процессора и компилятора C/C++. По ходу статьи мы также разберемся с концепцией «состояния гонок» с точки зрения стандарта C++11.

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

На практике мы обычно обладаем некоторой информацией о платформах, для которых создается код. Например, мы обычно знаем, что на всех современных процессорах x86, x64, Itanium, SPARC, ARM и PowerPC обычное 32-битное присваивание атомарно в том случае, если переменная назначения выровнена. В этом можно убедиться, перечитав соответствующий раздел документации процессора и/или компилятора. Я могу сказать, что в игровой индустрии атомарность очень многих 32-битных присваиваний гарантируется этим конкретным свойством.
Ну, в общем - интересно, читайте...

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++: параллельность, асинхронность и атомарность

Непрочитанное сообщение Olej » 20 фев 2021, 15:12

Olej писал(а):
18 фев 2021, 00:15
Классический пример с многозадачностью
В отношении самих потоков в C++, по идее, не должно возникать никаких вопросов? ... потому что просто это наследование модели из C стандарта POSIX - с некоторыми своими внешними по форме, отличиями.
Для напоминания (и там есть тонкие, не очень известные, детали) вот такая публикация (хоть и не самая свежая) - Потоки, блокировки и условные переменные в C++11 [Часть 1]
8 июня 2013 в 19:01

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++: параллельность, асинхронность и атомарность

Непрочитанное сообщение Olej » 20 фев 2021, 15:29

Olej писал(а):
20 фев 2021, 15:12
это наследование модели из C стандарта POSIX
Главное бросающееся в глаза отличие - это то что функции потока передаётся сколько угодно параметров списком:
Однако принять функция может любое количество параметров.

Код: Выделить всё

void threadFunction(int i, double d, const std::string &s)
...
std::thread thr(threadFunction, 1, 2.34, "example");
...
В отличие от POSIX, где сколько угодно параметров предварительно упаковывается в блок данных здесь это упаковывание неявно делает языковая система.
Olej писал(а):
20 фев 2021, 15:12
там есть тонкие, не очень известные, детали
А вот это очень важное здесь напоминание:
Несмотря на то, что передавать можно любое число параметров, все они были переданы по значению Если в функцию необходимо передать параметры по ссылке, они должны быть обернуты в std::ref или std::cref, как в примере:

Код: Выделить всё

void threadFunction(int &a)
{
     a++;
}
 
int main()
{
     int a = 1;
     std::thread thr(threadFunction, std::ref(a));
     thr.join();
     std::cout << a << std::endl; 
     return 0;
}
Программа напечатает в консоль 2. Если не использовать std::ref, то результатом работы программы будет 1.

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++: параллельность, асинхронность и атомарность

Непрочитанное сообщение Olej » 20 фев 2021, 17:03

Olej писал(а):
20 фев 2021, 15:12
В отношении самих потоков в C++, по идее, не должно возникать никаких вопросов?
Вот, местами, интересная публикация (с которой не везде и не во всём можно соглашаться) - 20 типичных ошибок многопоточности в C++
16.05.18 в 20:30
Но некоторые любопытные:
#6 Забыть вызвать unlock()
...
Чтобы защититься от ошибок такого рода воспользуемся std::lock_guard, который манипулирует временем жизни блокировки в стиле RAII.

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

Код: Выделить всё

void foo(const std::string &message)
{
    std::lock_guard<std::mutex> lock(cout_guard);
    std::cout << "thread " << std::this_thread::get_id() << ", message "
    ...
#10 Излишняя предосторожность
Когда возникает необходимость модифицировать простые типы наподобие bool или int использование 'std::atomic' почти всегда более эффективно в сравнении с использованием mutex.
#13 Имитация асинхронной работы без std::async
Когда нужно выполнить часть кода независимо от основного потока отличным выбором будет использование std::async для запуска. Это тоже самое, что создать ещё один поток и передать ему на выполнение функцию или лямбду. Правда при этом за жизненным циклом потока и исключений в нём тоже придётся следить самостоятельно. В случае использования std::async можно не заботиться об этом да ещё и существенно сократить вероятность блокировки.

Еще одно важно преимущество заключается в возможности получить результат работы функции через std::future. Функция int foo(), будучи выполнена как асинхронная задача, заранее установит результат своей работы. А получим мы его тогда, когда нам это будет удобно.

Код: Выделить всё

#include <iostream>
#include <cmath>
#include <future>

int main(int argc, char *argv[])
{
    auto f = std::async(sqrt, 9.0);

    std::cout << f.get() << std::endl;

    return 0;
}

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++: параллельность, асинхронность, атомарность

Непрочитанное сообщение Olej » 22 фев 2021, 02:40

Olej писал(а):
07 фев 2021, 15:36
Про параллелизм написано во множестве
Кстати, как я вспомнил ... кстати :lol: - я ещё в 2014г. написал рукопись-черновик книги "Параллелизм, конкурентность, многопроцессорность в Linux", 97 стр. текста с примерами кода - там много не очевидных подробностей.
Кому интересно, может найти и текст и архив примеров здесь: Параллелизм, конкурентность, многопроцессорность в Linux
вторник, 19 июня 2018 г.
:-o ... ну, значит я позже правки вносил. ;-)

Там, в частности, есть примеры того, как параллельное исполнение записывается в коде Go.

P.S. А для себя, чтоб было под рукой здесь, я просто целиком скопирую сюда текст рукописи.
P.P.S. И архив кодов примеров позже нашёл и добавил.
P.P.P.S. Это написано а). применительно к POSIX API и механизмам Linux и б). году в 2014-м, 7 лет назад, ... пропасть. Очень интересно на всё это взглянуть "7 лет спустя" и как это преломляется через формализмы C++ скрывающие платформенно зависимость.
Вложения
SMP_05.odt
(180.81 КБ) 21 скачивание
examples.SMP.05.tgz
(307.37 КБ) 19 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 14939
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++: параллельность, асинхронность, атомарность

Непрочитанное сообщение Olej » 05 мар 2021, 06:05

Olej писал(а):
17 фев 2021, 18:55
Следующая часть нашего марлизонского балета: <atomic>, атомарные операции
Очень интересный цикл статей (штук 11 - в конце публикации их ссылки) относительно аппаратных проблем атомарности, и что всё там совсем не так просто: Lock-free структуры данных. Основы: Атомарность и атомарные примитивы
8 октября 2013 в 10:00
Современные архитектуры процессоров делятся на два больших лагеря – одни поддерживают в своей системе команд CAS-примитив, другие – пару LL/SC. CAS реализован в архитектурах x86, Intel Itanium, Sparc; впервые примитив появился в архитектуре IBM System 370. Пара LL/SC – это архитектуры PowerPC, MIPS, Alpha, ARM; впервые предложена DEC. Стоит отметить, что примитив LL/SC реализован в современных архитектурах не самым идеальным образом: ...
Производительность
А как обстоит дело с производительностью атомарных примитивов?
...
В этой работе авторы, помимо прочего, сравнивают длительность атомарного инкремента и примитива CAS с длительностью операции nop (no-operation). Итак, для Intel Xeon 3.06 ГГц (образца 2005 года) атомарный инкремент имеет длительность 400 nop, CAS – 850 – 1000 nop. Процессор IBM Power4 1.45 ГГц: 180 nop для атомарного инкремента и 250 nop для CAS. Измерения довольно старые (2005 год), за прошедшее время архитектура процессоров сделала ещё несколько шагов вперед, но порядок цифр, я думаю, остался прежним.
Это вот такая плата за атомарность!
Справедливости ради стоит заметить, что, по моим ощущениям, с каждым новым поколением архитектуры Intel Core примитив CAS становится всё быстрее, видимо, инженеры Intel немало сил вкладывают в совершенствование микроархитектуры.
А это повод задуматься к вопросу: "А нужно ли мне менять на новый процессор, если там всего на 20% выше тактовая частота?" :lol:

Ответить

Вернуться в «Программирование»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя