Pull to refresh

Comments 8

Как то не увидел из статьи, да и из кода простой идеи: можно взять отдельно какие то написанные корутины и просто использовать их.

А библиотека должна предоставить только операторы co await для своих типов или функции возвращающие awaiter

Например, насколько я понял в движке куча глобальных штук типа менеджера таймеров, таким образом чтобы подождать какое то время в корутине нужно только создать awaiter который в await suspend в глобального менеджера регистрирует по таймеру coroutine handle как калбек

И в await resume не делает ничего

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

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

На каждый чих? В статье за awaiter выступает использование Future. Можно писать функции, которые возвращают Future и использовать в тасках с возвращаемым значением.

В плагине UE5Coro до момента написании статьи не было возможности использования co_return (т.е. не было нормальных тасков), а в реализации от EpicGames их таски предназначены для многопоточного исполнения без возможности их запуска в основном потоке.

Это говорит лишь о плохой реализации корутин. К сожалению многие компании/люди ещё не поняли как это должно выглядеть.(см. boost asio с очень странной и корявой реализацией)
Корутины практически никак не должны зависеть ни от задач которые исполняют, ни от исполнителя, ни от наличия/отсутствия многопоточности.

Для себя конечно использовал свои, но это мой личный эксперимент (https://github.com/kelbon/kelcoro)

Насчет future не понял посыла, ведь эффективнее и вероятно удобнее делать что-то такое:

struct time_awaiter {
  std::chrono::milliseconds ms;
  bool await_ready() const { return false; }
  void await_suspend(std::coroutine_handle<> handle) const {
    GetWorld().Timers().ExecuteAfter(ms, handle);
  }
  void await_resume() {}
};

Далее достаточно это вернуть из opeartor co_await для Delay или ещё какой-то функции(возможно функции менеджера таймеров сразу же)

И работать это будет с любой достаточно хорошо написанной корутиной(кроме чего-то типа однопоточного генератора, ведь в нём co_await с suspend это логическая ошибка)

Да по сути тот же Future, только в левой руке.

Future (тот же awaitable), на мой взгляд, даёт возможность инкапсулировать использование coroutine_handle и ряда специальных методов:

По SetResult вызываем resume тем самым передавая выполнение обратно с подкидыванием возвращаемого значения в await_resume. Функция, которая возвращает Future инициирует какое-либо отложенное действие, а затем, в колбеке (у меня это лямбда) выставляет результат. Ну как вариант.

Не позиционирую себя как человека, который точно знает как должно быть (технология относительно новая в C++ и, насколько слышал, спорная), но я тоже стараюсь обобщить на разные юзкейсы. Хотя тут есть некоторая привязка к фреймворку, так как использую UE делегаты, для того, чтобы аудитории понятнее было. В остальном, данная реализация даёт возможность писать асинхронный код используя только типы CoroTasks::TTask<> и CoroTasks::TFuture<> , а далее просто манипулировать ими. Ну и речь об однопоточной асинхронщине, так как её предоставляет сам движок (многопоточное вполнение он сам синкает в MainThrd), позволяя вешаться на неё пользовательскими колбеками.

Потому что C++20 дают не возможность написать готовую корутину, а возможность накидать себе готовый фреймворк для написания корутин. Эдакая сверх-абстракция. Может, в будущих стандартах докинут стандартных реализаций некоторых общих вещей (тот-же task, например)

А что мешает писать по старинке. С помощью loop-ов: int loop(context); которая выполняет часть работы, меняет состояние и отдаёт управление обратно, возвращая 0-если закончила и не 0 если еще не закончила.
В отличии от нового чудного механизма коротин, где всё прекрасно. В таком методе у вас есть полный контроль над происходящим и можно даже сериализовать состояние и продолжить после загрузки. Можно набирать очереди и выстраивать pipe-line-ы. Из плюсов можно даже без плюсов использовать.
Для удобства написания таких конечный автоматов можно использовать аналогию с трассами на которых расставлены контрольные точки. Каждый участок выполняется атомарно. Есть конечно некоторые оговорки, но если их соблюдать можно писать даже на C примерно так:

track-fn.h
/* track-fn.h */
#pragma once

enum TrackConsts { track_no_limit=-1, track_start_line=0, track_end_line=-1 };
enum TrackResultCodes { track_rc_done=0, track_rc_active=1, track_rc_int=2 };

typedef struct Track { int line,limit; } Track;

#define TRACK_RESET(v) { Track *_track=(v); _track->line=0; _track->limit=1; }
#define TRACK_SET_LIMIT(track,n) { (track)->limit=n; }

#define TRACK_BEGIN(v) { Track *_track=(v); track_begin: \
    switch(_track->line) { default: case 0: TRACK_POINT
#define TRACK_POINT { case __LINE__: _track->line=__LINE__; \
    if (_track->limit>=0 && !_track->limit--) { _track->limit=1; return 1; } }
#define TRACK_END   track_end: case -1: _track->line=-1; return 0; } \
    track_interrupt: return 2; }

