yield to main_co: %d\n", aco_get_co(), ((int)(aco_get_arg())));
aco_yield(); ((int)(aco_get_arg())) = ct + 1; }
void co_fp0() { printf("co: %p: entry: %d\n", aco_get_co(), ((int)(aco_get_arg()))); int ct = 0; while(ct < 6){ foo(ct); ct++; } printf("co: %p: exit to main_co: %d\n", aco_get_co(), ((int)(aco_get_arg()))); aco_exit(); }
int main() { aco_thread_init(NULL);
aco_t* main_co = aco_create(NULL, NULL, 0, NULL, NULL);
aco_share_stack_t* sstk = aco_share_stack_new(0);
int co_ct_arg_point_to_me = 0;
aco_t* co = aco_create(main_co, sstk, 0, co_fp0, &co_ct_arg_point_to_me);
int ct = 0;
while(ct < 6){
assert(co->is_end == 0);
printf("main_co: yield to co: %p: %d\n", co, ct);
aco_resume(co);
assert(co_ct_arg_point_to_me == ct);
ct++;
}
printf("main_co: yield to co: %p: %d\n", co, ct);
aco_resume(co);
assert(co_ct_arg_point_to_me == ct);
assert(co->is_end);
printf("main_co: destroy and exit\n");
aco_destroy(co);
co = NULL;
aco_share_stack_destroy(sstk);
sstk = NULL;
aco_destroy(main_co);
main_co = NULL;
return 0;
}
**В запросе представлен фрагмент кода на языке C, который реализует работу с сопрограммами (coroutines) в рамках библиотеки libaco.**
В коде создаются две сопрограммы: main_co и co. Сопрограмма main_co является основной и создаёт новую сопрограмму co с помощью функции aco_create(). Затем main_co выполняет цикл из шести итераций, на каждой из которых она приостанавливает своё выполнение с помощью aco_yield() и передаёт управление сопрограмме co. После этого main_co возобновляет выполнение и проверяет состояние сопрограммы co.
Сопрограмма co также выполняет цикл из шести итераций. На каждой итерации она увеличивает значение переменной ct на единицу. По завершении цикла сопрограмма co завершает своё выполнение с помощью функции aco_exit().
После завершения работы с сопрограммой co основная сопрограмма main_co также завершает свою работу. Она уничтожает созданные объекты и освобождает ресурсы. libaco в каждом из потоков. В libaco нет совместного использования данных между потоками, и вам придётся самостоятельно справляться с конкуренцией за данные между несколькими потоками (как это делает `gl_race_aco_yield_ct` в этом руководстве).
Одно из правил в libaco — вызывать `aco_exit()` для завершения выполнения неосновного co вместо стандартного прямого возврата в стиле C, иначе libaco будет рассматривать такое поведение как незаконное и запускать стандартный защитник, задача которого — регистрировать информацию об ошибке о нарушающем co в stderr и немедленно завершать процесс. Ситуация «нарушающего co» показана в `test_aco_tutorial_4.c`.
Вы также можете определить свой собственный защитник для замены стандартного (чтобы сделать некоторые настроенные «последние слова»). Но независимо от ситуации процесс будет прерван после выполнения защитника. Как определить функцию последнего слова показано в `test_aco_tutorial_5.c`.
Последний пример — простой планировщик сопрограмм в `test_aco_tutorial_6.c`.
# API
Будет очень полезно читать соответствующую реализацию API в исходном коде одновременно с чтением следующего описания API libaco, поскольку исходный код довольно понятен и лёгок для понимания. Также рекомендуется прочитать все [учебники](#tutorials) перед чтением документа API.
Перед началом написания реального приложения на основе libaco настоятельно рекомендуется прочитать раздел [Best Practice](#best-practice) (в дополнение к описанию того, как действительно раскрыть экстремальную производительность libaco в вашем приложении, есть также замечание о программировании libaco).
Примечание: контроль версий libaco соответствует спецификации: [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). Поэтому API в следующем списке имеют гарантию совместимости. (Обратите внимание, что такой гарантии для API нет в списке.)
## aco_thread_init
```c
typedef void (*aco_cofuncp_t)(void);
void aco_thread_init(aco_cofuncp_t last_word_co_fp);
Инициализирует среду libaco в текущем потоке.
Он сохранит текущие контрольные слова FPU и MXCSR в локальную глобальную переменную потока.
ACO_CONFIG_SHARE_FPU_MXCSR_ENV
не определён, сохранённые контрольные слова будут использоваться в качестве эталонного значения для настройки контрольных слов FPU и MXCSR нового co (в aco_create
), и каждый co будет поддерживать свою собственную копию контрольных слов FPU и MXCSR во время последующего переключения контекста.ACO_CONFIG_SHARE_FPU_MXCSR_ENV
определён, то все co используют одни и те же контрольные слова FPU и MXCSR. Дополнительную информацию об этом можно найти в разделе «Сборка и тестирование» этого документа.Как сказано в test_aco_tutorial_5.c
раздела «Учебники», когда 1-й аргумент last_word_co_fp
не равен NULL, функция, на которую указывает last_world_co_fp
, заменит стандартный защитник, чтобы выполнить некоторые «последние действия» относительно нарушающего co перед прерыванием процесса. В такой функции последнего слова вы можете использовать aco_get_co
, чтобы получить указатель на нарушающий co. Для получения дополнительной информации вы можете прочитать test_aco_tutorial_5.c
.
aco_share_stack_t* aco_share_stack_new(size_t sz);
Эквивалентно aco_share_stack_new2(sz, 1)
.
aco_share_stack_t* aco_share_stack_new2(size_t sz, char guard_page_enabled);
Создаёт новый общий стек с рекомендуемым размером памяти sz
в байтах и может иметь защитную страницу (только для чтения) для обнаружения переполнения стека, которое зависит от второго аргумента guard_page_enabled
.
Использовать значение по умолчанию (2 МБ), если 1-й аргумент sz
равен 0. После некоторых вычислений выравнивания и резервирования эта функция гарантирует окончательный допустимый размер общего стека в возвращаемом значении:
final_valid_sz >= 4096
final_valid_sz >= sz
final_valid_sz % page_size == 0 if the guard_page_enabled != 0
И как можно ближе к значению sz
. Эти 3 макроса определены в заголовке aco.h
, и их значение соответствует спецификации: Semantic Versioning 2.0.0 (https://semver.org/spec/v2.0.0.html).
// provide the compiler with branch prediction information
#define likely(x) aco_likely(x)
#define unlikely(x) aco_unlikely(x)
// override the default `assert` for convenience when coding
#define assert(EX) aco_assert(EX)
// equal to `assert((ptr) != NULL)`
#define assertptr(ptr) aco_assertptr(ptr)
// assert the successful return of memory allocation
#define assertalloc_bool(b) aco_assertalloc_bool(b)
#define assertalloc_ptr(ptr) aco_assertalloc_ptr(ptr)
Можно включить заголовок "aco_assert_override.h"
, чтобы переопределить стандартный C-макрос [assert](http://man7.org/linux/man-pages/man3/assert.3.html)
в приложении libaco, как это делает test_aco_synopsis.c
(этот заголовок следует включать последним в списке директив include в исходном файле, поскольку C-макрос [assert]
также является определением макроса C), и определить остальные 5 макросов, указанных выше. Не включайте этот заголовок в исходный файл приложения, если вы хотите использовать стандартный C-макрос [assert]
.
Для получения более подробной информации обратитесь к исходному файлу aco_assert_override.h
.
Дата: Sat Jun 30 UTC 2018. Машина: c5d.large на AWS. ОС: RHEL-7.5 (Red Hat Enterprise Linux 7.5).
Вот краткое изложение результатов тестирования:
aco_create/init_save_stk_sz=64B 2 000 000 0.131 с 65.63 нс/оп 15237386.02 оп/с aco_resume/co_amount=2 000 000/copy_stack_size=8B 20 000 000 0.664 с 33.20 нс/оп 30119155.82 оп/с aco_destroy 2 000 000 0.065 с 32.67 нс/оп 30604542.55 оп/с
aco_create/init_save_stk_sz=64B 2 000 000 0.131 с 65.33 нс/оп 15305975.29 оп/с aco_resume/co_amount=2 000 000/copy_stack_size=24B 20 000 000 0.675 с 33.74 нс/оп 29638360.61 оп/с aco_destroy 2 000 000 0.067 с 33.31 нс/оп 30016633.42 оп/с
aco_create/init_save_stk_sz=64B 2 000 000 0.131 с 65.61 нс/оп 15241767.78 оп/с aco_resume/co_amount=2 000 000/copy_stack_size=40B 20 000 000 0.678 с 33.88 нс/оп 29518648.08 оп/с aco_destroy 2 000 000 0.079 с 39.74 нс/оп 25163018.30 оп/с
aco_create/init_save_stk_sz=64B 2 000 000 0.221 с 110.73 нс/оп 9030660.30 оп/с aco_resume/co_amount=2 000 000/copy_stack_size=56B 20 000 000 0.684 с 34.18 нс/оп 29253416.65 оп/с aco_destroy 2 000 000 0.067 с 33.40 нс/оп 29938840.64 оп/с
aco_create/init_save_stk_sz=64B 2 000 000 0.131 с 65.60 нс/оп 15244077.65 оп/с aco_resume/co_amount=2 000 000/copy_stack_size=120B 20 000 000 0.769 с 38.43 нс/оп 26021228.41 оп/с aco_destroy 2 000 000 0.087 с 43.74 нс/оп 22863987.42 оп/с
aco_create/init_save_stk_sz=64B 10 000 000 1.251 с 125.08 нс/оп 7994958.59 оп/с aco_resume/co_amount=10 000 000/copy_stack_size=8B 40 000 000 1.327 с 33.19 нс/оп 30133654.80 оп/с aco_destroy 10 000 000 0.329 с 32.85 нс/оп 30439787.32 оп/с
aco_create/init_save_stk_sz=64B 10 000 000 0.674 с 67.37 нс/оп 14843796.57 оп/с aco_resume/co_amount=10 000 000/copy_stack_size=24B 40 000 000 1.354 с 33.84 нс/оп 29548523.05 оп/с aco_destroy 10 000 000 0.339 с 33.90 нс/оп 29494634.83 оп/с
aco_create/init_save_stk_sz=64B 10 000 000 0.672 с 67.19 нс/оп 14882262.88 оп/с aco_resume/co_amount=10 000 000/copy_stack_size=40B 40 000 000 1.361 с 34.02 нс/оп 29393520.19 оп/с aco_destroy 10 000 000 0.338 с 33.77 нс/оп 29609577.59 оп/с
aco_create/init_save_stk_sz=64B 10 000 000 0.673 с 67.31 нс/оп 14857716.02 оп/с aco_resume/co_amount=10 000 000/copy_stack_size=56B 40 000 000 1.371 с 34.27 нс/оп 29181897.80 оп/с aco_destroy 10 000 000 0.339 с 33.85 нс/оп 29540633.63 оп/с
aco_create/init_save_stk_sz=64B 10 000 000 0.672 с 67.24 нс/оп 14873017.10 оп/с aco_resume/co_amount=10 000 000/copy_stack_size=120B 40 000 000 1.548 с 38.71 нс/оп 25835542.17 оп/с aco_destroy 10 000 000 0.446 с 44.61 нс/оп 22415961.64 оп/с
aco_create/init_save_stk_sz=64B 2 000 000 0.132 с 66.01 нс/оп 15148290.52 оп/с aco_resume/co_amount=2 000 000/copy_stack_size=136B 20 000 000 0.944 с 47.22 нс/оп 21177946.19 оп/с aco_destroy 2 000 000 0.124 с 61.99 нс/оп 16132721.97 оп/с Все остальные сопрограммы, не являющиеся основными, делают то же самое.
Следующая диаграмма представляет собой простой пример переключения контекста между main_co и co.
В этом доказательстве мы просто предполагаем, что работаем под Sys V ABI от intel386, поскольку нет принципиальных различий между Sys V ABI от intel386 и x86-64. Мы также предполагаем, что ни один код не изменит управляющие слова FPU и MXCSR.
На следующей диаграмме фактически представлена модель работы симметричной сопрограммы с неограниченным количеством не основных co и одной основной co. Это нормально, потому что асимметричная сопрограмма — это всего лишь частный случай симметричной сопрограммы. Доказать корректность симметричной сопрограммы немного сложнее, чем асимметричной, и поэтому интереснее. (libaco в настоящее время реализовал только API асимметричной сопрограммы, потому что семантическое значение API асимметричной сопрограммы гораздо легче понять и использовать, чем у симметричной.)
Поскольку основная co — это первая сопрограмма, которая начинает работать, первое переключение контекста в этом потоке ОС должно быть в форме acosw(main_co, co)
, где второй аргумент co
— это не основная co.
Легко доказать, что существует только два вида передачи состояния на приведённой выше диаграмме:
Чтобы доказать корректность реализации void* acosw(aco_t* from_co, aco_t* to_co)
, нужно доказать, что все co постоянно соответствуют ограничениям Sys V ABI до и после вызова acosw
. Мы предполагаем, что другая часть двоичного кода (кроме acosw
) в co уже соответствует ABI (обычно они правильно генерируются компилятором).
Вот краткое изложение ограничений регистров в соглашении о вызовах функций Intel386 Sys V ABI:
Использование регистров в соглашении о вызове функций Intel386 System V ABI:
Регистры, сохраняемые вызывающим (временные) регистры:
C1.0: EAX
При входе в вызов функции:
может иметь любое значение
После возврата из acosw
:
содержит возвращаемое значение для acosw
C1.1: ECX,EDX
При входе в вызов функции:
может иметь любое значение
После возврата из acosw
:
может иметь любое значение
C1.2: Арифметические флаги, флаги x87 и mxcsr
При входе в вызов функции:
может иметь любое значение
После возврата из acosw
:
может иметь любое значение
C1.3: ST(0-7)
При входе в вызов функции:
стек FPU должен быть пустым
После возврата из acosw
:
стек FPU должен быть пустым
C1.4: Флаг направления
При входе в вызов функции:
DF должен быть 0
После возврата из acosw
:
DF должен быть 0
C1.5: другие: xmm*,ymm*,mm*,k*...
При входе в вызов функции:
может иметь любое значение
После возврата из acosw
:
может иметь любое значение
Сохраняемые вызываемым регистры:
C2.0: EBX,ESI,EDI,EBP
При входе в вызов функции:
может иметь любое значение
После возврата из acosw
:
должно быть таким же, как при входе в acosw
C2.1: ESP
При входе в вызов функции:
должен быть действительным указателем стека
(выравнивание по 16 байтам, retaddr и т. д.)
После возврата из acosw
:
должен быть таким же, как перед вызовом acosw
C2.2: управляющее слово FPU & mxcsr
При входе в вызов функции:
может быть любой конфигурации
После возврата из acosw
:
должно быть таким же, как до вызова acosw
,
если только вызывающий acosw
не предполагает,
что acosw
может изменить управляющие слова FPU или MXCSR намеренно,
как fesetenv
Использование регистра определяется в «P13 — Таблица 2.3: Использование регистра» Sys V ABI Intel386 V1.1 и для AMD64 в «P23 — Рисунок 3.4: Использование регистра» Sys V ABI AMD64 V1.0.
Доказательство:
Рисунок proof_2 (не приведён).
Диаграмма выше относится к первому случаю: «состояние co -> начальное состояние co».
Ограничения: C 1.0, 1.1, 1.2, 1.5 (удовлетворены ✓).
Скретч-регистры ниже могут содержать любое значение при входе в функцию:
EAX, ECX, EDX
XMM*, YMM*, MM*, K*...
status bits of EFLAGS, FPU, MXCSR
Ограничения: C 1.3, 1.4 (удовлетворены ✓).
Поскольку стек FPU уже должен быть пустым, а DF уже должен равняться 0 перед вызовом acosw(co, to_co)
(двоичный код co уже соответствует ABI), ограничения 1.3 и 1.4 выполняются acosw
.
Ограничения: C 2.0, 2.1, 2.2 (удовлетворены ✓).
C 2.0 & 2.1 уже удовлетворены. Поскольку мы уже предположили, что никто не будет изменять управляющие слова FPU и MXCSR, C 2.2 также удовлетворяется.
Рисунок proof_3 (не приведён).
Диаграмма выше относится ко второму случаю: состояние co -> состояние co.
Ограничение: C 1.0 (удовлетворено ✓).
EAX уже содержит возвращаемое значение, когда acosw
возвращается обратно в to_co (возобновление).
Ограничения: C 1.1, 1.2, 1.5 (удовлетворены ✓).
Скретч-регистры ниже могут содержать любое значение при входе в функцию и после возврата из acosw
:
ECX, EDX
XMM*, YMM*, MM*, K*...
status bits of EFLAGS, FPU, MXCSR
Ограничения: C 1.3, 1.4 (удовлетворены ✓).
Поскольку стек FPU уже должен быть пустым, а DF уже должен равняться 0 перед вызовом acosw(co, to_co)
(двоичный код co уже соответствует ABI), ограничения 1.3 и 1.4 выполняются acosw
.
Ограничения: C 2.0, 2.1, 2.2 (удовлетворены ✓).
C 2.0 & 2.1 удовлетворяются, поскольку происходит сохранение и восстановление регистров, сохранённых вызываемым объектом, при вызове/возврате acosw
. Поскольку мы уже предположили, что никто не будет изменять управляющие слова FPU и MXCSR, C 2.2 также удовлетворяется.
Первый acosw
в потоке должен быть первым случаем: состояние co → начальное состояние co, и все последующие acosw
должны быть одним из двух случаев, описанных выше. Последовательно можно доказать, что «все co постоянно соответствуют ограничениям Sys V ABI до и после вызова acosw
». Таким образом, доказательство завершено.
Существует новая концепция, называемая красной зоной в System V ABI x86-64:
Область в 128 байт за местоположением, на которое указывает %rsp, считается зарезервированной и не должна изменяться обработчиками сигналов или прерываний. Поэтому функции могут использовать эту область для временных данных, которые не нужны при вызовах функций. В частности, листовые функции могут использовать эту область для всего своего стекового фрейма вместо корректировки указателя стека в прологе и эпилоге. Эта область известна как красная зона.
Так как красная зона «не сохраняется вызываемым», мы просто не учитываем её при переключении контекста между сопрограммами (поскольку acosw
является листовой функцией).
Конец области входных аргументов должен быть выровнен по границе в 16 (32 или 64, если __m256 или __m512 передаются в стеке) байт. Другими словами, значение (%esp + 4) всегда кратно 16 (32 или 64), когда управление передаётся точке входа функции. Указатель стека, %esp, всегда указывает на конец последнего выделенного стекового фрейма.
— Intel386-psABI-1.1:2.2.2 Стек
Указатель стека, %rsp, всегда указывает на конец последнего выделенного стекового фрейма.
— Sys V ABI AMD64 Version 1.0:3.2.2 Стек
Вот пример ошибки в Tencent's libco. ABI утверждает, что (E|R)SP
должен всегда указывать на конец последнего выделенного стекового фрейма. Но в файле coctx_swap.S. Из документации libco: использование (E|R)SP для адресации памяти в куче
По умолчанию обработчик сигнала вызывается в обычном стеке процесса. Можно организовать работу так, чтобы обработчик сигналов использовал альтернативный стек; см. sigalstack(2) для обсуждения того, как это сделать и когда это может быть полезно.
— man 7 signal: Signal dispositions
Могут произойти ужасные вещи, если (E|R)SP указывает на структуру данных в куче, когда приходит сигнал. (Использование команд breakpoint и signal в gdb может удобно создать такую ошибку. Хотя с помощью sigalstack для изменения стандартного стека сигналов можно решить проблему, но всё же такое использование (E|R)SP нарушает ABI.)
Лучшая практика
Если вы хотите получить максимальную производительность от libaco, просто сделайте использование стека неавтономным неглавным co в точке вызова aco_yield как можно меньше. И будьте очень осторожны, если вы хотите передать адрес локальной переменной от одного co другому co, поскольку локальная переменная обычно находится в общем стеке. Всегда разумнее выделять такие переменные из кучи.
В деталях есть 5 советов:
Ключом к тому, чтобы использование стека функцией было как можно меньше, является выделение локальных переменных (особенно больших) в куче и управление их жизненным циклом вручную вместо выделения их по умолчанию в стеке. Опция -fstack-usage в gcc очень полезна в этом отношении.
int* gl_ptr;
void inc_p(int* p){ (*p)++; }
void co_fp0() {
int ct = 0;
gl_ptr = &ct; // строка 7
aco_yield();
check(ct);
int* ptr = &ct;
inc_p(ptr); // строка 11
aco_exit();
}
void co_fp1() {
do_sth(gl_ptr); // строка 16
aco_exit();
}
int* gl_ptr;
void inc_p(int* p){ (*p)++) };
void co_fp0() {
int* ct_ptr = malloc(sizeof(int));
assert(ct_ptr != NULL);
*ct_ptr = 0;
gl_ptr = ct_ptr;
aco_yield();
check(*ct_ptr);
int* ptr = ct_ptr;
inc_p(ptr);
free(ct_ptr);
gl_ptr = NULL;
aco_exit();
}
void co_fp1() {
do_sth(gl_ptr);
aco_exit();
}
Новые идеи приветствуются!
Поддержка других платформ (особенно arm и arm64).
v1.2.4 Sun Jul 29 2018
Изменено `asm` на `__asm__` в aco.h для поддержки флага компилятора `--std=c99`
(проблема №16, предложенная Тео Шлосснаглом @postwait).
v1.2.3 Thu Jul 26 2018
Добавлена поддержка MacOS;
Добавлена поддержка сборки разделяемой библиотеки libaco (PR #10, предложена
Тео Шлосснаглем @postwait);
Добавлен макрос C ACO_REG_IDX_BP в aco.h (PR #15, предложен Тео Шлосснаглем
@postwait);
Добавлен глобальный макрос конфигурации C ACO_USE_ASAN, который может включить
дружественную поддержку санитайзера адресов (как gcc, так и clang) (PR #14,
предложен Тео Шлосснаглом @postwait);
Добавлен README_zh.md.
v1.2.2 Mon Jul 9 2018
В make.sh добавлена новая опция `-o <no-m32|no-valgrind>`;
Исправление значения макроса ACO_VERSION_PATCH (проблема №1, любезно сообщена
Маркусом Эльфрингом @elfring);
Скорректировано некоторое несоответствующее именование идентификаторов (двойное
подчёркивание `__`) (проблема №1, предложено Маркусом Эльфрингом @elfring);
Поддерживается включение заголовочного файла на C++ (проблема №4, предложено
Маркусом Эльфрингом @elfring).
v1.2.1 Sat Jul 7 2018
Исправлены некоторые несоответствующие защитные включения в двух заголовочных
файлах C (проблема №1 любезно сообщена Маркусом Эльфрингом @elfring);
Удалено слово «pure» из утверждения «чистый C», поскольку оно содержит коды
сборки (любезно сообщено Питером Коули @corsix);
Много обновлений в документе README.md.
v1.2.0 Tue Jul 3 2018
Предоставлен ещё один заголовок с именем `aco_assert_override.h`, чтобы пользователь
мог выбрать, переопределять ли по умолчанию `assert` или нет;
Добавлено несколько макросов о информации о версии.
v1.1 Mon Jul 2 2018
Убрано требование к версии GCC (> = 5.0).
v1.0 Sun Jul 1 2018
Выпуск v1.0 libaco, ура 🎉🎉🎉
Я разработчик с полной занятостью в открытом исходном коде. Любая сумма пожертвований будет высоко оценена и может стать для меня большим стимулом.
Paypal
Alipay (支付(宝|寶))
Логотип libaco любезно предоставлен Питером Бехом (Peteck). Логотип лицензирован по CC BY-ND 4.0. Веб-сайт libaco.org также любезно предоставлен Питером Бехом (Peteck).
Авторское право (C) 2018, Сен Хан 00hnes@gmail.com.
Под лицензией Apache, версия 2.0.
Подробности см. в файле LICENSE.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )