跳到主要内容
版本:master

共享变量(share_value)

1. 概述

共享变量作为一种支持多线程访问的线程安全数据容器,是并发编程中实现高效数据共享的核心工具。本章节从基础概念入手,系统讲解了共享变量的创建方法、加锁访问机制(包括基本写锁和读锁优化)、关键数据操作(写入、删除、查询)以及性能优化技巧,并通过丰富的代码示例演示了其在实际并发场景中的应用。

本文档适用于所有 GS 语言开发者,特别是那些需要进行多协程并发编程、设计高并发系统或优化并行程序性能的同学。

通过阅读本文档,您将能够理解共享变量在 GS 并发模型中的定位和核心价值。掌握共享变量的正确创建方法和加锁访问模式。学会根据场景选择合适的锁类型(写锁/读锁)以优化性能。熟练使用各类数据操作方法,并避免常见的性能陷阱。在实际项目中正确应用共享变量解决数据竞争和状态共享问题。

2. 基础

2.1 概念

共享变量(share_value)是一个一个支持多线程并行访问的、线程安全的数据容器。共享变量的设计是为了在并行环境中,为多个并行或并发协程提供一种安全、高效的数据共享机制,避免数据竞争。GS 中共享变量的核心特性如下:

  • 锁集成:内置锁机制,所有访问都必须通过锁(try_lock)进行。
  • 读写锁优化:支持共享读锁(try_lock(read : sv))和独占写锁,提升读多写少的场景性能。
  • 数据深拷贝:放入共享变量或从共享变量取出时,包括share_value.allocateshare_value.fetch_valueshare_value.put_value等操作,值会进行深拷贝。

2.2 创建

通过外部函数share_value.allocate(string name, mixed val)创建,且声明时通常带有只读readonly或并行parallel关键字。用于在不同的域,不同的协程中并行访问。

share_value.allocate(string name, mixed val)参数说明

  • name:字符串类型,share_value 的名称标识符
  • val:混合类型,初始值

共享变量的创建示例如下:

#pragma parallel

// 创建 share_value,初始值为 0
readonly handle sv := share_value.allocate("counter", 0);

// 创建包含复杂数据的 share_value
readonly handle config_sv := share_value.allocate("config", {
"max_connections": 100,
"timeout": 30,
"enabled": 1
});

实例2-1:共享变量的创建

3. 加锁操作

共享变量(share_value)在访问或修改时需要进行try_lock操作,以持有共享变量的内部锁并访问或修改锁保护的共享变量数据。

  • 要读写share_value前,要进行 try_lock 操作:try_lock(sv) {...}

  • try_lock也支持只读访问形式:try_lock(read : sv) {...} 。这样可以多个线程并行处理,其底层实现是使用读写锁,但是这种读锁的方式只能使用有限的读方法,比如fetch_valueget等,不能使用如put_valueset这样的写方法。

    • 读锁(共享锁)

      • 多个协程可以同时获取读锁。
      • 读操作期间允许其他读操作并行执行。
      • 只有当所有读锁释放后,写锁才能获取。
    • 写锁(独占锁)

      • 同一时间只允许一个协程获取写锁。
      • 写操作期间阻塞所有其他读写操作。
      • 保证数据修改的原子性和一致性。

3.1 基本写锁

一个添加基本写锁对计数器进行保护的示例如下:

#pragma parallel
readonly handle counter := share_value.allocate("counter", 0);

void increment_counter()
{
// 使用写锁(独占锁)
try_lock(counter)
{
int current = counter.fetch_value(); //在锁保护中,使用`fetch_value`深拷贝取数据
counter.put_value(current + 1);
printf("Counter incremented to: %d\n", current + 1);
}
}

void decrement_counter()
{
try_lock(counter)
{
int current = counter.fetch_value(); //在锁保护中,使用`fetch_value`深拷贝取数据
counter.put_value(current - 1);
printf("Counter decremented to: %d\n", current - 1);
}
}

// 测试代码
void test_counter()
{
// 创建多个协程并发操作计数器
array cos = [];
for (int i = 0; i < 100; i++)
{
cos.push_back(coroutine.create_with_domain(sprintf("inc_%d", i), domain.create(), (: increment_counter :)));
cos.push_back(coroutine.create_with_domain(sprintf("dec_%d", i), domain.create(), (: decrement_counter :)));
}

for(coroutine co:cos)
{
co.wait();
}
printf(HIG"Final counter result is %d\n"NOR, counter.lock_fetch_value());
}

test_counter();

示例3-1:基本写锁示例

示例中创建了一个简单的计数器的 share_value,并分别创建了100个可以并行的自加及自减协程。由于共享变量 share_value 对计数器的保护,最终的加减结果一定为0,可以自行运行代码尝试输出。

3.2 读锁优化

​ 对于读多写少的场景,可以使用读锁提升并发性能,实例如下:

#pragma parallel
readonly handle config := share_value.allocate("config", {
"db_host": "localhost",
"db_port": 5432,
"cache_size": 1000
});

// 配置无法并行读取
void get_db_config_by_write_lock(string key, int idx)
{
try_lock(config)
{
for(int i = 0 upto 100000)
{
config.fetch_value([key]); //在锁保护中,使用`fetch_value`深拷贝取数据
}
}
}

// 读锁优化,多个协程可以并行读取配置
void get_db_config_by_read_lock(string key, int idx)
{
try_lock(read : config)
{
for(int i = 0 upto 100000)
{
config.fetch_value([key]); //在读锁保护中,使用`fetch_value`深拷贝取数据
}
}
}

// 测试读写锁性能
void test_lock()
{
// 创建多个使用写锁的读协程(无法并行读取)
int start_time = time.time_ms();
array cos = [];
for (int i = 0; i < 16; i++)
{
cos.push_back(coroutine.create_with_domain(sprintf("reader_%d", i), domain.create(), (:get_db_config_by_write_lock, "db_host", i:)));
}

for(coroutine co:cos)
{
co.wait();
}
int end_time = time.time_ms();
printlnf(HIY"Coroutines read by write lock cost: %d ms"NOR, end_time - start_time);

start_time = time.time_ms();
cos = [];
for (int i = 0; i < 16; i++)
{
cos.push_back(coroutine.create_with_domain(sprintf("reader_%d", i), domain.create(), (:get_db_config_by_read_lock, "db_host", i:)));
}

for(coroutine co:cos)
{
co.wait();
}
end_time = time.time_ms();
printf(HIG"Coroutines read by read lock cost: %d ms"NOR, end_time - start_time);
}

test_lock();

示例3-2:读锁优化示例

示例的输出结果如下:

Coroutines read by write lock cost: 213 ms
Coroutines read by read lock cost: 29 ms

对比测试的输出结果,直观地验证了 share_value读锁的优化价值。使用**写锁(独占锁)时,所有读操作被迫串行执行,总耗时较长;而使用读锁(共享锁)**时,读操作能够并行执行,总耗时大幅降低。这一对比鲜明地体现了二者特性:写锁通过独占访问确保数据写入的原子性与一致性,而读锁则通过共享访问实现了读操作的高并发,从而在读多写少的应用场景中,能带来数量级的性能提升。

3.3 多个共享变量加锁

为多个共享变量加锁时,不需要很麻烦的写多个try_lock嵌套加锁,可以采用如下语法形式在一个try_lock语句中为多个共享变量加锁。

  • try_lock([sv1, sv2, sv3, ...]):在try_lock中将多个待加锁的共享变量放在数组中
  • try_lock(s1, s2, s3):在try_lock中以逗号区分多个待加锁的共享变量

示例如下:

// 一次try_lock多个变量的示例

#pragma parallel
readonly share_value s1 := share_value.allocate("s1", {});
readonly share_value s2 := share_value.allocate("s2", {});
readonly share_value s3 := share_value.allocate("s3", {});