#define TRACK_END_R track_end: _track->line=-1; _track->limit=1; return 0; } \
    track_interrupt: return 2; }

#define TRACK_REPEAT_LAST goto track_begin;
#define TRACK_LEAVE       goto track_end;
#define TRACK_INTERRUPT   goto track_interrupt;

#define TRACK_CALL(fn,state) { int rc; \
    for(fn##_reset(state);0!=(rc=fn(state));) { \
        if (rc==track_rc_int) { TRACK_INTERRUPT } else { TRACK_POINT } \
    }}


/* tracks.c */
#include <stdio.h>
#include "track-fn.h"

typedef struct {
    Track track[1];
    int in0;
} fn1_state;
void fn1_reset(fn1_state *self) { TRACK_RESET(self->track); }

int fn1(fn1_state *self) {
    TRACK_BEGIN(self->track)
        printf("\tfn1.1\t%d\n",self->in0);
        TRACK_POINT
        printf("\tfn1.2\t%d\n",self->in0);
    TRACK_END
}

typedef struct {
    Track track[1];
    fn1_state fn1[1]; 
    int i;
} fn2_state;
void fn2_reset(fn2_state *self) { TRACK_RESET(self->track); }

int fn2(fn2_state *self) {
    TRACK_BEGIN(self->track)
        printf("fn2.1\n");
        TRACK_POINT
        for(self->i=1;self->i<=3;self->i++) {
            self->fn1->in0=self->i; /* function input arg */
            TRACK_CALL(fn1,self->fn1)
        }
        TRACK_POINT 
        printf("fn2.2\n");
    TRACK_END
}

int main(int argc, char const *argv[]) {    
    fn2_state s[1];
    fn2_reset(s);

    s->track->limit=3; fn2(s);
    printf("--\n");
    s->track->limit=track_no_limit; fn2(s);
    return 0;
}

"Почему бы не сделать один класс? Зачем специализация?"

Кстати, да, игрался с этим пол года назад - не пропускает если использовать std::enable_if<...> для выбора нужного метода. Концепты, вроде, раньше шаблонов резолвятся

С концептами тоже не выйдет. Даже если добавить функциям return_value и return_void requires, то при компиляции ругнётся: C3782 (обещание сопрограммы не может содержать return_value и return_void одновременно):

	template<typename T>
	concept CIsNotVoid = !TIsSame<T, void>::Value;
		
	template<typename T>
	concept CIsVoid = TIsSame<T, void>::Value;

	template<typename ReturnType, typename TaskType>
	struct TPromise_Base : FPromise_Exception
	{
		TMulticastDelegate<void(ReturnType)> OnDone;

		template<typename T = ReturnType>
		void return_value(T Result) requires CIsNotVoid<T>
		{
			if (CurrentException)
			{
				OnException.ExecuteIfBound(CurrentException);
				OnException.Unbind();
			}
			else if (OnDone.IsBound())
				OnDone.Broadcast(Result);
		}
	
		template<typename T = ReturnType>
		void return_void() const requires CIsVoid<T>
		{
			if (CurrentException)
			{
				OnException.ExecuteIfBound(CurrentException);
				OnException.Unbind();
			}
			else if (OnDone.IsBound())
				OnDone.Broadcast();
		}
	};

Sign up to leave a comment.

Articles