На мой взгляд, корутины (сопрограммы) - самое трудное для использования нововведение, появившиеся в стандарте С++20. Несмотря на то, что по данной теме имеется большое количество статей на Хабре, например один, два, три, а также существует много видеоматериалов с выступлений на различных конференциях, при изучении сопрограмм в С++20 сталкиваешься с большим количеством проблем, обусловленных несколькими причинами. 

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

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

В поисках ответов на эти вопросы я потратил много времени и выяснил, что писать код с использованием сопрограмм можно было задолго до появления С++20. Для этого можно даже не использовать С++, а вести разработку на языке Си. Теперь я  хочу поделиться своим опытом с читателями. Надеюсь, он будет полезен тем разработчикам как на C++, так и на других языках, которые, как и я, всегда стараются понять как та или иная технология может быть реализована изнутри. Лучший способ добиться такого понимания - это попытаться на практике самостоятельно создать механики, лежащие в основе технологии корутин.  Так как накоплено большое количество материала, то я планирую серию статей, в которых постараюсь ответить на заданные вопросы и поделиться своим опытом их решения. Итак, приступим. 

Для начала вспомним определение. Сопрограмма — программный модуль, особым образом организованный для обеспечения взаимодействия с другими модулями по принципу кооперативной многозадачности: модуль приостанавливается в определённой точке, сохраняя полное состояние (включая стек вызовов и счётчик команд), и передаёт управление другому. Тот, в свою очередь, выполняет задачу и возвращает управление обратно, сохраняя свое состояние.  Применительно к корутинам вместо термина “состояние” может применяться термин “продолжение” - абстрактное представление состояния программы в определённый момент времени, которое может быть сохранено и использовано для перехода в это состояние.

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

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

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

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

Протопоток - единица программной обработки, не задействующая механизмы потоков операционной системы, одна из форм реализации сопрограмм. Он работает как “легковесный” поток, не использующий стек для хранения состояния программы. Из-за этого данная задача ложится на плечи программиста. Для ее решения могут использоваться либо глобальные переменные, что обычно является не очень хорошей практикой, либо пользовательские структуры данных. Протопотоки не вытесняемы, поэтому переключение контекста возможно только на блокирующих операциях. Существуют библиотеки для реализации протопотоков для Си и С++. В этой статье рассмотрим библиотеку protothreads. Она используется, в первую очередь, при написании программ для систем с ограниченной памятью, например на основе микроконтроллеров. 

Неприятной новостью для программистов на C++ является то, что вся библиотека реализована на макросах, а как мы помним из рекомендаций Скотта Мейерса, нужно избегать макросов там, где это возможно. Однако, в данном случае использование макросов обусловлено тем, что они, в отличие от простых функций, могут изменять поток управления программой только лишь средствами стандартных конструкций языка Си. Необходимо понимать, что протопоток работает в пределах единственной функции Си и не может охватывать другие функции. Функция протопотока декларируется следующим образом: 

#define PT_THREAD(name_args) char name_args 

Вместо name_args подставляется имя функции, выполняемой потоком и ее аргументы. Возвращается переменная типа char, значение которой отражает текущее состояние функции, переданной в макрос PT_THREAD. Всего доступно 4 состояния:

  1. PT_WAITING //ожидание наступления некоторого условия

  2. PT_EXITED //произошел выход из протопотока

  3. PT_ENDED //функция завершилась

  4. PT_YIELDED //возврат этого значения сигнализирует о том, что протопоток, следуя принципу кооперативной многозадачности, уступает процессорное время вызывающему коду

API protothreads состоит из 4 базовых операций. Это инициализация локального продолжения PT_INIT, выполнение протопотока PT_BEGIN,  возврат из потока с сохранением состояния PT_YIELD и завершение PT_END. Кроме того есть еще блокировки на условии PT_WAIT_UNTIL и PT_WAIT_WHILE,  а также блокировки на протопотоке PT_WAIT_THREAD. Также можно ожидать выполнения дочерних protothread’ов PT_WAIT_THREAD и PT_SPAWN, перезапускать PT_RESTART и планировать поток при помощи PT_SCHEDULE.  

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

Рассмотрим реализацию базовых типов данных protothreads.

typedef unsigned short lc_t; // переменная для сохранения состояния, которое является необходимой информацией для корректной работы локального продолжения. Хранит номер строки исходного кода протопотока в которую нужно совершить переход при его первом или повторном вызове.  