void foo1(mixed v1, mixed v2, mixed v3)
{
// 同时锁3个share value的第一种方式
try_lock([s1, s2, s3])
{
s1.put_value(v1);
s2.put_value(v2);
s3.put_value(v3);
}

// 同时锁3个share value的第二种方式
try_lock(s1, s2, s3)
{
s1.put_value(v1);
s2.put_value(v2);
s3.put_value(v3);
}
}

foo1(1, 2, 3);

示例3-3:为多个共享变量加锁示例

4. 关键操作

对于关键操作可能混淆的一个参数这里给出说明。由于共享变量放入和取出RW的引用类型变量都会深拷贝,为了避免不必要的深拷贝问题,多数共享变量参数中都会带有一个代表路径的数组参数path。而部分操作还有额外的key参数,两者容易混淆。以get操作为例子:

share_value sv = share_value.allocate("",{1:{2:{3:"Treasure"}}});

try_lock(sv)
{
// share_value_instance.get(mixed key_or_index, array? path = nil);
// key 与 path 同时存在,先应用 path:[1,2] 路径查找表,之后再应用 key:3参数查找表下对应内容
string data = sv.get(3, [1,2]);
printlnf("Find data with path: '1' -> '2', key '3', result is '%s'\n", data);
}

示例4-1:path 参数示例

path参数是不为nil的数组时,函数操作先按照path路径查找内容然后在找到的内容中通过key_or_index查找最终结果。遵循:

  • 先查找path路径
  • path路径查找结果中在查找key_or_index参数内容

4.1 写入

写入操作用于修改共享变量内部的数据。

方法说明适用数据类型
void put_value(mixed val, array? path = nil)设置完整值所有类型
mixed set(mixed pos_or_key, mixed value, array? path = nil)设置指定位置或键的元素值array, map, buffer
mixed push_back(mixed value, array? path = nil)在尾部添加一个元素array, buffer
mixed insert(int pos, mixed value, array? path = nil)在指定位置插入一个元素array, buffer

写入操作的简单代码示例如下:

#pragma parallel
void demo_write_operations()
{
share_value config_sv = share_value.allocate("config", {});
share_value data_sv = share_value.allocate("data", []);
try_lock(data_sv, config_sv)
{
// 1. put_value: 全量设置
data_sv.put_value(["apple", "banana"]);
writeln("After put_value: ", data_sv.fetch_value()); // ["apple", "banana"]

// 2. push_back: 尾部追加
data_sv.push_back("cherry");
writeln("After push_back: ", data_sv.fetch_value()); // ["apple", "banana", "cherry"]

// 3. insert: 指定位置插入
data_sv.insert(1, "orange");
writeln("After insert: ", data_sv.fetch_value()); // ["apple", "orange", "banana", "cherry"]

// 4. set: 修改指定位置元素
data_sv.set(0, "avocado");
writeln("After set: ", data_sv.fetch_value()); // ["avocado", "orange", "banana", "cherry"]

// 5. 对映射(Map)类型使用set(key, value)
config_sv.set("host", "localhost"); // {"host":"localhost"}
config_sv.set("port", 8080); // {"host":"localhost", "port":8080}
config_sv.set("sub_map", {"from":nil}); // {"host":"localhost", "port":8080, "sub_map":{"from":nil}}

//6. 为 "sub_map"->"from" 路径下设置一个 {"owner":"jszx"} 表
config_sv.set("owner", "jszx", ["sub_map", "from"]); // {"host":"localhost", "port":8080, "sub_map":{"from":{"owner":"jszx"}}}
//7. 为 "sub_map"->"from" 路径下的表添加{"server":"cicd"} 键值对
config_sv.set("server", "cicd", ["sub_map", "from"]); // {"host":"localhost", "port":8080, "sub_map":{"from":{"owner":"jszx", "server":"cicd"}}}
writeln("Config map: ", config_sv.fetch_value()); // {"host":"localhost", "port":8080, "sub_map":{"from":{"owner":"jszx", "server":"cicd"}}}
}
}
demo_write_operations();

示例4-2:共享变量写入操作示例

4.2 删除

删除操作用于移除共享变量内部的数据。

