标签归档:Zig

[Zig]使用 std.Thread.Pool 线程池

我发现Zig的官方文档里似乎没有写这个`std.Thread.Pool`的具体用例,所以写一篇笔记给大家参考一下。

这个Zig标准库中的线程池主要用到两个部分,一个是线程池本身的结构体`std.Thread.Pool`,一个是用于等待任务结束的`std.Thread.WaitGroup`(等待组)结构体。等待组的作用是把一组相关的任务放在一起,之后可以使用特定方法等待它们全部完成。

流程:

  • 在线程池初始化好了之后在创建线程任务时给pool.spawnWg方法传入指定的WaitGroup和要执行的函数以及参数
  • 分配任务时线程池就会开始执行任务
  • →随时使用`pool.waitAndWork`方法阻塞当前线程,等待WaitGroup中所有任务完成
  • ↘或者在任何时候使用`wait_group.isDone()`方法检查任务是否全部完成,如果没有完成可以让主线程去做其他事情。

以下直接写一个示例,内容依然是在我前面的笔记中已经出现了好几次的多线程数据竞争示例。

const std = @import("std");
const debug = std.debug;
const Pool = std.Thread.Pool;
const Thread = std.Thread;
const WaitGroup = std.Thread.WaitGroup;

const print = debug.print;

const NUM_THREADS = 2;

var sum: u64 = 0;//让几个线程分别把这个sum自增,完成后sum应当由于非加锁自增导致其值为一个随机数
pub fn sumTask(idx: usize) void {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Thread {} (idx:{}) : starting...\n", .{ tid, idx });
    var timer = std.time.Timer.start() catch unreachable; //获取一个计时器,用于之后计算线程执行时间
    const y = ∑
    for (0..50000000) |_| {
        y.* += 1;
    }
    print("Thread {} (idx:{}) : done in {}ms.\n", .{ tid, idx, timer.read() / 1000000 });
}
test "threadpool" {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Main thread {} start\n", .{tid});
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var thread_pool: Pool = undefined;
    try thread_pool.init(.{ //初始化线程池
        .allocator = allocator,
        .n_jobs = NUM_THREADS, //线程数
    });
    defer thread_pool.deinit();
    var wait_group: WaitGroup = undefined;
    wait_group.reset(); //初始化等待组
    //分配10个任务,线程池内的空闲线程会分别完成这些任务
    for (0..10) |idx| {
        thread_pool.spawnWg(
            &wait_group,//传入等待组
            sumTask,//传入任务函数
            .{idx},//任务函数的参数元组
        );
    }
    thread_pool.waitAndWork(&wait_group); //等待以上分配的任务全部完成
    print("Main: sum = {}\n", .{sum});
    print("Main: program completed. Exiting.\n", .{});
}

需要注意的是,如果在分配完任务后立刻使用`thread_pool.waitAndWork`等待任务完成(像上面的示例中一样,也就是在任务没有全部完成时就开始等待),你会发现虽然设置的线程数是2,理论上任务应该两个两个一起执行,但实际上却是3个任务一起执行的,因为调用这个等待方法的线程也执行了任务。我当时也疑惑了一会儿,然后想明白了,既然等待线程已经等在那里了,那么与其干等不如一起把任务做完,这样的逻辑确实是合理的,但这样实际并行执行的线程就会多一个,这是要注意的地方。

另一个要注意的是,目前的线程池实现中每一个线程启动之后就会开始从任务队列的开头取任务,但添加任务的时候也是添加到队列的开头的,所以虽然不严格保证顺序(因为你后续还可以继续添加任务),但任务队列大致上是后进先出的,所以不要连续不断地添加任务,至少要等线程池任务清空了才可以继续添加新的任务。(我觉得这样不太合理)

[WebAssembly]使用Zig语言制作wasm模块

“前几篇文章你不是还在写C++的wasm笔记,为什么突然开始使用Zig了?”

实际上由于C++古老的语言特性导致有的时候必须把声明和定义分开来写非常繁琐,而且在Emscripten环境下要添加第三方的库比较麻烦,所以我想改到一个自带包管理的语言。于是就首先捡起以前学到一半被劝退的Rust,这个国庆我花了几天时间把rust的教程看完了,但我又一次被这个反人类语言极致的复杂性劝退了,也就有了上一篇文章的内容。

在群友的推荐下我发现Zig这个类C的语言好像还行,于是抱着试一试的心态来学习一下,也就有了这篇笔记。不过不代表我之后就会用这个语言写项目,还有待观察。

这篇笔记的主要内容不是怎么编写Zig语言,而是如何把一个Zig项目编译成wasm模块,因此需要先有一点点的zig基础才能看懂。现在zig的文档和社区生态还不是很完善,且有的东西还在变化,有的东西是查不到或者很难查到的,我花了一些时间摸索才搞明白一些要点,这就是写这篇笔记的原因。

环境准备

如果还没有zig环境,可以用scoop快速部署,只需执行两个脚本即可

安装scoop

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

用scoop安装最新版的zig,我写这篇笔记的时候zig版本是0.14.0-dev.1798,部分特性有可能在以后改变。

scoop bucket add versions
scoop install versions/zig-dev

以上即可完成zig环境的安装。顺带一提,使用scoop update zig-dev可以更新zig-dev的版本。

简单的示例

最简单快速的方式就是创建一个zig文件,然后使用命令把它编译成wasm文件。

main.zig 文件(由于代码高亮插件没有支持这个语言,而且以下示例看着挺像rust的,所以先用rust高亮来凑活一下)

const std = @import("std");

pub fn main() !void {
    const poi: u16 = 666;

    std.debug.print("poi: {}\n", .{poi});
}

然后使用命令编译

zig build-exe main.zig -target wasm32-wasi

此时目录下就会多出一个main.wasm和一个main.wasm.o文件(以后不同版本可能有所区别),和Emscripten不同的是它不会帮你生成一个胶水js文件供你直接加载。

这里我们使用的是wasi接口的wasm,如果你的运行环境是wasmtime的话,那么就可以直接执行这个wasm文件了。

生成的结果使用nodejs也可以运行,但要在nodejs中运行它,我们得自己为加载wasm写一些代码,还好也不多:

start.js 文件

const { readFile } = require('node:fs/promises');
const { WASI } = require('node:wasi');
const { argv, env } = require('node:process');
const { join } = require('node:path');

const wasi = new WASI({
	version: 'preview1',//该项必须定义,见文档:
	// https://nodejs.org/docs/latest-v20.x/api/wasi.html#new-wasioptions
	args: argv,//该项不是必须的,用于把node运行参数传给wasm程序
	env,//该项不是必须的,用传递环境变量
	
	//其它参数也见上面的文档链接
	//注意这是一个较新的特性,不同nodejs版本的接口可能会有区别,注意选择对应的文档版本
});

(async () => {
	const wasm = await WebAssembly.compile(
		await readFile(join(__dirname, 'main.wasm')),
	);
	const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
	wasi.start(instance);
})();

使用nodejs执行以上js文件将得到输出结果:poi: 666

继续阅读[WebAssembly]使用Zig语言制作wasm模块