struct pt { //структура хранящая состояние

  lc_t lc; };

Теперь посмотрим как реализованы вспомогательные макросы для работы API protothreads.

Справка для программистов на других языках. В С и С++ есть механизм, называемый препроцессором. Его предназначение состоит в обработке исходного кода программы до того, как она будет скомпилирована. У препроцессора есть свои директивы, в частности #define, при помощи которой задается текст для подстановки, например #define PI 3.14. В тексте программы используется символьное имя PI. В результате препроцессинга, который по сути является подстановкой текста, во всех местах программы вместо PI будет подставлено значение 3.14. 

Реализация макросов тривиальна и имеет следующий вид

#define LC_INIT(s) s = 0; 

Этот макрос используется для того, чтобы инициализировать нулем переменную lc рассмотренную выше. Основная цель макросов в protothreads - формирование конечного автомата из состояний при помощи конструкций switch case. Вызов LC_INIT нужен для того чтобы при первом переходе в switch попасть в начальное состояние, отмеченное в коде как case 0.

#define LC_RESUME(s) switch(s) { case 0:

LC_RESUME используется для встраивания в функцию потока блока switch и начального состояния автомата.

#define LC_SET(s) s = __LINE__; case __LINE__:

LC_SET используется для формирования остальных состояний в соответствии с логикой работы программы. В качестве входного параметра s используется  переменная lc (локальное продолжение) из структуры pt. Задача, решаемая макросом, состоит в том, чтобы инициализировать lc значением номера текущей строки исходного кода (макрос __LINE__) и сформировать новое состояние конечного автомата при помощи case. Зачем это нужно, рассмотрим позже на конкретном примере.

#define LC_END(s)  }

LC_END встраивает в код функции протопотока закрывающую скобку для switch.

Теперь рассмотрим основные макросы API protothreads.

#define PT_INIT(pt)   LC_INIT((pt)->lc)

PT_INIT инициализирует локальное продолжение lc значением 0.

#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)

PT_BEGIN инициализирует переменную PT_YIELD_FLAG, используемую для передачи управления другой функции по принципу кооперативной многозадачности, а также встраивает в код блок switch case 0.

#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \

                                     PT_INIT(pt); return PT_ENDED; }

Встраивает закрывающую скобку для оператора switch, сбрасывает PT_YIELD_FLAG в 0. Устанавливает lc в 0 и возвращает PT_ENDED, сигнализируя о том что протопоток закончил выполнение. После этого поток можно вызывать повторно, его работа начнется с нулевого состояния.

За логику возврата с сохранением локального продолжения отвечает самый сложный многострочный макрос PT_YIELD.                   

#define PT_YIELD(pt)				\

  do {						\

    PT_YIELD_FLAG = 0;				\

    LC_SET((pt)->lc);				\

    if(PT_YIELD_FLAG == 0) {			\

      return PT_YIELDED;			\

    }						\

  } while(0)      

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

Чтобы понять, как описанные макросы работают вместе, создадим нашу первую сопрограмму на протопотоках. Следуя традициям, напишем генератор чисел Фибоначчи на языке Си. Исходный код расположен в репозитории, файл fibonacci.c. При этом для наглядности рядом с исходным кодом (колонка слева) помещен результат обработки этого кода препроцессором (команда gcc -E fibonacci.c).

1:#include <stdio.h>

2:#include <stdlib.h>

3:#include "lib/pt.h"

4:

5:struct task {

6: unsigned long a;

7: unsigned long b;

8: unsigned long tmp;

9: struct pt state;

10:};

11:

12:PT_THREAD(fibonacci(struct task *tsk))

13:{

14:

15:  PT_BEGIN(&tsk->state);

16:  while(1) {

17: PT_YIELD(&tsk->state);

18: tsk->tmp = tsk->a;

19: tsk->a = tsk->b;

20: tsk->b = tsk->b + tsk->tmp;

21:  }

22:  

23:  PT_END(&tsk->state);

24:}

25: 

26:int main(void)

27:{

28:  int i = 0;

29:  

30:  struct task tsk;

31:  tsk.a = 0;

32:  tsk.b = 1;  

33:

34:  PT_INIT(&tsk.state);

35:  

36:  while(i < 10) {

37:    fibonacci(&tsk);

38:    printf("fibonacci %d : %lu \n", i+1, tsk.a);

39:    i++;

40:  }

41:  return EXIT_SUCCESS;

42:}

1:struct task {

2: unsigned long a;

3: unsigned long b;

4: unsigned long tmp;

5: struct pt state;

6:};

7:

8:char fibonacci(struct task *tsk)

9:{

10:

11:  { char PT_YIELD_FLAG = 1; switch((&tsk->state)->lc) { case 0:;

12:  while(1) {

13: do { PT_YIELD_FLAG = 0; (&tsk->state)->lc = 17; case 17:; if(PT_YIELD_FLAG == 0) { return 1; } } while(0);

14: tsk->tmp = tsk->a;

15: tsk->a = tsk->b;

16: tsk->b = tsk->b + tsk->tmp;

17:  }

18:

19:  }; PT_YIELD_FLAG = 0; (&tsk->state)->lc = 0;; return 3; };

20:}

21:

22:int main(void)

23:{

24:  int i = 0;

25:

26:  struct task tsk;

27:  tsk.a = 0;

28:  tsk.b = 1;

29:

30:  (&tsk.state)->lc = 0;;

31:

32:  while(i < 10) {

33:    fibonacci(&tsk);

34:    printf("fibonacci %d : %lu \n", i+1, tsk.a);

35:    i++;

36:  }

37:  return  0;

38:}

Для использования protothreads достаточно подключить заголовочный файл pt.h. Структура task (строки 5-10) используется для сохранения внутреннего состояния сопрограммы fibonacci между ее вызовами. Создание этой структуры продиктовано желанием сделать код более универсальным и не использовать статические переменные.

Переменные task.a и task.b вначале служат для хранения первого и второго чисел Фибоначчи (0 и 1 соответственно), а также для дальнейших вычислений. Функция main инициализирует task, в том числе, локальное продолжение при помощи макроса PT_INIT, выставляя lc в 0 (строка 30 в колонке справа).

Далее в цикле вызывается сопрограмма fibonacci для получения первых 10 чисел и результаты выводятся на экран. Рассмотрим код корутины после препроцессинга (строки 8-20 в колонке справа). Перед первым вызовом fibonacci значение lc равно 0.  При входе в функцию PT_YIELD_FLAG устанавливается в 1 и далее в операторе switch по значению lc осуществляется переход в case 0 с переходом в цикл while(1).  

В строке 13 идет код макроса PT_YIELD.  Вначале PT_YIELD_FLAG устанавливается в 0, подготавливая выход из функции. Далее lc устанавливается в значение 17 - номер строки в которой расположен макрос PT_YIELD. После этого осуществляется переход в case 17. И так как PT_YIELD_FLAG равен нулю функция передает управление в main c кодом возврата PT_YIELDED. 

Выход из функции сделан таким образом потому что первое число Фибоначчи не нужно вычислять, оно задано при инициализации tsk функцией main. Во время второго вызова fibonacci PT_YIELD_FLAG снова устанавливается в 1, но в операторе switch происходит переход в case 17 так как значение lc было установлено в 17 при предыдущем вызове. Таким образом реализуется запоминание места с которого нужно продолжить выполнение сопрограммы. Условный оператор if(PT_YIELD_FLAG == 0) не срабатывает и выполняются строки с вычислением очередного числа Фибоначчи, после чего переходим в начало цикла while(1) с установкой PT_YIELD_FLAG в 0 и выходом из функции. Так получается второе число. Далее описанные действия повторяются. 

Несмотря на то, что вычисления чисел Фибоначчи осуществляется в бесконечном цикле, нужно все равно в конце сопрограммы вставить макрос PT_END. Иначе программа не компилируется.

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

1:#include <stdio.h>

2:#include <stdlib.h>

3:#include "lib/pt.h"

4:

5:#define LINE_LENGTH 256

6:struct line_task {

7:	unsigned long lc;

8:	struct pt state;

9:};

10:

11:struct file_task {

12:	FILE* stream;

13:	char* line;

14:	int line_length;

15:	struct pt state;

16:};

17:

18:PT_THREAD(line_counter(struct line_task *tsk))

19:{

20:  PT_BEGIN(&tsk->state);

21:  while(1) {

22:	PT_YIELD(&tsk->state);

23:	tsk->lc++;

24:  }

25:  

26:  PT_END(&tsk->state);

27:}

28:

29:PT_THREAD(line_reader(struct file_task *fltsk))