方法说明适用数据类型
mixed delete_at(int pos, int n, array? path = nil)删除从指定位置开始的n个元素array, buffer
mixed delete_by_key(mixed key, array? path = nil)根据键删除映射中的元素map
bool clear(array? path = nil)清空所有元素array, map, buffer
void clean_up(array? path = nil)清除空值(如nil)array, map

删除操作的代码示例如下:

#pragma parallel
readonly handle data_sv := share_value.allocate("data", ["a", "b", "c", "d", "e", nil]);
readonly handle map_sv := share_value.allocate("map", {"k1": "v1", "k2": "v2", "k3": "v3"});

void demo_delete_operations()
{
try_lock(data_sv, map_sv)
{
writeln("Initial array: ", data_sv.fetch_value()); // ["a", "b", "c", "d", "e", nil]
writeln("Initial map: ", map_sv.fetch_value()); // {"k1":"v1", "k2":"v2", "k3":"v3"}

// 1. delete_at: 删除位置元素
data_sv.delete_at(1, 2); // 从索引1开始删除2个元素 ("b", "c")
writeln("After delete_at(1,2): ", data_sv.fetch_value()); // ["a", "d", "e", nil]

// 2. clean_up: 清理空值(如nil)
data_sv.clean_up();
writeln("After clean_up: ", data_sv.fetch_value()); // ["a", "d", "e"]

// 3. delete_by_key: 按键删除映射元素
map_sv.delete_by_key("k2");
writeln("After delete_by_key('k2'): ", map_sv.fetch_value()); // {"k1":"v1", "k3":"v3"}

// 4. clear: 清空所有数据
data_sv.clear();
map_sv.clear();
writeln("After clear - array: ", data_sv.fetch_value()); // ({})
writeln("After clear - map: ", map_sv.fetch_value()); // ([])
}
}
demo_delete_operations();

示例4-3:共享变量删除操作

4.3 查询

查询操作用于读取共享变量内部的数据,不会修改数据。

方法说明适用数据类型
mixed fetch_value(array? path = nil)获取完整值所有类型
mixed get(mixed pos_or_key, array? path = nil)获取指定位置或键的元素值array, map, buffer
mixed find(mixed val, array? path = nil)查找值首次出现的索引array, buffer
array keys(array? path = nil)获取映射的所有键map
array values(array? path = nil)获取映射的所有值map
int length(array? path = nil)获取元素个数array, map, buffer

示例如下:

#pragma parallel
readonly handle array_sv := share_value.allocate("array", ["x", "y", "z", "y"]);
readonly handle map_sv := share_value.allocate("map", {"name": "Alice", "age": 25, "city": "London"});

void demo_query_operations()
{
try_lock(read: array_sv, map_sv) // 使用读锁,允许多个查询并行
{
// 1. get: 获取特定元素
mixed elem = array_sv.get(1);
writeln("array_sv.get(1): ", elem); // "y"

// 2. find: 查找元素位置
int index = array_sv.find("y");
writeln("array_sv.find('y'): ", index); // 1

// 3. length: 获取长度
int arr_len = array_sv.length();
writeln("array_sv.length(): ", arr_len); // 4

// 4. 映射(Map)的查询操作
mixed age = map_sv.get("age");
writeln("map_sv.get('age'): ", age); // 25

array keys = map_sv.keys();
array values = map_sv.values();
int map_size = map_sv.length();

writeln("map_sv.keys(): ", keys); // ({"age", "city", "name"})
writeln("map_sv.values(): ", values); // ({25, "London", "Alice"})
writeln("map_sv.length(): ", map_size); // 3

// 5. fetch_value: 全量获取(深拷贝,谨慎使用)
map full_map = map_sv.fetch_value();
writeln("Full map: ", full_map); // (["age":25, "city":"London", "name":"Alice"])
}
}
demo_query_operations();

示例4-4:共享变量查询操作实例

5. 陷阱

5.1 性能陷阱

需要注意的是对于RW的引用类型变量,在每次放入共享变量或从共享变量取出都会深拷贝。一个典型的错误用法就是放一个大表在共享变量中,每次使用时通过fetch_value直接取出整个大表后读内容。正确的做法是通过fetch_valueget方法的数组path参数只取出目标内容,以减小深拷贝消耗。一个简单的对比示例如下:

#pragma parallel
// 假设这是一个非常大的数据
readonly handle big_config_sv := nil;

void process_user_data(int user_id)
{
// 陷阱:每次调用都使用 fetch_value 获取整个大配置!
try_lock(read : big_config_sv)
{
// fetch_value 会深拷贝整个 big_config_sv 的数据!
map entire_config = big_config_sv.fetch_value();
// 但实际上我们只需要一个用户的配置
map user_data = entire_config[sprintf("user_%d", user_id)];
// 处理这个用户的数据...
printf("Processing user: %d, theme: %s\n", user_id, user_data["preferences"]["theme"]);
}
}

void get_user_data(int user_id)
{
// 正确:每次调用都使用 get 仅获取需要的配置!
try_lock(read : big_config_sv)
{
// fetch_value 会深拷贝整个 big_config_sv 的数据!
mixed theme = big_config_sv.get("theme", [sprintf("user_%d", user_id), "preferences"]);

// 处理这个用户的数据...
printf("Processing user: %d, theme: %O\n", user_id, theme);
}
}

void demo_performance_trap()
{
array cos = [];
int start_time = time.time_ms();

// 创建 10 个协程模拟并发处理
for (int i = 1; i <= 10; i++)
{
cos.push_back(coroutine.create_with_domain(sprintf("worker_%d", i), domain.create(), (: process_user_data, i % 10000 + 1 :)));
}

for(coroutine co : cos)
{
co.wait();
}

int end_time = time.time_ms();
printf(HIR"Performance trap cost time: %d ms\n"NOR, end_time - start_time);
}

void demo_correct_test()
{
array cos = [];
int start_time = time.time_ms();

// 创建 10 个协程模拟并发处理
for (int i = 1; i <= 10; i++)
{
cos.push_back(coroutine.create_with_domain(sprintf("worker_%d", i), domain.create(), (: get_user_data, i % 10000 + 1 :)));
}

for(coroutine co : cos)
{
co.wait();
}

int end_time = time.time_ms();
printf(HIG"Correct avoid unnecessary deep dup cost time: %d ms\n"NOR, end_time - start_time);
}

// 初始化模拟的配置数据
void init_user_data()
{
map user_data = {};
for(int i = 0 upto 10000)
{
user_data["user_"+ i] = {"preferences": {"theme": "dark", "language": "cn", "history": get_system_info()}}; // get_system_info 模拟用户数据
}
big_config_sv := share_value.allocate("big_config", user_data);
}

init_user_data();
demo_correct_test();
demo_performance_trap();

示例5-1:错误的共享变量使用示例

示例代码查看输出结果可以看到,错误示例的耗时Performance trap cost time: 72 ms比正确示例的耗时Correct avoid unnecessary deep dup cost time: 2 ms多出了一个量级。所以必须要注意合理取用share_value中保存的数据!!!避免不必要的深拷贝!!!

6. 拓展阅读

​ 更多共享变量相关内容请参阅如下文档:

7. 总结

本章全面介绍了 GS 语言中共享变量(share_value)的各个方面。我们首先明确了共享变量作为线程安全数据容器的基本概念及其内置锁机制的核心特性。重点讲解了加锁访问的必要性,详细对比了**写锁(独占锁)读锁(共享锁)**的不同特性与适用场景,并通过性能对比实验证明了读锁在读多写少场景下的显著优势。

在关键操作部分,系统梳理了共享变量的写入删除查询三类操作。通过多个实用示例,展示了如何对单一或多个共享变量进行安全的加锁访问和数据处理。以及通过示例展示了共享变量深拷贝导致的性能陷阱,明确了共享变量在保护RW的引用类型变量时,应通过path参数尽量精确查找或修改对应内容以避免不必要的深拷贝操作。总而言之,共享变量是 GS 并行编程中管理共享状态的利器,正确理解并应用其锁机制和操作方法是构建高效、健壮并发程序的基础。