协程(coroutine)
1. 概述
本章节将系统性地介绍 GS 语言中的协程(Coroutine) 机制。协程是 GS 中用于实现并发、并行和协作式多任务的基本单元。本章内容面向已经学习过域、函数指针概念的 GS 语言初学者或希望了解或回顾 GS 协程基础的同学。通过本章学习,您将掌握 GS 协程的核心概念、创建方法、调度规则以及实际应用技巧。
2. 协程基础
2.1 协程的概念
在GS语言中,协程(Coroutine) 是并发编程的基本执行单元,它代表了一个可以暂停和恢复执行的轻量级任务流。与传统编程语言中的线程或简单的协程不同,GS协程融合了"域(Domain)"的概念,形成了一个独特而强大的并发编程范式。
GS中每个协程都是一个独立的执行流,拥有自己的执行上下文,但比传统操作系统线程更加轻量,可以创建成千上万个协程而不会导致系统资源耗尽。协程之间的切换通常由GS运行时系统管理,而不是操作系统内核,这使得上下文切换的成本更低。GS中的协程一般都需要在调度器的安排下,在有限的线程中排队执行。为了尽可能减少协程切换时的上下文切换开销,在正常执行流程中,GS的调度器不会主动打断协程执行,而是要等待协程主动让出(例如遇到锁、执行 coroutine.sleep 等)。
GS 不 同域的协程是可以并行执行,GS运行时会尽可能地将它们调度到不同的操作系统线程上,充分利用多核处理器的计算能力。这种"域内串行、域间并行"的模型既保证了数据访问的安全性,又提供了真正的并行计算能力。
2.2 协程的创建
协程可以通过coroutin.create、coroutine.create_with_domain或coroutine.create_in_thread三种函数创建,除了指定协程名称与协程的入口函数以外,这几个协程创建函数的区别在在于是否能指定协程的执行域以及是否能指定协程在固定的线程执行。
协程可以通过coroutine.create系列的3个函数创建;除了必须指定的协程名称(可以是匿名)和入口函数相同以外,这几个函数的区别在于可以指定协程的执行域和指定调度这个协程的OS线程。接下来分别用样例介绍:
coroutine create(mixed name, string|function name_or_func, ...)此方法创建的协程执行域会被自动指定,相应规则如下:
-
创建协程时,使用入口函数的参数域做为协程执行域。
-
若入口函数的参数域为
nil时,使用创建时的当前域做为协程的执行域 -
使用默认的OS线程调度
// test.gs
#pragma parallel // 带有此选项的object下所有函数均为 parallel 的
public void test()
{
// 故意指定函数的参数域
function fn1 = (: co_entry, 1:);
fn1.bind_arg_domain(domain.create());
// 无参数域
function fn2 = (: co_entry, 2:);
// 创建协程
coroutine co1 = coroutine.create(nil, fn1);
coroutine co2 = coroutine.create(nil, fn2);
// 协程执行域就是函数的参数域
printf("----------------------\n");
printf("init domain :%M\n", this_domain());
printf("co1 entry domain: %M\n", co1.get_entry_domain());
printf("fn1 arg domain: %M\n", fn1.get_arg_domain());
printf("co2 entry domain: %M\n", co2.get_entry_domain());
printf("fn2 arg domain: %M\n", fn2.get_arg_domain());
printf("----------------------\n");
}
void co_entry(int n)
{
printf("+++++++++++++++++++++++++++\n");
printf("coroutine %d, entry domain: %M\n", n, this_domain());
printf("+++++++++++++++++++++++++++\n");
}
test();
示例2-1:coroutine.create 创建协程示例
输出结果如下:
----------------------
init domain :domain[2048:v0]zero
co1 entry domain: domain[2883348:v0]~d2883348:v0@(test@/test/test.gs)
fn1 arg domain: domain[2883348:v0]~d2883348:v0@(test@/test/test.gs)
co2 entry domain: domain[2048:v0]zero
fn2 arg domain: nil
----------------------
+++++++++++++++++++++++++++
coroutine 1, entry domain: domain[2883348:v0]~d2883348:v0@(test@/test/test.gs)
+++++++++++++++++++++++++++
Welcome driver shell.
GS 1.35.250803 Copyright (C) G-bits
Shell> +++++++++++++++++++++++++++
coroutine 2, entry domain: domain[2048:v0]zero
+++++++++++++++++++++++++++
从输 出结果可以看到,由绑定了参数域的函数指针f1创建的co1的执行域与f1的参数域相同。未绑定参数域,参数域为nil的函数指针f2创建的协程co2使用了当前参数域zero作为协程执行域。之后入口函数在相应的协程与域中执行,可以看到函数的执行域被输出。
coroutine.create_with_domain(mixed name, domain domain, mixed name_or_func, ...): 创建协程时,使用参数指定的域做为协程执行域(entry domain)。
- 协程执行域必须通过参数手动指定。
- 使用默认的OS线程调度。
- 入口函数的参数域必须与协程执行域一致(相同或者为nil)
// test.gs
#pragma parallel
public void test()
{
domain d = domain.create("xxx");
// 故意指定函数的参数域
function fn1 = (: co_entry :);
fn1.bind_arg_domain(d);
// 无参数域
function fn2 = (: co_entry :);
// 有参数域但是和协程执行域不一致
function fn3 = (: co_entry :);
fn3.bind_arg_domain(this_domain());
// 创建协程
coroutine co1 = coroutine.create_with_domain(nil, d, fn1);
coroutine co2 = coroutine.create_with_domain(nil, d, fn2);
// 这个创建要发生错误
coroutine co3;
catch(co3 = coroutine.create_with_domain(nil, d, fn3));
// 协程执行域就是函数的参数域
printf("----------------------\n");
printf("this domain:%M\n", this_domain());
printf("co1 entry domain: %M\n", co1.get_entry_domain());
printf("fn1 arg domain: %M\n", fn1.get_arg_domain());
printf("co2 entry domain: %M\n", co2.get_entry_domain());
printf("fn2 arg domain: %M\n", fn2.get_arg_domain());
printf("----------------------\n");
}
void co_entry()
{
coroutine.sleep(1);
printf("+++++++++++++++++++++++++++\n");
coroutine co = this_coroutine();
printf("entry domain: %M\n", co.get_entry_domain());
printf("+++++++++++++++++++++++++++\n");
}
test();
示例2-2:coroutine.create_with_domain 创建协程示例
输出结果如下:
Error(-1): The function 'co_entry' can not be called in domain[2887444:v0]xxx, expected to be called in domain[2048:v0]zero
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At unknown:0 (create_with_domain) in object[2872913:v0]/test/test.gs
Argument name = nil
Argument domain = domain[2887444:v0]xxx
Argument name_or_func = (: .co_entry :)object[2872913:v0]/test/test.gs<Parallel>
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:25 (test) in object[2872913:v0]/test/test.gs
Variable d = domain[2887444:v0]xxx
Variable fn1 = (: .co_entry :)object[2872913:v0]/test/test.gs<Parallel>
Variable fn2 = (: .co_entry :)object[2872913:v0]/test/test.gs<Parallel>
Variable fn3 = (: .co_entry :)object[2872913:v0]/test/test.gs<Parallel>
Variable co1 = co[2937168:v0]anonymous
Variable co2 = co[2937208:v0]anonymous
Variable co3 = (optimized)
Variable $catch_ret_0 = nil
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:47 (::entry) in object[2872913:v0]/test/test.gs
At <Internal routine>
----------------------
this domain:domain[2048:v0]zero
co1 entry domain: domain[2887444:v0]xxx
fn1 arg domain: domain[2887444:v0]xxx
co2 entry domain: domain[2887444:v0]xxx
fn2 arg domain: nil
----------------------
Welcome driver shell.
GS 1.35.250803 Copyright (C) G-bits
Shell> +++++++++++++++++++++++++++
entry domain: domain[2887444:v0]xxx
+++++++++++++++++++++++++++
+++++++++++++++++++++++++++
entry domain: domain[2887444:v0]xxx
+++++++++++++++++++++++++++
从示例的输出结果可以看到,以coroutine.create_with_domain方式创建的协程必须手动指定协程执行域。协程执行域与函数指针参数域冲突的情况下,比如函数指针fn3参数域为zero协程co3却指定执行域为xxx此时协程的创建就会报错函数不能在xxx域执行。其他的情况中,fn1函数指针参数域与协程co1的执行域相同。fn2函数指针无参数域,协程co2不会出现域冲突问题。
coroutine.create_in_thread(mixed name, domain domain, string|function name_or_func, ...):对于一些实时性要求较高,不希望堵在调度器里的任务,一般会使用 coroutine.create_in_thread 创建独立于调度器的协程,这种协程会在一个独立的线程上运行,服从操作系统的调度。
- 创建协程时,使用参数指定的域做为协程执行域
- 特别的,为这个协程准备一个单独的OS线程进行调度
create_in_thread创建的独立调度的协(线)程数量是有数量限制的'get_system_info().max_os_threads指令可以获取最大允许的协(线)程数量
// test.gs
#pragma parallel
public void test()
{
domain d = domain.create("xxx");
// 故意指定函数的参数域
function fn1 = (: co_entry :);
fn1.bind_arg_domain(d);
// 创建协程
coroutine co1 = coroutine.create_in_thread(nil, d, fn1);
// 协程执行域就是函数的参数域
printf("----------------------\n");
printf("this domain:%M, d: %M\n", this_domain(), d);
printf("co1 entry domain: %M\n", co1.get_entry_domain());
printf("fn1 arg domain: %M\n", fn1.get_arg_domain());
printf("----------------------\n");
}
void co_entry()
{
coroutine.sleep(1);
printf("+++++++++++++++++++++++++++\n");
coroutine co = this_coroutine();
printf("entry domain: %M\n", co.get_entry_domain());
printf("+++++++++++++++++++++++++++\n");
}
test();
示例2-3:coroutine.create_in_thread 创建协程示例
输出结果如下:
----------------------
this domain:domain[2048:v0]zero, d: domain[2879252:v0]xxx
co1 entry domain: domain[2879252:v0]xxx
fn1 arg domain: domain[2879252:v0]xxx
----------------------
2.3 协程的返回值
在协程结束后,通过get_ret函数获取协程的返回值,只要协程入口函数原型有声明返回值且在协程预处CoState.ENDING状态之后,就可以通过co.get_ret()获取这个返回值,这样大大方便了多线程相关业务的开发,避免需要额外利用queue、share_value或额外的回调方法去同步协程的结果,示例如下:
map co_get_data()
{
coroutine.sleep(0.5);
return make_parallel(get_system_info());
}
coroutine co = coroutine.create(nil, (: co_get_data :));
co.wait();
write(co.get_ret());
示例2-4:协程的返回值获取
可以看到通过协程返回值获取了system_info,结果为:
{ /* sizeof() == 56 */
"version" : "1.0.220829.01",
"run_in_main_thread" : false,
"first_execution" : true,
"git_commit_version" : "784cd0ccb2dc980199160a99e6d9661a5de609d2 @ origin/master @ 2022-09-16 07:27:53 +0000",
...后续略...
}
要注意的是,返回值如果是引用类型,必须为parallel或constant,以避免产生并发访问问题
3. 协程与域
在之前的章节中我们已经了解过域(Domain)的基础概念。GS 的协程必须在域(Domain)内执行,且同一时间一个域中仅可有一个协程执行。相关规则如下:
- 一个域下可以有多个协程,但同一时刻只能有一个协程在该域内执行。
- 不同域的协程可以并行执行。
- 同一域的两个协程不能同时执行。
- 协程被挂起(如调用
coroutine.sleep)时,会释放占有的域,此时其他协程即可尝试进入该域并执行相关逻辑。
3.1 同域串行
让我们通过一些样例来了解协程与域的关系,这是一个两个不会让域的协程在同一域执行的样例:
// test.gs
#pragma parallel
void task(string name, int duration, string format) {
for(int i = 0 upto 7)
{
printlnf(format + "[%s] output %d"NOR, name, i);
// coroutine.sleep(0.1);
}
}
void test()
{
domain shared_domain = domain.create(); // 创建一个共享域
// 创建两个协程,都指定在同一个 shared_domain 中执行
coroutine co1 = coroutine.create_with_domain("coroutine_A", shared_domain, (: task, "coroutine_A", 1, HIG:));
coroutine co2 = coroutine.create_with_domain("coroutine_B", shared_domain, (: task, "coroutine_B", 2, HIM:));
co1.wait(); // 等待协程A完成
co2.wait(); // 等待协程B完成
}
test();
示例3-1:两个不会让域的协程在同一域中执行
输出如下:
[coroutine_A] output 0
[coroutine_A] output 1
[coroutine_A] output 2
[coroutine_A] output 3
[coroutine_A] output 4
[coroutine_A] output 5
[coroutine_A] output 6
[coroutine_A] output 7
[coroutine_B] output 0
[coroutine_B] output 1
[coroutine_B] output 2
[coroutine_B] output 3
[coroutine_B] output 4
[coroutine_B] output 5
[coroutine_B] output 6
[coroutine_B] output 7
运行示例的输出中可以看到协程A或协程B在同一个域中轮流执行。同一协程中的循环输出总是连续的,总是8个[coroutine_A]输出或者8个[coroutine_B]输出连续出现。