Перевод текста с английского языка на русский:
Описание
На рисунке thread_model_0 изображено, что состояние выполнения пользовательского пространства (обычно это поток ОС) состоит из четырёх основных элементов: регистров процессора, кода, кучи и стека.
Поскольку информация о местоположении исполняемого кода двоичной программы определяется регистрами (E|R)?IP, а информация об адресе памяти, выделенном из кучи, обычно сохраняется непосредственно или косвенно в рабочем стеке, мы можем свести эти четыре элемента к двум: регистрам процессора и стеку.
Мы определяем main co (основной поток) как поток, который монопольно использует рабочий стек текущего запущенного потока по умолчанию. Поскольку main co является единственным пользователем этого рабочего стека, при переключении контекста связанного с main co потока нам нужно только сохранить и восстановить некоторые необходимые регистры main co.
Далее мы определяем non-main co (не основной поток) как поток, использующий рабочий стек, отличный от рабочего стека текущего запущенного потока (это может быть собственный рабочий стек или общий с другими non-main co). Поэтому у non-main co есть собственный приватный стек сохранения, который используется для восстановления или сохранения рабочего стека при переключении в этот поток или из него (в реализации libaco стратегия сохранения приватного стека является ленивым оптимальным решением; подробности см. в исходном коде aco_resume).
Это особый случай non-main co, который в libaco называется standalone non-main co (независимый не основной поток), то есть поток, монопольно использующий один рабочий стек. При переключении контекста, связанного со standalone non-main co потоком, достаточно сохранить или восстановить только некоторые необходимые регистры (поскольку его рабочий стек является эксклюзивным, состояние рабочего стека не изменяется, когда он переключается вне).
В итоге мы получаем глобальный обзор libaco.
Если вы хотите реализовать свою собственную библиотеку потоков или глубже понять реализацию libaco, раздел «Доказательство корректности» будет очень полезен.
Затем вы можете прочитать раздел «Учебники» или раздел о производительности. Отчёт о тестировании производительности производит глубокое впечатление и заставляет задуматься.
Сборка и тестирование
CFLAGS
Опция компилятора -m32 позволяет пользователям создавать 32-битные двоичные файлы libaco на платформе AMD64.
Если программа пользователя не изменяет во время выполнения управляющие слова FPU и MXCSR, можно выбрать определение глобального макроса C ACO_CONFIG_SHARE_FPU_MXCSR_ENV, чтобы немного ускорить переключение контекста между потоками. Если этот макрос не определён, каждый поток будет поддерживать свою собственную независимую среду управления FPU и MXCSR. Поскольку изменение управляющих слов FPU или MXCSR встречается крайне редко, пользователи могут выбрать постоянное определение этого макроса, но если это невозможно гарантировать, пользователям следует избегать определения этого макроса.
Если пользователи хотят использовать инструмент memcheck valgrind для тестирования приложения libaco, необходимо определить глобальный макрос C ACO_USE_VALGRIND при сборке, чтобы включить поддержку libaco для valgrind memcheck. Из соображений производительности использование этого макроса не рекомендуется в окончательной производственной сборке двоичных файлов. Перед сборкой приложения libaco с глобально определённым этим макросом пользователям необходимо установить заголовочный файл valgrind (например, пакет разработки для Centos называется «valgrind-devel»). В настоящее время memcheck valgrind поддерживает только потоки с независимым рабочим стеком, и memcheck выдаёт много ложных срабатываний при проверке потоков с общим рабочим стеком. Дополнительную информацию можно найти в test_aco_tutorial_6.c.
Сборка
$ mkdir output
$ bash make.sh
Скрипт make.sh содержит более подробные параметры сборки:
$bash make.sh -h
Использование: make.sh [-o <no-m32|no-valgrind>] [-h]
Пример:
# сборка по умолчанию
bash make.sh
# сборка без вывода i386 двоичного файла
bash make.sh -o no-m32
# сборка без поддержки valgrind двоичного вывода
bash make.sh -o no-valgrind
# сборка без поддержки valgrind и без вывода i386 двоичного файла
bash make.sh -o no-valgrind -o no-m32
Короче говоря, если в системе нет заголовочного файла valgrind C, можно использовать параметр -o no-valgrind для сборки тестового набора; если система представляет собой платформу AMD64 и нет установленного 32-разрядного инструментария разработки C-компилятора, можно использовать опцию -o no-m32 для сборки тестового комплекта.
Тестирование
$ cd output
$ bash ../test.sh
Учебники
Файл test_aco_tutorial_0.c содержит базовый пример использования libaco. В этом примере есть только один main co и один standalone non-main co, а комментарии в коде также полезны.
Файл test_aco_tutorial_1.c содержит пример использования статистики выполнения потоков libaco. Определение типа aco_t находится в aco.h и легко понятно.
В файле test_aco_tutorial_2.c есть один standalone non-main co и два non-main co, которые совместно используют один и тот же рабочий стек.
Файл test_aco_tutorial_3.c показывает, как использовать libaco в многопоточной среде. По сути, для достижения наилучшей производительности переключения контекста между потоками, экземпляр libaco должен работать только в одном фиксированном потоке во время проектирования. Таким образом, если вы хотите использовать libaco в нескольких потоках, вам просто нужно использовать его так же, как в однопоточном режиме, в каждом потоке отдельно. Внутри libaco нет никакого обмена данными между потоками; в многопоточных сценариях пользователь должен сам обрабатывать проблемы конкуренции данных (как это делается в переменной gl_race_aco_yield_ct, совместно используемой потоками в этом экземпляре).
Чтобы завершить работу с non-main потоком в libaco, вызовите API aco_exit(). co的执行, а не использовать напрямую ключевое слово C return для возврата (иначе libaco будет рассматривать такое поведение как исключение и запускать процесс protector: выводить сообщение об ошибке в stderr и немедленно вызывать abort для завершения выполнения процесса). В исходном файле test_aco_tutorial_4.c показан пример сопрограммы, нарушающей это правило.
Кроме того, пользователь может настроить логику обработки protector по своему усмотрению (например, выполнить некоторые пользовательские «последние слова» или «завещания»). Однако после завершения работы protector процесс обязательно будет прерван (abort). Исходный файл test_aco_tutorial_5.c описывает, как настроить protector.
В исходном файле test_aco_tutorial_6.c приведён пример простого диспетчера сопрограмм.
API
Рекомендуется читать исходный код вместе с документацией по API, так как он очень понятный и легко читаемый. Кроме того, перед чтением документации по API рекомендуется сначала прочитать раздел Tutorials.
Также перед началом написания приложения на основе libaco настоятельно рекомендуется ознакомиться с разделом Best Practice, где помимо описания того, как использовать libaco для достижения максимальной производительности, также описаны некоторые аспекты программирования с использованием libaco.
Обратите внимание: управление версиями libaco соответствует стандарту Semantic Versioning 2.0.0. Поэтому все перечисленные ниже функции имеют стандартные гарантии совместимости (обратите внимание, что вызовы функций, не включённых в этот список, таких гарантий не имеют).
typedef void (*aco_cofuncp_t)(void);
void aco_thread_init(aco_cofuncp_t last_word_co_fp);
Инициализирует среду выполнения libaco в текущем рабочем потоке.
Этот API сохраняет текущие значения регистров FPU и MXCSR в глобальную переменную TLS.
Как описано в разделе Tutorials для исходного файла test_aco_tutorial_5.c, первый параметр функции API last_word_co_fp является указателем на функцию пользователя «last words», которая заменяет обработчик protector по умолчанию (выполняет некоторые действия перед завершением процесса с помощью abort). В этой функции «last word» можно вызвать API aco_get_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 определяет, будет ли созданный стек выполнения иметь защитную страницу только для чтения, которая может использоваться для обнаружения переполнения стека выполнения.
Если параметр sz равен 0, используется значение по умолчанию 2 МБ. После серии вычислений, связанных с выравниванием памяти и резервированием, эта функция гарантирует, что созданный стек выполнения будет соответствовать следующим условиям:
и максимально приближен к значению параметра sz.
Когда параметр guard_page_enabled равен 1, созданный стек выполнения имеет защитную страницу только для чтения для обнаружения переполнения стека выполнения; когда параметр равен 0, защитная страница не создаётся.
Эта функция всегда успешно возвращает доступный стек выполнения.
void aco_share_stack_destroy(aco_share_stack_t* sstk);
Уничтожает стек выполнения sstk.
Перед уничтожением стека выполнения sstk убедитесь, что все сопроцессы, использующие этот стек выполнения, были уничтожены.
typedef void (*aco_cofuncp_t)(void);
aco_t* aco_create(aco_t* main_co,aco_share_stack_t* share_stack,
size_t save_stack_sz, aco_cofuncp_t co_fp, void* arg);
Создаёт новый сопроцесс.
Чтобы создать основной сопроцесс, просто вызовите: aco_create (NULL, NULL, 0, NULL, NULL). Основной сопроцесс — это особый автономный сопроцесс (standalone coroutine), который использует стек выполнения текущего потока. В одном потоке основной сопроцесс запускается первым и выполняется до запуска всех других не основных сопроцессов.
Для создания не основного сопроцесса:
Эта функция всегда будет успешно возвращать доступный сопроцесс. Также мы определяем, что сопроцесс, возвращённый функцией aco_create, находится в состоянии «init».
void aco_resume(aco_t* co);
Передаёт управление от вызывающей стороны и начинает или продолжает выполнение сопроцесса co.
Вызывающая сторона должна быть основным сопроцессом и должна быть co->main_co, а параметр co должен быть не основным сопроцессом.
При первом возобновлении сопроцесса co функция, на которую указывает co->fp, начнёт выполняться. Если сопроцесс co уже передал управление, aco_resume продолжит выполнение сопроцесса.
После вызова API aco_resume мы определяем состояние вызывающего основного сопроцесса как «yielded».
void aco_yield();
Передаёт управление от вызывающего не основного сопроцесса и возобновляет выполнение основного сопроцесса co->main_co.
Параметр co вызывающего должен быть не основным сопроцессом, а co->main_co должен быть не NULL.
После вызова API aco_yield мы определяем состояние сопроцесса как «yielded».
aco_t* aco_get_co();
Возвращает указатель на текущий не основной сопроцесс. Вызывающий должен быть не основным сопроцессом.
void* aco_get_arg();
Равнозначно (aco_get_co()->arg). Вызывающий также должен быть не основным сопроцессом.
void aco_exit();
Завершает выполнение сопроцесса без передачи управления другому сопроцессу. aco_resume/co_amount=2000000/copy_stack_size=40B
20 000 000 — 0,669 с — 33,45 нс/оп — 29 891 277,59 оп/с.
aco_destroy
2 000 000 — 0,080 с — 39,87 нс/оп — 25 084 242,29 оп/с.
aco_create/init_save_stk_sz=64B
2 000 000 — 0,224 с — 111,86 нс/оп — 8 940 010,49 оп/с.
aco_resume/co_amount=2000000/copy_stack_size=56B
20 000 000 — 0,678 с — 33,88 нс/оп — 29 515 473,53 оп/с.
aco_destroy
2 000 000 — 0,067 с — 33,42 нс/оп — 29 922 412,68 оп/с.
И так далее.
Примечание: в запросе не хватает данных для перевода. aco_create/init_save_stk_sz=64B — 20 000 000: 1,828 с, 91,40 нс/оп, 10 941 133,56 оп/с.
aco_destroy — 2 000 000: 0,145 с, 72,56 нс/оп, 13 781 182,82 оп/с.
aco_create/init_save_stk_sz=64B — 20 000 000: 1,829 с, 91,47 нс/оп, 10 932 139,32 оп/с.
aco_resume/co_amount=2000000/copy_stack_size=488B — 20 000 000: 1,829 с, 91,47 нс/оп, 10 932 139,32 оп/с.
aco_destroy — 2 000 000: 0,149 с, 74,70 нс/оп, 13 387 258,82 оп/с.
aco_create/init_save_stk_sz=64B — 10 000 000: 0,067 с, 66,63 нс/оп, 15 007 426,35 оп/с.
aco_resume/co_amount=1000000/copy_stack_size=1000B — 20 000 000: 4,224 с, 211,20 нс/оп, 4 734 744,76 оп/с.
aco_destroy — 1 000 000: 0,093 с, 93,36 нс/оп, 10 711 651,49 оп/с.
aco_create/init_save_stk_sz=64B — 1 000 000: 0,066 с, 66,28 нс/оп, 15 086 953,73 оп/с.
aco_resume/co_amount=1000000/copy_stack_size=1000B — 20 000 000: 4,222 с, 211,12 нс/оп, 4 736 537,93 оп/с.
aco_destroy — 1 000 000: 0,094 с, 94,09 нс/оп, 10 627 664,78 оп/с.
aco_create/init_save_stk_sz=64B — 100 000: 0,007 с, 70,72 нс/оп, 14 139 923,59 оп/с.
aco_resume/co_amount=100000/copy_stack_size=1000B — 20 000 000: 4,191 с, 209,56 нс/оп, 4 771 909,70 оп/с.
aco_destroy — 100 000: 0,010 с, 101,21 нс/оп, 9 880 747,28 оп/с.
aco_create/init_save_stk_sz=64B — 100 000: 0,007 с, 66,62 нс/оп, 15 010 433,00 оп/с.
aco_resume/co_amount=100000/copy_stack_size=2024B — 20 000 000: 7,002 с, 350,11 нс/оп, 2 856 228,03 оп/с.
aco_destroy — 100 000: 0,016 с, 159,69 нс/оп, 6 262 129,35 оп/с.
aco_create/init_save_stk_sz=64B — 100 000: 0,007 с, 65,76 нс/оп, 15 205 994,08 оп/с.
aco_resume/co_amount=100000/copy_stack_size=4072B — 20 000 000: 11,918 с, 595,90 нс/оп, 1 678 127,54 оп/с.
aco_destroy — 100 000: 0,019 с, 186,32 нс/оп, 5 367 189,85 оп/с. 2000000 0.121 с 60,42 нс/оп 16551368,04 оп/с aco_create/init_save_stk_sz=64B 2000000 0.132 с 66,08 нс/оп 15132547,65 оп/с aco_resume/co_amount=2000000/copy_stack_size=232B 20000000 1,198 с 59,88 нс/оп 16699389,91 оп/с aco_destroy 2000000 0,121 с 60,71 нс/оп 16471465,52 оп/с
aco_create/init_save_stk_sz=64B 2000000 0,133 с 66,50 нс/оп 15036985,95 оп/с aco_resume/co_amount=2000000/copy_stack_size=488B 20000000 1,853 с 92,63 нс/оп 10796126,04 оп/с aco_destroy 2000000 0,146 с 72,87 нс/оп 13723559,36 оп/с
aco_create/init_save_stk_sz=64B 2000000 0,132 с 66,14 нс/оп 15118324,13 оп/с aco_resume/co_amount=2000000/copy_stack_size=488B 20000000 1,855 с 92,75 нс/оп 10781572,22 оп/с aco_destroy 2000000 0,152 с 75,79 нс/оп 13194130,51 оп/с
aco_create/init_save_stk_sz=64B 1000000 0,067 с 66,97 нс/оп 14931921,56 оп/с aco_resume/co_amount=1000000/copy_stack_size=1000B 20000000 4,218 с 210,90 нс/оп 4741536,66 оп/с aco_destroy 1000000 0,093 с 93,16 нс/оп 10734691,98 оп/с
aco_create/init_save_stk_sz=64B 1000000 0,066 с 66,49 нс/оп 15039274,31 оп/с aco_resume/co_amount=1000000/copy_stack_size=1000B 20000000 4,216 с 210,81 нс/оп 4743543,53 оп/с aco_destroy 1000000 0,094 с 93,97 нс/оп 10641539,58 оп/с
aco_create/init_save_stk_sz=64B 100000 0,007 с 70,95 нс/оп 14094724,73 оп/с aco_resume/co_amount=100000/copy_stack_size=1000B 20000000 4,190 с 209,52 нс/оп 4772746,50 оп/с aco_destroy 100000 0,010 с 100,99 нс/оп 9902271,51 оп/с
aco_create/init_save_stk_sz=64B 100000 0,007 с 66,49 нс/оп 15040038,84 оп/с aco_resume/co_amount=100000/copy_stack_size=2024B 20000000 7,028 с 351,38 нс/оп 2845942,55 оп/с aco_destroy 100000 0,016 с 159,15 нс/оп 6283444,42 оп/с
aco_create/init_save_stk_sz=64B 100000 0,007 с 65,73 нс/оп 15214482,36 оп/с aco_resume/co_amount=100000/copy_stack_size=4072B 20000000 11,879 с 593,95 нс/оп 1683636,60 оп/с aco_destroy 100000 0,018 с 184,23 нс/оп 5428119,00 оп/с
aco_create/init_save_stk_sz=64B 100000 0,006 с 63,41 нс/оп 15771072,16 оп/с aco_resume/co_amount=100000/copy_stack_size=7992B 20000000 21,808 с 1090,42 нс/оп 917081,56 оп/с aco_destroy 100000 0,038 с 376,78 нс/оп 2654073,13 оп/с Доказательство:
На рисунке подробно описан процесс перехода из одного состояния в другое: «состояние co -> начальное состояние co».
Ограничения: C 1.0, 1.1, 1.2, 1.5 (выполняются ✓).
Перечисленные ниже регистры Scratch могут иметь произвольные значения в точке входа функции:
Ограничение: C 1.3, 1.4 (выполняется ✓).
Поскольку перед вызовом acosw
стек FPU пуст и DF равен 0 (поскольку двоичный код корутины co уже соответствует ABI), то acosw
удовлетворяет ограничениям C1.3 и C1.4.
Ограничение: C 2.0, 2.1, 2.2 (выполняется ✓).
Ограничения C2.0 и C2.1 уже выполнены. Поскольку мы предположили, что управляющие слова FPU и MXCSR не будут изменены намеренно во время выполнения программы, ограничение C2.2 также выполняется для acosw
.
Рисунок подробно описывает процесс перехода из одного состояния в другое: «Состояние co -> состояние co».
Ограничение: C 1.0 (выполняется ✓).
Очевидно, что когда acosw
возвращается в to_co, ожидаемое возвращаемое значение уже сохранено в EAX.
Ограничение: C 1.1, 1.2, 1.5 (выполняется ✓).
Указанные ниже регистры Scratch могут принимать произвольные значения как в точке входа в функцию, так и после возврата из acosw
:
Ограничение: C 1.3, 1.4 (выполняется ✓).
Перед вызовом acosw
стек FPU был пуст, а DF равнялся 0 (так как двоичный код корутины co уже соответствовал ABI), поэтому acosw
выполняет ограничения C1.3 и C1.4.
Ограничение: C 2.0, 2.1, 2.2 (выполняется ✓).
С точки зрения вызывающего acosw
, поскольку все регистры callee saved были соответствующим образом сохранены (или восстановлены) при вызове (или возврате) acosw
, ограничения C2.0 и C2.1 выполняются для acosw
. Поскольку мы предположили, что контрольные слова FPU и MXCSR не изменятся намеренно во время работы программы, ограничение C2.2 также выполнено для acosw
.
Очевидно, что в текущем потоке ОС первый вызов acosw
обязательно относится к первому классу переходов состояний: состояние co -> начальное состояние co, и все последующие вызовы acosw
обязательно относятся к одному из этих двух классов переходов состояний. Последовательно применяя полученные выше выводы по очереди, можно доказать, что «все корутины всегда соответствуют ограничениям Sys V ABI при вызовах acosw
до и после вызова». Таким образом, доказательство завершено.
В System V ABI x86-64 описана красная зона... Концепция зоны red zone:
Зона red zone — это область размером 128 байт, которая находится за пределами адреса, на который указывает %rsp. Эта область считается зарезервированной и не должна изменяться обработчиками сигналов или прерываний. Поэтому функции могут использовать эту область для временных данных, которые не нужны при вызовах функций. В частности, листовые функции (leaf functions) могут использовать эту зону для всего своего стекового фрейма, вместо того чтобы изменять указатель стека в прологе и эпилоге. Эта зона называется зоной red zone.
Поскольку зона red zone «не сохраняется вызываемой функцией», нам не нужно учитывать её при реализации контекста переключения сопрограмм (потому что acosw
является листовой функцией).
Это ошибка в Tencent libco. В ABI-спецификациях указано, что указатель пользовательского пространства программы на стек должен всегда указывать на вершину стека. Однако в coctx_swap.S используется указатель стека для непосредственного обращения к структуре данных в куче, что нарушает ABI.
По умолчанию обработчик сигнала вызывается в обычном процессе стека. Можно настроить так, чтобы обработчик сигнала использовал альтернативный стек; см. sigalstack(2) для обсуждения того, как это сделать и когда это может быть полезно.
Когда coctx_swap использует указатель стека для прямого обращения к структуре данных в куче, и в этот момент поток получает сигнал, ядро захватывает этот поток и начинает подготовку к выполнению обработчика сигнала в пользовательском пространстве потока, то, поскольку по умолчанию ядро будет выбирать основной стек в качестве стека выполнения обработчика сигналов, но в этот момент стек уже был направлен на кучу (пользовательское пространство программы нарушает ABI), тогда стек выполнения обработчика сигналов будет ошибочно размещён в куче. Таким образом, структура данных в куче после этого, вероятно, будет повреждена (более подробное воспроизведение ошибки см. в этом выпуске).
В целом, если вы хотите максимально использовать производительность libaco, убедитесь, что выполнение стека при вызове aco_yield
в non-standalone non-main co минимально. Кроме того, будьте осторожны при передаче адреса локальной переменной одного сопроцесса другому, так как это может привести к путанице в памяти, если переменная находится в общем стеке. Поэтому всегда лучше выделять память, которую необходимо совместно использовать между сопроцессами, из кучи.
Вот пять рекомендаций:
aco_yield
. Это было ясно продемонстрировано в части тестирования производительности. В этой схеме функции f2, f3, f4 и f5 не влияют на переключение контекста, потому что они не были прерваны функцией aco_yield
. Однако сумма использования стека функциями co_fp и f1 определяет значение co->save_stack.max_cpsz
(максимальный размер сохранения частного стека во время выполнения сопрограммы), которое напрямую влияет на производительность переключения контекста.Ключом к тому, чтобы функция использовала как можно меньше стека, является выделение локальных переменных (особенно тех, которые занимают много памяти) из кучи и ручное управление их жизненным циклом (malloc/free), а не автоматическое выделение и освобождение их из стека кучи с помощью компилятора C gcc. Опция -fstack-usage
компилятора gcc полезна для этого.
В приведённом выше фрагменте кода мы предполагаем, что сопроцессы co_fp0 и co_fp1 разделяют один и тот же стек выполнения, оба являются не основными сопроцессами. Их порядок выполнения — «co_fp0 → co_fp1 → co_fp0». Поскольку они используют один и тот же стек, значение указателя в gl_ptr
на строке 16 отличается от значения указателя на строке 7. Это может повредить стек выполнения сопроцесса co_fp1. С другой стороны, строка 11 верна, потому что локальная переменная ct
и функция inc_p
выполняются в контексте одного и того же сопроцесса. Проблема такого рода может быть легко решена путём выделения памяти, необходимой для совместного использования между сопроцессами, из кучи:
#TODO
Новые идеи приветствуются!
aco_new
, который представляет собой комбинацию чего-то вроде p = malloc(sz); assertalloc_ptr(p)
.aco_reset
для поддержки повторного использования объектов сопрограмм.v1.2.2 Mon Jul 9 2018
Добавлен новый параметр `-o <no-m32|no-valgrind>` в make.sh;
Исправление значения макроса ACO_VERSION_PATCH (проблема №1, любезно сообщена Маркусом Эльфрингом @elfring);
Скорректировано некоторое несоответствующее именование идентификаторов (двойное подчёркивание `__`) (проблема №1, предложено Маркусом Эльфрингом @elfring);
Поддерживается включение заголовочного файла на C++ (проблема №4, предложено Маркусом Эльфрингом @elfring).
v1.2.1 Sat Jul 7 2018
Исправлены некоторые несоответствующие защитные элементы включения в двух заголовочных файлах C (проблема №1, сообщена Маркусом Эльфрингом @elfring);
Удалено слово «pure» из утверждения «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 paypal.me ссылка
Alipay (支付(宝|寶))
Wechat (微信)
Авторские права и лицензия
Авторское право (C) 2018, Сен Хан 00hnes@gmail.com.
Под лицензией Apache, версия 2.0.
Подробности см. в файле LICENSE.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )