C++: корутины

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

Модератор: Olej

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

Re: C++: корутины

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

Olej писал(а):
25 фев 2021, 18:36
symmetric_coroutine

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

#include <boost/coroutine/all.hpp>
#include <cstdlib>
#include <iostream>
#include <boost/bind.hpp>

typedef boost::coroutines::symmetric_coroutine< int >  coro_t; // парамет int

coro_t::call_type * c1 = 0;
coro_t::call_type * c2 = 0;

static const int lim = 4;

void foo( coro_t::yield_type & yield)
{
	for(int i = 0; i < lim; i++ )
	{
		auto val = yield.get();                        // получить параметр
		std::cout << "foo : " << i << " -> " << val << std::endl;
		yield(*c2, val + 2);                             // изменитьпараметр
	}
}

void bar( coro_t::yield_type & yield)
{
	for(int i = 0; i < lim; i++ )
	{
		auto val = yield.get();                        // получить параметр
		std::cout << "bar : " << i << " -> " << val << std::endl;
		yield(*c1, val + 2);                             // изменитьпараметр
	}
}

int main( int argc, char * argv[])
{
	coro_t::call_type coro1( foo );
	coro_t::call_type coro2( bar );
	c1 = & coro1;
	c2 = & coro2;
	coro1( 1 );                                                 // начальное значение параметра
	std::cout << "Done" << std::endl;
	return EXIT_SUCCESS;
}
Собираем так:

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ make
g++ -Wall  -pedantic -std=c++17 corout3s.cc -o corout3s -lboost_coroutine -lboost_context
И вот 2 корутины выполняются пошагово-попеременно, обмениваясь блоком данных:

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./corout3s 
foo : 0 -> 1
bar : 0 -> 3
foo : 1 -> 5
bar : 1 -> 7
foo : 2 -> 9
bar : 2 -> 11
foo : 3 -> 13
bar : 3 -> 15
Done
Очень хорошо видно как foo и bar поочерёдно перепасовывают друг-другу уравление.
Вложения
corout3s.cc
(1.17 КБ) 18 скачиваний

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

Re: C++: корутины

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

Olej писал(а):
22 фев 2021, 15:16
Для Boost::fiber они дают разительные результаты (Performance): скорость, для разных вариантов fiber, выше от 25-50 до 500-1000 раз чем: pthread, std::thread, std::async
А вот теперь - основная часть марлизонского балета :lol: : одна и та же работа (пусть самая дурная :-o ), которая выполняется классическими потоками (pthread) и корутинами, сравнительно... Естественно, что ветки выполнения работы, и в том и в другом случае (в случае корутин и не может быть по-другому!), переключаются не вытеснением (механизмами ядра системы), а передачей управления в соседнюю ветку (что-то типа std::this_thread::yield()) - то что называют кооперативная многозадачность.

Но, поскольку для всех сравнительных вариантов нужно определять опции запуска - то делаю сначала общую часть, файл common.h:

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

#include <iostream>
#include <vector>
#include <chrono>
#include <cstdlib>
#include <unistd.h>

void delay( uint64_t ones = 1000 )                     // повторяющаяся активность работы
{
	int n = 0;
	while (--ones)
		for (int i = 0; i < 100; i++)          // единичный повтор активной работы
			n = ( n + 37 ) % 31;
}

int nwork = 3,                                         // число рабочих активностей
    nserv = 5,                                         // число циклов на активность
    ones = 1000,                                       // число повторов в одном цикле
    debug_level = 0;

void opts(int argc, char *argv[])                      // опции командной строки
{
	auto halt = [argv](bool err = true)->void      // ошибка параметра? - завершение
	{
		std::cout << "usage: " << argv[0] << " [-a<активностей>] [-r<циклов>] [-d<повторов>] [-v...]" << std::endl;
		exit(err ? EXIT_FAILURE : EXIT_SUCCESS);
	};
	auto get_int = [halt](const char* optarg)->int {
		if (atoi(optarg) > 0)
			return atoi(optarg);
		halt();
		return 0;
	};
	int c;
	char sopt[] = "a:r:d:vh";
	while (-1 != (c = getopt(argc, argv, sopt )))
		switch( c ) {
			case 'a':                      // число активностей
				nwork = get_int(optarg);
				break;
			case 'r':                      // число циклов на активность
				nserv = get_int(optarg);
				break;
			case 'd':                      // задержка активной работы (повторы в цикле)
				ones = get_int(optarg);
				break;
			case 'v':
				debug_level++;
				break;
			case 'h':
				halt(false);
			default:
				halt();
		}
	if (!debug_level) return;
	std::cout << nwork << " активностей : " << nserv << " циклов по " << ones << " повторов в цикле"  << std::endl;
}
Здесь delay - это будет та "дурная" работа ... а опции и их числовые параметры и их назначение - в комментариях.
Вложения
common.h
(1.96 КБ) 19 скачиваний

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

Re: C++: корутины

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

Olej писал(а):
25 фев 2021, 22:57
которая выполняется классическими потоками (pthread) и корутинами, сравнительно...
Потоки (pthread), файл threadS.cc:

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

#include <atomic>         // std::atomic_flag
#include <thread>         // std::thread
#include "common.h"

std::atomic<bool> ready (false);                       // сигнал одновременного старта
std::atomic_flag lock_run = ATOMIC_FLAG_INIT;

void worker(int id, int ones) {
	while (!ready) { std::this_thread::yield(); }  // ожидание сигнала ready
	for (int n = 0; n < ::nserv; n++)              // число циклов работы
	{
		while (lock_run.test_and_set()) {};    // чередование потоков
		if (debug_level > 1)
			std::cerr << "thread: #" << std::this_thread::get_id() << " id=" << id << " loop=" << n << std::endl;
		delay(ones);
		lock_run.clear();
//		std::this_thread::yield();
	}
}

int main(int argc, char *argv[])
{
	opts(argc, argv);                              // опции командной строки
	std::vector<std::thread> threads;
	auto begin = std::chrono::steady_clock::now();
	for (int i = 1; i <= nwork; ++i) threads.push_back(std::thread(worker, i, ones));
	ready = true;
	for (auto& th : threads) th.join();
	auto end = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
        std::cout << "полное время (мксек.) = " << elapsed.count() / 1000 << "."
                  << elapsed.count() % 1000 << std::endl;
}
Вложения
threadS.cc
(1.37 КБ) 19 скачиваний

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

Re: C++: корутины

Непрочитанное сообщение Olej » 25 фев 2021, 23:11

Olej писал(а):
25 фев 2021, 22:57
которая выполняется классическими потоками (pthread) и корутинами
Корутины fibers (корутины coroutines, которые могут быть ещё выиграшнее я просто ещё не успел сделать, времени нет), файл fiberS.cc:

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

#include <boost/fiber/all.hpp>
#include "common.h"

auto fn = [](int id, int ones)->void {
	for (int n = 0; n < ::nserv; n++ )             // число циклов работы
	{
		if (debug_level > 1)
			std::cerr << "fiber: " << boost::this_fiber::get_id() << " id=" << id << " loop=" << n << std::endl;
		delay( ones );
		boost::this_fiber::yield();
	}
};

int main(int argc, char *argv[])
{
	opts(argc, argv);                              // опции командной строки
	try {
		std::vector<boost::fibers::fiber> fibers;
		auto begin = std::chrono::steady_clock::now();
		for (int i = 1; i <= nwork; ++i)
				fibers.push_back(boost::fibers::fiber(fn, i, ones));
		for (auto& fb : fibers) fb.join();
		auto end = std::chrono::steady_clock::now();
		auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
		std::cout << "полное время (мксек.) = " << elapsed.count() / 1000 << "."
		          << elapsed.count() % 1000 << std::endl;
		return EXIT_SUCCESS;
	} catch ( std::exception const& e) {
		std::cerr << "exception: " << e.what() << std::endl;
	} catch (...) {
		std::cerr << "unhandled exception" << std::endl;
	}
	return EXIT_FAILURE;
}
Вложения
fiberS.cc
(1.18 КБ) 22 скачивания

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

Re: C++: корутины

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

Сборка (у меня это Makefile, но видно по выводу команды):

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ make
g++ -Wall  -pedantic -std=c++17 -pthread threadS.cc -o threadS 
g++ -Wall  -pedantic -std=c++17 fiberS.cc -o fiberS -lboost_fiber -lboost_context
Запуски...

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./threadS -z
./threadS: invalid option -- 'z'
usage: ./threadS [-a<активностей>] [-r<циклов>] [-d<повторов>] [-v...] [-h]

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./threadS -h
usage: ./threadS [-a<активностей>] [-r<циклов>] [-d<повторов>] [-v...] [-h]
Деффаултные опции:

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./threadS -v
3 активностей : 5 циклов по 1000 повторов в цикле
полное время (мксек.) = 61033.325
Вот так, при любом наборе опций, можем посмотреть (проверить) как чередуются активности:

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./threadS -vv
3 активностей : 5 циклов по 1000 повторов в цикле
thread: #140096540047104 id=2 loop=0
thread: #140096531654400 id=3 loop=0
thread: #140096531654400 id=3 loop=1
thread: #140096540047104 id=2 loop=1
thread: #140096540047104 id=2 loop=2
thread: #140096540047104 id=2 loop=3
thread: #140096548439808 id=1 loop=0
thread: #140096540047104 id=2 loop=4
thread: #140096531654400 id=3 loop=2
thread: #140096548439808 id=1 loop=1
thread: #140096531654400 id=3 loop=3
thread: #140096548439808 id=1 loop=2
thread: #140096548439808 id=1 loop=3
thread: #140096531654400 id=3 loop=4
thread: #140096548439808 id=1 loop=4
полное время (мксек.) = 68653.616

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./fiberS -vv
3 активностей : 5 циклов по 1000 повторов в цикле
fiber: 0x7f6d58408f00 id=1 loop=0
fiber: 0x7f6d583c6f00 id=2 loop=0
fiber: 0x7f6d583a5f00 id=3 loop=0
fiber: 0x7f6d58408f00 id=1 loop=1
fiber: 0x7f6d583c6f00 id=2 loop=1
fiber: 0x7f6d583a5f00 id=3 loop=1
fiber: 0x7f6d58408f00 id=1 loop=2
fiber: 0x7f6d583c6f00 id=2 loop=2
fiber: 0x7f6d583a5f00 id=3 loop=2
fiber: 0x7f6d58408f00 id=1 loop=3
fiber: 0x7f6d583c6f00 id=2 loop=3
fiber: 0x7f6d583a5f00 id=3 loop=3
fiber: 0x7f6d58408f00 id=1 loop=4
fiber: 0x7f6d583c6f00 id=2 loop=4
fiber: 0x7f6d583a5f00 id=3 loop=4
полное время (мксек.) = 49356.876

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

Re: C++: корутины

Непрочитанное сообщение Olej » 25 фев 2021, 23:24

Olej писал(а):
25 фев 2021, 23:18
Вот так, при любом наборе опций, можем посмотреть (проверить) как чередуются активности:
А вот теперь то, для чего сравнение делалось - время выполнения больших объёмов при большом числе параллельных активностей:

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./threadS -a30 -r100 -d500
полное время (мксек.) = 12109615.373

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

olej@nvme:~/2021/OWN_TEST.codes/coroutine$ ./fiberS -a30 -r100 -d500
полное время (мксек.) = 1034172.515
Время выполнение корутин (при таких параметра запуска) в 12 раз (!) меньше,чем при переключении активностей переключая контекст ядром! .. больше чем на порядок разница.
А при других параметрах разница может быть и существенно больше.

P.S. Вот что такое "кооперативная многозадачность" ;-) ... тогда когда задачу можно сформулировать в её терминологии :-? (и вот как переключает, но не только так!, свои параллельные ветки язык Go).

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

Re: C++: корутины

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

Olej писал(а):
25 фев 2021, 23:11
(корутины coroutines, которые могут быть ещё выиграшнее я просто ещё не успел сделать, времени нет)
А вот и оно ... это наш 3-й компонент в сравнении.
Здесь код будет самый простой по форме и лаконичный ... но самый сложный для понимания :lol:

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

#include <boost/coroutine/all.hpp>
#include <vector>
#include "common.h"

typedef boost::coroutines::symmetric_coroutine<int>  coro_t;  // параметр int
std::vector<coro_t::call_type*> vpcoro;

void worker(coro_t::yield_type& yield){
	for(int n = 0; n < ::nserv; n++ )
	{
		int val = yield.get(),                        // извлечь номер следующего
		    next = (val + 1) % vpcoro.size();         // и кому ему передать управление
		if (debug_level > 1)
			std::cerr << "coroutine: " <<  vpcoro[val] << " id: " << val << "->" << next
			          << " loop=" << n << std::endl;
		delay(ones);
		yield(*vpcoro[val], next);                    // передать следующему
	}
}

int main( int argc, char * argv[])
{
	opts(argc, argv);                                     // опции командной строки
	auto begin = std::chrono::steady_clock::now();
	for (int i = 0; i < nwork; i++)
		vpcoro.push_back(new coro_t::call_type(worker));
	(*vpcoro[0])(1);                                      // старт 1-й корутины
	auto end = std::chrono::steady_clock::now(); 
	auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
	std::cout << "полное время (мксек.) = " << elapsed.count() / 1000 << "."
	          << elapsed.count() % 1000 << std::endl;
}
- корутине, с типом параметра int, передаётся в качестве этого int - номер корутины (в векторе их таких vpcoro) которой управление нужно передать следующей (выполнив свою работу) - это значение val в функции корутины;
- она же вычисляет "подсказку" для корутины val какой номер той отдавать управление далее - next.

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

olej@nvidia:~/2021_WORK/OWN_TEST.codes/coroutine$ make
g++ -Wall  -pedantic -std=c++17 coroutS.cc -o coroutS -lboost_coroutine -lboost_context
С отладочной диагностикой это выглядит так:

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

olej@nvidia:~/2021_WORK/OWN_TEST.codes/coroutine$ ./coroutS -vv
3 активностей : 5 циклов по 1000 повторов в цикле
coroutine: 0x55b25c534550 id: 1->2 loop=0
coroutine: 0x55b25c534530 id: 2->0 loop=0
coroutine: 0x55b25c524500 id: 0->1 loop=0
coroutine: 0x55b25c534550 id: 1->2 loop=1
coroutine: 0x55b25c534530 id: 2->0 loop=1
coroutine: 0x55b25c524500 id: 0->1 loop=1
coroutine: 0x55b25c534550 id: 1->2 loop=2
coroutine: 0x55b25c534530 id: 2->0 loop=2
coroutine: 0x55b25c524500 id: 0->1 loop=2
coroutine: 0x55b25c534550 id: 1->2 loop=3
coroutine: 0x55b25c534530 id: 2->0 loop=3
coroutine: 0x55b25c524500 id: 0->1 loop=3
coroutine: 0x55b25c534550 id: 1->2 loop=4
coroutine: 0x55b25c534530 id: 2->0 loop=4
coroutine: 0x55b25c524500 id: 0->1 loop=4
полное время (мксек.) = 370.78
А сравнительные времена выполнения:

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

olej@nvidia:~/2021_WORK/OWN_TEST.codes/coroutine$ ./threadS -a50 -r30 -d200
полное время (мксек.) = 1926346.606

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

olej@nvidia:~/2021_WORK/OWN_TEST.codes/coroutine$ ./fiberS -a50 -r30 -d200
полное время (мксек.) = 226150.404

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