30:{

31:  PT_BEGIN(&fltsk->state);

32:  while(1) {

33:	if(!fgets(fltsk->line, fltsk->line_length, fltsk->stream))

34:      	break;

35:	PT_YIELD(&fltsk->state);

36:  }

37:  

38:  PT_END(&fltsk->state);

39:}

40:

41:int main(int argc, char *argv[])

42:{

43:   char line[LINE_LENGTH];   

44:   struct file_task fltsk;

45:

46:   if (argc != 2) {

47:        printf("Usage : %s <file_name>\n", argv[0]);

48:        return EXIT_FAILURE;

49:    }

50:

51:    fltsk.stream = fopen(argv[1], "r");

52:    

53:    if (!fltsk.stream) {

54:        printf("File not found\n");

55:        return EXIT_FAILURE;

56:    }

57:    fltsk.line = (char *)&line;

58:    fltsk.line_length = sizeof(line);

59:   

60:    PT_INIT(&fltsk.state);

61:    

62:    struct line_task tsk;

63:    tsk.lc=1;

64:

65:    PT_INIT(&tsk.state);

66:

67:    while(line_reader(&fltsk) != PT_ENDED) {

68:        line_counter(&tsk);

69:        printf("%lu:", tsk.lc);

70:	  printf("%s", fltsk.line);

71:    }	

72:

73:    fclose(fltsk.stream);

74:    return EXIT_SUCCESS;

75:}

Основу программы составляют корутины line_counter и line_reader. Первая из них является генератором номеров строк, осуществляя инкремент номера при каждом вызове (строки 21-24). Вторая при каждом вызове считывает файл построчно при помощи функции fgets пока не будет достигнут конец файла. 

Логика организации обоих корутин, такая же, как и в предыдущем примере. Функция main осуществляет вызов line_counter и line_reader в цикле (строки 67-71). Если сопрограмма line_reader не завершилась, то из файла считывается очередная строка, для нее генерируется порядковый номер и осуществляется вывод на экран.

Кооперативная многозадачность в примере выше реализована в виде цикла, последовательно вызывающего корутины.  В более сложных случаях можно добавить планировщик последовательно выбирающий на выполнение задачи из очереди. Использование такого вида многозадачности даёт несколько преимуществ перед вытесняющей многозадачностью:

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

  2. Низкие накладные расходы памяти на протопоток. Фактически выделение памяти для локальных переменных ложится на плечи программиста.  В многопоточных приложениях размер выделяемой под стек памяти измеряется мегабайтами.

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

  4. Использование корутин позволяет отказаться от использования функций обратного вызова, которые широко используются при многопоточной разработке и снижают читаемость программ.

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

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

По умолчанию все файловые дескрипторы в Unix-системах создаются в “блокирующем” режиме. Это означает, что системные вызовы  ввода вывода, такие как read или write блокируют выполнение программы вплоть до готовности данных. Например, при вызове read для stdin дальнейшее выполнение блокируется до тех пор, пока данные не будут введены пользователем с клавиатуры и прочитаны системой. То же самое происходит и для других файловых дескрипторов. Для устранения блокировок существует два способа, которые могут дополнять друг друга:

  1. Неблокирующий режим ввода вывода.

  2. Мультиплексирование с помощью специального API, например select, epoll, io_uring.

В данной статье рассмотрим только первый способ.  Для того чтобы задать для файлового дескриптора “неблокирующий” режим,  нужно добавить флаг O_NONBLOCK к существующему набору флагов этого дескриптора при помощи функции fcntl. 

С момента установки O_NONBLOCK дескриптор становится неблокирующим. Любые системные вызовы для ввода вывода, такие как read и write, при отсутствии данных вызывали блокировку. Теперь они возвращают -1. Кроме того глобальная переменная errno может принимать значения EWOULDBLOCK или EAGAIN. Используя эти знания напишем программу чтения строки в неблокирующем режиме со стандартного устройства ввода при помощи корутины.

1:#include <stdio.h>

2:#include <stdlib.h>

3:

4:#include <unistd.h>

5:#include <fcntl.h>

6:#include <errno.h>

7:

8:#include "lib/pt.h"

9:

10:#define READ_SIZE 256

11:

12:struct read_task {

13:	char* line;

14:	int line_length;

15:	int index;

16:	ssize_t bytes_read;

17:	struct pt state;

18:};

19:

20:PT_THREAD(stdin_reader(struct read_task *rdtsk))

21:{

22:  PT_BEGIN(&rdtsk->state);

23:  while(1) {

24:	PT_YIELD(&rdtsk->state);

25:	

26:	rdtsk->bytes_read = read(STDIN_FILENO, &rdtsk->line[rdtsk->index], rdtsk->line_length);

27:	

28:	if((rdtsk->bytes_read < 0) && (errno != EAGAIN) && (errno != EWOULDBLOCK))

29:		break;

30:		

31:	if(rdtsk->bytes_read >= 0)

32:	{

33:	rdtsk->index += rdtsk->bytes_read;

34:	if(rdtsk->line[rdtsk->index-1] == '\n') {

35:		rdtsk->line[rdtsk->index] = '\0';

36:		break;

37:	}

38:    }

39:  }

40:  

41:  PT_END(&rdtsk->state);

42:}

43:

44:int set_stdin_nonblock_mode() {

45:    // Устанавливаем stdin в неблокирующий режим

46:    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);

47:    if (flags == -1) {

48:        perror("Error fcntl getting flag F_GETFL\n");

49:        return EXIT_FAILURE;

50:    }

51:    if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) {

52:        perror("Error fcntl setting flag F_SETFL\n");

53:        return EXIT_FAILURE;

54:    }

55:    return EXIT_SUCCESS;

56:}

57:

58:void init_reader(struct read_task rtask, char buffer, int size) {

59:    rtask->line = buffer;

60:    rtask->line_length = size;

61:    rtask->index = 0;

62:    PT_INIT(&rtask->state);

63:}

64:

65:int main(void) {

66:    char buffer[READ_SIZE];

67:    struct read_task rtask;

68:

69:    if (set_stdin_nonblock_mode())

70:    return EXIT_FAILURE;

71:

72:    init_reader(&rtask, (char *)&buffer, sizeof(buffer));

73:

74:     printf("Enter some string:\n");

75:    do{

76:    }while(stdin_reader(&rtask) != PT_ENDED);

77:    

78:    if(rtask.bytes_read >= 0)

79:		printf("Read: %s \n", rtask.line);

80:	else 

81:		printf("Error reading from STDIN\n");

82:

83:  return EXIT_SUCCESS;

84:}

Программа устанавливает неблокирующий режим stdin в функции set_stdin_nonblock_mode. Для этого вначале получаем текущие флаги функцией fcntl. Далее добавляем к ним O_NONBLOCK и записываем изменения при помощи fcntl.  После этого в функции init_reader готовим структуру данных для функционирования корутины stdin_reader.  В теле сопрограммы (строка 26) пытаемся прочитать данные вызовом read. Он возвращает количество прочитанных байт либо -1 если пользователь ничего не ввел или произошла какая-то другая ошибка. 

В строке 28 проверяем произошла ли ошибка, связанная с отсутствием данных (errno установлен в значение EWOULDBLOCK или EAGAIN). Если функция read вернула отрицательное значение и код ошибки не связан с отсутствием данных, то корутину можно завершить. Если же данные считаны, то они накапливаются в буфере (строки 31-37) до тех пор пока не будет получен символ новой строки. После этого корутина завершает свою работу. Накопление введенных символов в буфере нужно потому что теперь данные поступают постепенно, а не сразу как в случае блокирующего режима. Как и в предыдущем примере накопление данных реализовано упрощенно. Предполагается что переполнения буфера никогда не происходит. 

Подводя итог, хочется отметить следующее плюсы реализации сопрограмм при помощи протопотоков:

  1. Использование protothreads позволяет писать переносимый код с корутинами без необходимости использовать  ассемблер для сохранения состояния сопрограммы.

  2. Низкие накладные расходы на создание и переключение сопрограмм, что особенно критично для  встраиваемых систем с ограниченными ресурсами.

  3. Независимость от операционной системы.

Из очевидных минусов можно выделить:

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

 2. Реализация библиотеки вводит небольшое ограничение для кода, который использует протопотоки - сам код не может использовать операторы switch. Однако эту проблему можно решить путем использования компилятора gcc и его расширений.

Комментарии (2)


  1. vadimr
    07.10.2025 10:29

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

    Какой смысл в продолжении, которое нельзя вызывать несколько раз, и где реализовано такое чудо?


  1. Jijiki
    07.10.2025 10:29

    интересно, у вас используется селект, там не нужен FD_SET + timestamp?