句柄(handle)
1 概述
本章节详细介绍 GS 中的可扩充类型 handle(句柄)机制,涵盖句柄的基本概念、内部结构、生命周期管理、线程安全机制以及在实际开发中的应用。重点解析句柄的资源管理原理、状态转换流程、防死锁策略,以及如何通过句柄机制安全地管理 object、domain 等核心资源。
主要面向需要了解 GS 句柄类型与资源管理的初学者,需要处理资源生命周期管理的开发者。
通过学习,读者将掌握句柄的工作原理、句柄的线程安全保证机制、正确的句柄使用方法,能够在复杂的多线程环境中安全高效地管理各种资源。具备在实际项目中正确运用句柄机制解决资源管理和线程安全问题的能力。
2 概念
2.1 什么是句柄?
句柄(Handle)是 GS 中用于管理资源的核心机制。可以把句柄想象成资源的"遥控器" - 你通过遥控器来安全地操作电视或者空调,而句柄让你安全地操作各种资源。
2.2 核心价值
- 线程安全:通过锁机制确保多线程环境下的数据安全。
- 资源管理:通过手动管理 + 垃圾收集辅助回收的方式管理,避免内存泄漏。
- 统一接口:所有资源类型都使用相同的管理接口。
2.3 句柄的类型体系
GS 中的多种资源类型都继承自句柄,形成统一的资源类型体系:
// 句柄类型继承关系
+-- handle -+-- object // 对象
|
+-- program // 程序
|
+-- domain // 域
|
+-- ref_value // 引用值
|
+-- coroutine // 协程
|
+-- timer // 定时器
|
+-- socket // 网络套接字
|
+-- share_value // 共享值
|
+-- sync_object // 同步对象
|
+-- queue // 队列
|
+-- file // 文件
|
+-- iterator // 迭代器
|
+-- archive // 归档
|
+-- weak_table // 弱引用表
一个各种资源通过句柄进行统一管理的示例如下:
void not_execute_me()
{
// 空函数,用于示例
}
// 创建不同类型的句柄资源
void handle_unified_management()
{
// 创建各种类型的句柄
handle h1 = socket.create(); // 网络套接字
handle h2 = domain.create(); // 域
handle h3 = timer.create(0.1, 0, (: not_execute_me :)); // 定时器
handle h4 = this_coroutine(); // 当前协程
handle h5 = share_value.allocate("unamed", 0); // 共享值
handle h6 = sync_object.create_semaphore(); // 信号量同步对象
// 统一使用 close() 方法关闭所有资源
h1.close();
h2.close();
h3.close();
h4.close();
h5.close();
h6.close();
printlnf("All handle resources have been safely closed");
}
handle_unified_management();
示例2-1:不同资源句柄管理的示例
句柄的核心价值之一就是于不同类型的资源(socket、domain、timer等)都通过相同的句柄接口进行管理,使用统一的 close()方法释放资源。
3. 生命周期
句柄有明确的生命周期状态,完整的状态流程如下:
INVALID → INITIALIZING → OPENED → PRE_CLOSING → CLOSING → CLOSED
↑ ↓ ↓ ↓ ↓ ↓
创建 初始化 正常使用 准备关闭 关闭中 已关闭
3.1 状态说明
handle有六个内置状态:INVALID, INITIALIZING, OPENED, PRE_CLOSING, CLOSING, CLOSED
- INVALID:为初始状态,此时的 handle 内存还没被分配,不可使用。
- INITIALIZING:创建一个新的句柄后,句柄就处于INITIALIZING状态,句柄初始化中,此时不能被 close 关闭。
- OPENED:对句柄进行Open操作后,句柄就处于OPENED状态,句柄可以被正常使用,此时可以被 close 关闭。
- PRE_CLOSING:全称(prepare to close),准备关闭阶段。当垃圾回收(GC)认为资源没有被使用时,handle 被置于准备会后阶段,并在之后尝试进行回收。
- CLOSING:关闭进行中状态。当资源被手动调用
close回收以及GC进行资源回收时进入此状态。在此状态下,handle正在执行关闭操作,不同句柄子类型在此状态所做的清理不同。 - CLOSED:关闭句柄后(调用句柄的close)成功后句柄处于CLOSED状态。
看起来状态很多,但不要害怕。绝大情况下使用handle的过程中,我们只需要关注一件事,及时的调用handle_instance.close()函数释放handle句柄管理的资源即可。
3.2 状态变化
句柄的状态变化示例:
// example.gs
void create()
{
// 在调用至create函数时,object 对象的句柄是已经成功创建了的,句柄的状态为 OPENDED
printf("handle state in 'create' function is %O\n", this_object().info().handle.state);
}
void destruct()
{
// 在调用句柄的 close 函数时,此函数被调用,此时句柄为 CLOSING 状态
printf("handle state in 'close' function is %O\n", this_object().info().handle.state);
}
// test.gs
import .example;
object ob = new_object(example, this_domain());
ob.close();
// 在句柄关闭完成后,句柄状态被转换为 CLOSED
printf("handle state after 'close' function is %O\n", ob.info().handle.state);
示例3-1:句柄的生命周期
示例的输出结果如下:
handle state in 'create' function is HandleState.OPENED(5)
handle state in 'close' function is HandleState.CLOSING(3)
handle state after 'close' function is HandleState.CLOSED(1)
4. 锁机制
共享变量利用了句柄内置的锁机制,我们使用该锁机制来确保多线程环境下的资源安全访问,示例如下:
#pragma parallel
parallel int counter := 0;
parallel share_value protected_counter := share_value.allocate("protected_counter", 0);
// 无锁保护 - 线程不安全
public void add_one()
{
counter := counter + 1; // 并行执行会导致结果错误
}
// 使用句柄锁保护 - 线程安全
public void lock_add_one()
{
try_lock(protected_counter) // 使用当前对象的句柄锁
{
int add_one = protected_counter.fetch_value() + 1;
protected_counter.put_value(add_one); // 受保护的临界区
}
}
void parallel_counter_test()
{
array coroutines = [];
// 测试有锁保护的版本
writeln("=== Lock-protected test ===");
// 创建1000个协程同时执行加操作
for(int i = 0; i < 1000; i++)
{
coroutines.push_back(coroutine.create_with_domain(
nil, domain.create(), (:lock_add_one:)
));
}
// 等待所有协程完成
for(coroutine co : coroutines)
{
co.wait();
}
writeln("Lock-protected result:", protected_counter.lock_fetch_value()); // 正确结果应为1000
// 测试无锁保护的版本
writeln("=== Unlocked test ===");
coroutines = [];
for(int i = 0; i < 1000; i++)
{
coroutines.push_back(coroutine.create_with_domain(
nil, domain.create(), (:add_one:)
));
}
for(coroutine co : coroutines)
{
co.wait();
}
writeln("Unlocked result:" + counter); // 结果通常小于1000
}
parallel_counter_test();
示例4-1:句柄的锁机制利用示例
示例输出如下:
=== Lock-protected test ===
Lock-protected result:1000
=== Unlocked test ===
Unlocked result:992
从示例的输出可以看到,利用了锁机制保护的多线程下的计数器加法得到了预取的结果1000,而没有锁保护的计数器加法通常会得到一个小于1000的值。除了share_value以外,其他继承自handle句柄的子类型也可通过try_lock()语句加锁利用其锁机制。
5. 防死锁
一个典型的死锁场景(哲学家就餐问题),一个描述死锁场景的简单示例如下:
#pragma parallel
parallel handle chopstick1 := sync_object.create_mutex();
parallel handle chopstick2 := sync_object.create_mutex();
void philosopher_A() {
chopstick1.lock(); // 拿到筷子1
coroutine.sleep(0.1); // 模拟一些操作
chopstick2.lock(); // 等待筷子2(但可能被B拿着)
// 就餐...
printf(HIG"Unlock all\n"NOR);
chopstick2.unlock();
chopstick1.unlock();
}
void philosopher_B() {
chopstick2.lock(); // 拿到筷子2
coroutine.sleep(0.1); // 模拟一些操作
chopstick1.lock(); // 等待筷子1(但被A拿着)
// 就餐...
printf(HIG"Unlock all\n"NOR);
chopstick1.unlock();
chopstick2.unlock();
}
void test_deadlock() {
coroutine co1 = coroutine.create_with_domain(0, domain.create(), (:philosopher_A:));
// coroutine.sleep(0.2); // 模拟极端情况下协程创建或执行延迟
coroutine co2 = coroutine.create_with_domain(0, domain.create(), (:philosopher_B:));
co1.wait();
co2.wait();
write(HIG"run success\n"NOR);
}
test_deadlock();
示例5-1:死锁问题示例
运行上述示例我们发现死锁产生,程序直接卡死,不是说句柄有机制可以避免死锁产生的吗?在这里我们需要明确的通过try_lock进行的加锁才是利用句柄的锁机制。而上述示例中通过调用mutex_instance.lock()是利用句柄下sync_object同步对象子类型中的锁机制进行的加锁。现在让我们修改代码如下:
#pragma parallel
parallel handle chopstick1 := share_value.allocate("chopstick1", 1);
parallel handle chopstick2 := share_value.allocate("chopstick2", 1);
void sleep()
{
for(int i = 0 upto 10000000)
{
}
return;
}
void philosopher_A() {
try_lock(chopstick1) // 拿到筷子1
{
sleep(); // 模拟一些操作, try_lock 中禁止调用 coroutine.sleep 以避免跨域,这里是一个次数较多的循环
try_lock(chopstick2) // 等待筷子2(但可能被B拿着)
{
// 就餐...
printf(HIG"Unlock all\n"NOR);
}
}
}
void philosopher_B() {
try_lock(chopstick2) // 拿到筷子2
{
sleep(); // 模拟一些操作, try_lock 中禁止调用 coroutine.sleep 以避免跨域,这里是一个次数较多的循环
try_lock(chopstick1) // 等待筷子1(但可能被A拿着)
{
// 就餐...
printf(HIG"Unlock all\n"NOR);
}
}
}
void test_deadlock() {
coroutine co1 = coroutine.create_with_domain(0, domain.create(), (:philosopher_A:));
// coroutine.sleep(0.2); // 模拟极端情况下协程创建或执行延迟
coroutine co2 = coroutine.create_with_domain(0, domain.create(), (:philosopher_B:));
co1.wait();
co2.wait();
write(HIG"run success\n"NOR);
}
test_deadlock();
示例5-2:句柄防死锁示例
运行实例发现我们加锁失败,报错Error(-10005): Inside share_value[2881108:v0] lock level (100) should be higher than current lock level (100).。此时我们要求内层锁的优先级高于外层锁的优先级,来强制统一了枷锁顺序,破坏了锁的循环等待条件。
句柄的防死锁解决方案
GS句柄通过独特的加锁方式来实现死锁的避免以下机制防止死锁:
- LockContext栈:以栈的形式管理lock/unlock操作。
- 锁排序:在加锁前对锁对象进行排序。
- 优先级检查:确保后加的锁优先级高于先前的。
6. 内存回收
在之前的章节了解到可以手动调用handle_instance.close()关闭及回收资源。但handle中的部分子类型,如object,调用handle_instance.close()关闭资源后会出现一种情况,尽管对象被关闭了,对象中的析构函数也同样被调用了,但内存却没有减少。一个简单的示例如下:
// example.gs
array data = array.allocate_dup(2000, get_system_info());
void create()
{
// 在调用至create函数时,object 对象的句柄是已经成功创建了的,句柄的状态为 OPENDED
printf("handle state in 'create' function is %O\n", this_object().info().handle.state);
}
void destruct()
{
// 在调用句柄的 close 函数时,此函数被调用,此时句柄为 CLOSING 状态
printf("handle state in 'close' function is %O\n", this_object().info().handle.state);
}
// test.gs
import .example;
array arr = nil;
public void init_arr()
{
arr = [];
}
public void construct_ob()
{
for(int i = 0 upto 10000)
{
object ob = new_object(example, this_domain());
arr.push_back(ob);
ob.close();
}
}
init_arr();
construct_ob();
gc.start(true);
示例6-1:简单的内存泄漏示例
示例中我们创建了10000个带有一定数据量的对象,并在创建后调用handle_instance.close()关闭资源。运行结束后我们在控制台输入如下指令查看 driver 整体占用内存字节数。
'mem_stat(true).total_used_size
指令的输出如下:
973425152
900MB以上,明显不对劲。我们创建的object 对象尽管已处于CLOSED状态,但对应的内存却并没有被回收。此时输入如下指令:
'mem_profiler.closed_objects(true);
从输出中我们可以看到,大量处于对象以销毁状态但未被回收的object对象。在之前的内存管理中,我们知晓了只有当一个对象不再被任何引用时,也就是其不可达时,垃圾回收器才会回收其内存。现在这种状态,说明仍有对这些对象的合法的引用,有可达路径。
此时再会看我们的代码示例,我们将所有example.gs的对象都放入了一个归属于test.gs对象的数组中,启动加载的test.gs是一个必须手动释放的静态对象。由此,我们有一个 test.gs(opened对象) -> 对象变量 arr 数组 -> 应被释放的 example.gs (closed对象)的引用路径。导致example.gs对象内存空间未被释放。打破该引用链条的任意一点即可。
在driver中输入如下指令:
Shell> test.init_arr() // 打破引用链条中数组对 example.gs 对象的引用
Shell> gc.start(true) // 手动触发一次 full gc
Shell> 'mem_stat(true).total_used_size
91380416
从指令的输出结果可以看到,总的内存占用变为 100MB 以下,example.gs 对象的内存被垃圾回收器回收释放。
7. 拓展阅读
更多句柄相关的内容,请参照如下文档:
8. 总结
句柄(Handle)是GS中资源管理的核心机制,它为所有资源类型提供了统一的生命周期管理和线程安全接口。其核心价值在于通过明确的状态转换(如OPENED、CLOSING)和统一的close()方法,简化了资源管理。更重要的是,其内置的锁机制和独特的try_lock语句,在提供多线程安全访问的同时,通过强制锁排序有效避免了死锁的发生。
在实际开发中,最关键的原则是及时调用close()方法手动释放资源,并理解手动关闭与垃圾回收(GC)的关系:手动关闭将资源状态置为CLOSED,而GC负责回收其内存,但前提是该资源已不可达。因此,必须注意打破无意的引用链,防止内存泄漏。
总而言之,句柄机制是编写健壮、高效GS程序的基石。掌握其生命周期、锁机制和正确的资源释放方法,是确保应用在多线程环境下稳定运行的关键。