olej@nvidia:~/2021_WORK/OWN_TEST.codes/coroutine$ ./coroutS -a50 -r30 -d200
полное время (мксек.) = 228704.229
При коротких задержках на активностях (малые значения -d), т.е. частых переключениях между ветвями, как следовало ожидать, корутины практически на порядок быстрее справляются, чем вытесняющая многозадачность управляемая ядром системы.

P.S. В приложенных файлах common.h и предыдущие 2 варианта - это только повторы того что было (возможно есть минимальные изменения, но это только для единообразия), а вот новое здесь, понятно - coroutS.cc
Вложения
common.h
(1.97 КБ) 18 скачиваний
threadS.cc
(1.35 КБ) 18 скачиваний
fiberS.cc
(1.18 КБ) 17 скачиваний
coroutS.cc
(1.34 КБ) 17 скачиваний

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

Re: C++: корутины

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

Относительно того, как это видят именно стандарты, C++ и далее, есть такой вот обзор конца 2020 года: Корутины в C++20. Часть 1
Корутины в C++20 асимметричные симметричные, первого класса (first-class) и бесстековые (stackless).
Асимметричные корутины возвращают контекст выполнения вызывающей стороне. Напротив, симметричные корутины делегируют последующее выполнение другой корутине.
Корутины первого класса идентичны функциям первого класса потому что корутины могут вести себя как данные. Аналогичное данным поведение означает, что корутины могут быть аргументами или возвращаемыми значениями функций или храниться в переменных.
Бесстековые корутины позволяют приостанавливать или возобновлять работу корутин более высокого уровня. Выполнение корутин и приостановка в корутине возвращает выполнение вызывающей стороне. Бесстековые корутины часто называют возобновляющими работу функциями (resumable functions).
Стандарт C++20 уже имеет 2 определения awaitables: std::suspend_always и std::suspend_never.
Вторая часть здесь: Корутины в C++20. Часть 2.
Мне она интересна оказалась только тем, что содержит несколько реальных компилирующихся примеров ... и в частности команда компиляция:

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

$ g++ -fcoroutines infiniteDataStream.cpp

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

Re: C++: корутины

Непрочитанное сообщение Olej » 27 фев 2021, 21:33

Olej писал(а):
27 фев 2021, 20:56
Относительно того, как это видят именно стандарты, C++ и далее, есть такой вот обзор конца 2020 года: Корутины в C++20. Часть 1
И вот ещё одна сердитая статья, объясняющая почему так сложно происходит со стандартами C++:
Возражения против принятия Coroutines с await в C++17
1 марта 2016 в 13:34
Этой модели авторы противопоставляют так называемую «suspend-down» модель, которая не является инвазивной и используется в Boost.Coroutines. Она также предлагалась в комитет, «Resumable Expression» от Christopher Kohlhoff, но предпочтение было отдано С#-подобному await.
Отличие этой модели в том, что не требуется помечать асинхронные вызовы словом await. Вызывается сущность, которая «снаружи выглядит как обычная функция», например, suspend(). Она внутри осуществляет переключение контекста (Boost.Coroutines использует Boost.Context) без выхода из вызывающего метода и «подвешивает» весь стек вызовов до завершения асинхронной операции.
3) Далее идет интересный аргумент в стиле «столкновение миров»: Linux — это наиболее распространенная платформа для высокопроизводительных систем, поэтому пробная реализация сопрограмм для MSVC/Windows мало что дает для оценки производительности.
Предложения

1) Отложить включение сопрограмм в С++17 и включить пока в Technical Specification для «обкатки».
...
3) Текущее предложение по сопрограммам в значительной степени получило преимущество потому, что Гор Нишанов имел возможность экспериментировать и дорабатывать компилятор Visual Studio в Microsoft. Поэтому авторы альтернативной модели сопрограмм ожидают сотрудничества с разработчиками GCC и Clang для доработки и экспериментов на высоконагруженных Linux системах.

Ответить

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

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

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