域(domain)
1.概述
本章将探讨 GS 语言中的核心概念——域(Domain)。域作为对象的执行环境和保护机制,是理解 GS 并发编程和对象安全的关键。我们将学习域的基本概念、创建方式、与对象的关系,以及如何通过域机制解决并发访问问题。
本章面向已经了解 GS 对象基础,希望深入理解并发安全和执行机制的开发者。
通过阅读本章,您将能够理解域的保护机制及其重要性,掌握域的创建和管理方法,学会正确使用跨域调用和域合并技术,并能够诊断和解决域冲突问题。
2. domain 基础
2.1 domain 的概念
在 GS 中,域(domain)就是区域或范围的意思,可视为一把大锁,用于在并发场景下,保护域所辖属的范围内的数据安全性。其实际等价于其他语言中用mutex管理一堆变量,只是做成了语言机制,方便代码撰写,这是GS的特色。
- GS的任意变量(对象句柄也是变量)都必然隶属于某个域,局部变量亦不能例外
- GS的任意变量,不能在其所属域外使用,跨域必然产生拷贝。只读变量例外,因为不能修改,拷贝和引用无差别。
- 域的出现主要是起到了一个保护对象数据安全的作用,它相当于一个内部实现的互斥锁。
- 如果一个对象不是只读对象,一旦有一个协程调用到这个对象,它就会给对象加锁,这时候其它协程就不能去调用这个对象, 直到这个协程执行完毕,再给对象解锁,此时其它协程才可以调用这个对象。
2.2 domain 的创建与使用
2.2.1 域的创建
通过调用domain.create("domain_name")函数可以创建指定名称的域,通过调用this_domain()可以获得当前的域。示例如下:
// example.gs
public void hello_world()
{
writeln("Hello world.");
}
import .example;
// 创建指定名称的域
handle dom = domain.create("example_domain");
printlnf("New created domain is '%O'", dom);
// 在特定域中创建对象
object obj = load_static(example, dom);
printlnf("Domain of object '%O' is '%O'", obj, obj.get_domain());
// 获取当前域
domain current_dom = this_domain();
printlnf("Current domain is '%O'", current_dom);
示例2-1:域的创建
示例的输出如下:
New created domain is 'domain[2988820:v0]example_domain'
Domain of object 'object[2881105:v0]/example.gs<Static>' is 'domain[2988820:v0]example_domain'
Current domain is 'domain[2048:v0]zero'
从示例的输出结果可以看到我们创建了一个名称为example_domain域,并在example_domain域内创建了一个新的example.gs对象实例。而函数执行的当前域与新建的域不同是一个名称为zero的域。
2.2.2 域的执行控制
需要注意,我们以RW(读写) 对象、RW变量为基础来介绍并发情况下域的执行控制机制。至于RO(只读或并行)对象及并行方法的相关内容会在后续的并发编程 章节详细介绍。
在之前的object 章节我们已经了到object在创建时都需要绑定所在域,若创建object实例时 domain 参数为 nil,则会绑定至 coroutine(协程)的当前域。而 object绑定域的主要目的是保证object中 RW成员变量的安全性。当 object 变量声明前没有 #pragma readonly 或 #pragma parallel 关键字时该 object 变量即为 RW 变量,如以下示例中的变量num:
// example.gs
int num = 0;
public void hello()
{
printf(HIG "coroutine domain is %O\n" NOR, this_domain());
printf(HIG "object domain is %O\n" NOR, this_object().get_domain());
printf(HIG "num is %d\n" NOR, num);
}
// test.gs
import .example;
void test()
{
domain new_domain = domain.create();
printf(HIY "coroutine domain is %O\n" NOR, this_domain());
printf(HIY "new domain is %O\n" NOR, new_domain);
object ob = new_object(example, new_domain);
ob.hello();
}
test();
示例2-2:RW 变量示例
- 若要访问或修改 RW 的 object 变量,必须占用 example.gs object 所绑定的域。
- 上述 test.gs 示例中
ob.hello();的调用会失败,因为域不同。 - example.gs 在另一个域
new_domain中创建,不跨域的.调用不能使 coroutine 占域用从zero domain切换至new_domain
修正方式1,可以修改 test.gs 第8行将 example object 创建在 coroutine 当前域中。
object ob = new_object(example, this_domain());
修改后输出的结果如下:
coroutine domain is domain[2048:v0]zero
new domain is domain[2891540:v0]~d2891540:v0@(test@/test.gs)
coroutine domain is domain[2048:v0]zero
object domain is domain[2048:v0]zero
num is 1
可以看到修改后 example object 所在域为 zero,coroutine初始所在域也为 zero, 这样 coroutine 可以直接调用 ob.hello() 函数,而不需要额外的切域操作。
修正方式2:也可以修改第9行为跨域调用
ob=>hello();
其输出结果如下:
coroutine domain is domain[2048:v0]zero
new domain is domain[2974484:v0]~d2974484:v0@(test@/test/test.gs)
coroutine domain is domain[2974484:v0]~d2974484:v0@(test@/test/test.gs)
object domain is domain[2974484:v0]~d2974484:v0@(test@/test/test.gs)
num is 777
可以看到example object 所在域为 domain[2974484:v0]~d2974484:v0,coroutine初始所在域为 domain[2048:v0]zero, 当进行跨域调用 ob=>hello() 时,coroutine让出初始所在域domain[2048:v0]zero,切换域换至example object 所在域 domain[2974484:v0]~d2974484:v0,执行函数。
若要调用一个使用了 RW 变量的 object 成员函数,coroutine 则必须切入 object 所属的域中,这也意味着若有多个的 coroutine ,它们只能并发而无法真正的并行进行程序处理,并发并行概念详见多线程开发。
很好,看完脑袋已经有点乱了,现在总结下示例展示的内容,即:
- 一个域同一时间只能有一个协程运行。
- 一个协程同一时间最多持有一个域。
- 若域B被某个协程B占用,另外一个协程A就无法持有该域B,进而也就无法访问或修改该域B保护的数据。
- 协程A必须等待协程B释放该域B后才能进入该域。
- 在协程A尝试切入另一个域B的时必须释放其持有的当前域A。
所以如果要访问或修改 example.gs 的对象实例数据,我们必须和该对象实例处在一个域中。要么我们原本就在同一个域, 要么我们需要调用跨域调用函数放弃当前持有的域尝试切入该对象实例绑定的域。
2.2.3 并行问题
既然我们必须拿到域才能修改或访问域所保护的数据,就像必须要加锁才能操作数据,那么事实上所有数据处理都是串行的,并不是真正的并行处理。难道GS没有真正的并行数据处理机制?当然不是。
上述域的执行控制内容均建立在一个基础上,即是所有对象、对象变量、方法均是(RW)读写的。
但事实上GS中的对象、对象变量、函数方法也可以是(Parallel)并行或 (Readonly)只读的。在后续的并发编程章节,我们会详细介绍在 GS 中如何真正的并行运行程序。
2.2.4 跨域调用机制
当访问RW对象中的not paralle方法,调用代码的域与RW对象不在同一个域中(无法使用.型调用的地方),需要使用=>类型的跨域调用。一个简单的跨域调用示例如下:
// example.gs
int i = 0;
public void test1()
{
i = i + 1;
printf("i = %d\n", i);
}
public void test2()
{
i = i - 1;
printf("i = %d\n", i);
}
// test.gs
import .example;
handle dom = domain.create("example_domain");
object obj = load_static(example, dom);
// 在dom上执行invoke_func_in_dom
coroutine.create_with_domain("example_co", dom, (: invoke_func_in_dom, obj :));
parallel void invoke_func_in_dom(object ob)
{
ob.test1(); // 调用此处时,当前域和对象所在的域一致,因此不需要跨域调用
}
obj=>test2(); // 跨域调用时一定要用跨域(=>)调用,使用(.)调用会出错
示例2-3:跨域调用示例
当一个协程调用obj的非parallel方法时,会尝试进入obj所在的域,而一个域只能有一个协程运行,另外一个协程就无法执行obj的方法了。因此在这个例子中,ob.test1 与 ob.test2 俩个函数不会同时执行。
如果没有domain对object进行保护,两个协程有可能同时取出i的值(此时为0),然后分别计算+1或-1之后再写入,最终结果就可能变成1或者-1甚至是其他值(可以参考其他语言的线程安全问题/原子操作等)
2.2.5 跨域调 用的深拷贝
跨域调用另一个域中的对象的函数方法时参数与返回值行为
- 引用类型**(array,map 等 ValueType > ValueType.STRING 类型**)的参数从调用域传入跨域对象的域时会被深度复制;
- 引用类型的返回值从跨域对象的域返回调用域时会被深度复制;
示例如下:
// ob.gs
map _dbase = {};
public void set(map m, string path, int v)
{
m.express_set(path, v);
printf("ss: m=%O\n", m);
}
public void set_dbase(string path, int v)
{
_dbase.express_set(path, v);
}
public map get_dbase()
{
return _dbase;
}
import .ob;
// test.gs
public void test()
{
// 跨域时传入的引用类型参数自动进行了深度复制
map m = {};
m.express_set("a/b", 100);
printf("1: m=%O\n", m);
ob=>set(m, "a/b", 200);
printf("2: m=%O\n", m);
// 跨域时返回的引用类型返回值进行了深度复制
ob=>set_dbase("c/d", 300);
map dbase = ob=>get_dbase();
printf("11: dbase=%O\n", dbase);
dbase.express_set("c/d", 400);
printf("22: dbase=%O\n", dbase);
printf("33: dbase=%O\n", ob=>get_dbase());
}
load_static("./test.gs", domain.create());
load_static("./ob.gs", domain.create());
test=>test();
示例2-4:跨域调用深复制示例
输出结果如下:
1: m={ /* sizeof() == 1 */
"a" : { /* sizeof() == 1 */
"b" : 100,
},
}
ss: m={ /* sizeof() == 1 */
"a" : { /* sizeof() == 1 */
"b" : 200,
},
}
2: m={ /* sizeof() == 1 */
"a" : { /* sizeof() == 1 */
"b" : 100,
},
}
11: dbase={ /* sizeof() == 1 */
"c" : { /* sizeof() == 1 */
"d" : 300,
},
}
22: dbase={ /* sizeof() == 1 */
"c" : { /* sizeof() == 1 */
"d" : 400,
},
}
33: dbase={ /* sizeof() == 1 */
"c" : { /* sizeof() == 1 */
"d" : 300,
},
}
从输出结果我们可以看到1:与2:的表输出结果并未改变,跨域调用的ob=>set函数实际修改的是深复制的另一个 map 表实例。
ob=>set_dbase("c/d", 300)函数调用后11:处的 map 表发生改变是因为我们此时用 m_base变量接受了该跨域调用深复制后的返回值。
dbase.express_set("c/d", 400);语句调用外部函数express_set后修改当前域map表后,22:对应输出可以看到表中键"d"对应的输出被调整为了400。但跨域调用ob=>get_dbase()获取到的另一个域中的 map 表该键值对应数据仍为 300。很好的说明了返回的引用也是被深复制的。
2.2.6 跨域调用选择
访问一个对象(下面用ob代替该对象)的方法是不是需要跨域调用(也就是=>调用),取决于几个因素:
- 设计目的;
- 调用栈的当前域(简称当前域): this_domain();
- 被调用对象所属的域(简称对象域): ob.get_domain();
- 被调用对象的方法是不是一个并行的方法(简称并行方法): 函数用parallel修饰;
| 条件 | 是否需要跨域调用 |
|---|---|
| 当前域和对象域相同时 | 否 |
| 调用的是并行方法时 | 否 |
| 当前域和对象域不同,且调用的不是并行方法时 | 是 |
3. 扩展阅读
更多域的相关内容,如域的常用函数如查找域、获取所有域、获取域的信息。以及跨域的实现细节、参数域、其他核心概念内容、并行并发内容可参阅以下文档:
4. 总结
本章系统介绍了 GS 中域(Domain)的核心概念与工作机制。我们深入理解了域作为并发安全的核心机制,通过为对象提供独立的执行环境和数据保护,有效解决了多协程环境下的资源竞争问题。域通过内置的锁机制确保同一时间只有一个协程能够访问其保护的资源,从而保证了数据的一致性和线程安全。
我们学习了域的创建与管理方法,掌握了跨域调用的正确使用方式,理解了引用类型参数和返回值在跨域调用时的深度复制行为。通过自动化的域锁管理和串行化执行,GS 确保了对象数据在并发环境下的安全访问。
需要注意的是,本章讨论的域保护机制主要针对读写(RW)对象和变量。在实际开发中,还可以通过只读(Readonly)和并行(Parallel)机制来实现真正的并行