ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

libuv 线程编程实战指南:从线程创建、同步原语到工作队列与跨线程通信

libuv 线程编程实战指南:从线程创建、同步原语到工作队列与跨线程通信 人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载导读本文基于 TEN-framework 仓库内置的 libuv 文档 threads.rst系统讲解 libuv 线程 API 的完整用法。读者将掌握如何用uv_thread_create/uv_thread_join创建并回收线程如何用互斥锁、读写锁、屏障等同步原语保护共享数据如何借助uv_queue_work把阻塞任务搬离事件循环线程以及如何用uv_async_send实现线程向事件循环的异步消息投递。这些能力是编写不阻塞事件循环、可并发、可取消的高质量异步应用的基石。为什么事件循环之外还需要线程事件循环常被视为web-scale 编程的终极方案但线程依然是处理器执行工作的基本介质libuv 内部也依赖线程来模拟系统调用的异步特性。libuv 允许应用把真正阻塞的任务放到独立线程中执行从而继续享受事件循环范式的好处。libuv 的线程 API 与 POSIXpthreads(7)在语法与语义上高度相似但在设计上有一个显著特点线程设施是 libuv 中完全自包含的部分。它不依赖事件循环和回调原则按需阻塞、通过返回值直接上报错误甚至如第一个示例所示在没有任何运行中的事件循环时也能工作。同时由于各平台线程语义与完整度不同libuv 的线程 API 刻意保持精简。本指南基于一个重要假设整个程序中只有一个事件循环运行在一个线程主线程上其他线程除调用uv_async_send外不与事件循环交互。这一点在多线程 事件循环混合编程中是必须遵守的纪律。核心线程操作创建与回收线程操作本身很简单用uv_thread_create()启动线程用uv_thread_join()等待其结束。以下示例取自 thread-create/main.c模拟龟兔赛跑两只动物各自在独立线程中按不同步频前进#include stdio.h #include uv.h void hare(void *arg) { int tracklen *((int *) arg); while (tracklen) { tracklen--; uv_sleep(1000); fprintf(stderr, Hare ran another step\n); } fprintf(stderr, Hare done running!\n); } void tortoise(void *arg) { int tracklen *((int *) arg); while (tracklen) { tracklen--; fprintf(stderr, Tortoise ran another step\n); uv_sleep(3000); } fprintf(stderr, Tortoise done running!\n); } int main() { int tracklen 10; uv_thread_t hare_id; uv_thread_t tortoise_id; uv_thread_create(hare_id, hare, tracklen); uv_thread_create(tortoise_id, tortoise, tracklen); uv_thread_join(hare_id); uv_thread_join(tortoise_id); return 0; }要点说明uv_thread_t在 Unix 上只是pthread_t的别名见 uv.h 中线程相关声明但这是实现细节不要依赖这一假设恒成立。uv_thread_create()的第一个参数是线程句柄第二个参数是线程入口函数签名void (*)(void *)第三个参数是void *自定义参数可向线程传递任意数据。上面代码中hare与tortoise将各自在独立线程中被操作系统抢占式调度。与pthread_join()不同uv_thread_join()不能通过第二个参数从目标线程回收返回值。若需在线程间传递数值应使用互斥锁、读写锁或uv_async_send等通信机制。该示例完全没有创建事件循环印证了线程部分是自包含的这一论断。从源码结构看uv.h 还提供了带 flags 的uv_thread_create_ex()用于对线程创建行为做更细粒度的控制常规场景下uv_thread_create()已足够。同步原语本节刻意保持精简——libuv 的同步原语与 pthread 对应物几乎一一映射多数细节可查阅pthreads(7)手册。以下仅罗列 libuv API 中值得注意的差异与陷阱。互斥锁Mutex互斥锁函数是 pthread 等价物的直接映射int uv_mutex_init(uv_mutex_t* handle); int uv_mutex_init_recursive(uv_mutex_t* handle); void uv_mutex_destroy(uv_mutex_t* handle); void uv_mutex_lock(uv_mutex_t* handle); int uv_mutex_trylock(uv_mutex_t* handle); void uv_mutex_unlock(uv_mutex_t* handle);uv_mutex_init()、uv_mutex_init_recursive()、uv_mutex_trylock()成功返回 0失败返回错误码。若 libuv 以调试模式编译uv_mutex_destroy()、uv_mutex_lock()、uv_mutex_unlock()出错时会直接abort()uv_mutex_trylock()则在错误码不是EAGAIN或EBUSY时 abort。libuv 支持递归互斥锁uv_mutex_init_recursive但不应依赖递归锁且递归锁不能与uv_cond_t条件变量配合使用。一个典型的陷阱默认BSD 系互斥锁实现会在已持有锁的线程再次 lock 同一把锁时报错。例如下面的构造可用来等待其他线程完成初始化后释放a_mutex但第二次uv_mutex_lock(a_mutex)在调试模式下会直接导致程序崩溃或返回错误uv_mutex_init(a_mutex); uv_mutex_lock(a_mutex); uv_thread_create(thread_id, entry, (void *)a_mutex); uv_mutex_lock(a_mutex); // more things here注意Windows 上的互斥锁总是递归的见 uv.h 中uv_mutex_init_recursive的声明与实现约定这是跨平台行为差异的一个典型例子。读写锁Read-Write Lock读写锁提供更细粒度的访问控制多个读者可以同时访问共享内存写者不能在读者持锁时获取锁读者或写者都不能在写者持锁时获取锁。读写锁在数据库类场景中非常常见。以下是 locks/main.c 中的玩具示例#include stdio.h #include uv.h uv_barrier_t blocker; uv_rwlock_t numlock; int shared_num; void reader(void *n) { int num *(int *)n; int i; for (i 0; i 20; i) { uv_rwlock_rdlock(numlock); printf(Reader %d: acquired lock\n, num); printf(Reader %d: shared num %d\n, num, shared_num); uv_rwlock_rdunlock(numlock); printf(Reader %d: released lock\n, num); } uv_barrier_wait(blocker); } void writer(void *n) { int num *(int *)n; int i; for (i 0; i 20; i) { uv_rwlock_wrlock(numlock); printf(Writer %d: acquired lock\n, num); shared_num; printf(Writer %d: incremented shared num %d\n, num, shared_num); uv_rwlock_wrunlock(numlock); printf(Writer %d: released lock\n, num); } uv_barrier_wait(blocker); } int main() { uv_barrier_init(blocker, 4); shared_num 0; uv_rwlock_init(numlock); uv_thread_t threads[3]; int thread_nums[] {1, 2, 1}; uv_thread_create(threads[0], reader, thread_nums[0]); uv_thread_create(threads[1], reader, thread_nums[1]); uv_thread_create(threads[2], writer, thread_nums[2]); uv_barrier_wait(blocker); uv_barrier_destroy(blocker); uv_rwlock_destroy(numlock); return 0; }运行后可观察到读者之间有时会重叠进入临界区当存在多个写者时调度器通常会给予写者更高优先级——若在示例中增加两个写者会看到两个写者往往先于读者完成全部写操作读者才有机会再次进入。示例中还使用了屏障barrieruv_barrier_init(blocker, 4)表示屏障需要 4 个参与者2 个读者 1 个写者 主线程主线程与三个工作线程各自在完成任务后uv_barrier_wait(blocker)从而保证主线程能等待所有读者和写者都宣告结束后再销毁屏障。其他同步设施信号量、条件变量、屏障与一次初始化libuv 还以与 pthread 高度相似的 API 支持信号量semaphore、条件变量condition variables和屏障barrier。此外libuv 提供了便捷函数uv_once()声明位于 uv.h。多个线程可以同时携带同一个 guard 和函数指针调用uv_once()但只有第一个调用者会胜出函数保证只被调用一次/* Initialize guard */ static uv_once_t once_only UV_ONCE_INIT; int i 0; void increment() { i; } void thread1() { /* ... work */ uv_once(once_only, increment); } void thread2() { /* ... work */ uv_once(once_only, increment); } int main() { /* ... spawn threads */ }所有线程结束后i 1。uv_once()非常适合延迟初始化共享资源如全局配置、单例对象且天然线程安全无需额外加锁。自 libuv v0.11.11 起还提供了uv_key_t结构及其配套 API用于线程局部存储Thread-Local StorageTLS让每个线程拥有独立的变量副本。libuv 工作队列把阻塞任务搬出事件循环uv_queue_work()是一个便捷函数在独立线程中运行任务任务完成时触发回调。它的价值在于让任意第三方阻塞库也能融入事件循环范式。使用事件循环时有一条铁律任何在循环线程中周期性运行的函数都不能在执行 I/O 时阻塞也不能是严重的 CPU 密集任务否则循环会变慢事件无法以满负荷处理。而大量既有代码天然包含阻塞函数例如底层执行 I/O 的例程传统做法是一客户端一线程。libuv 把在独立线程中跑任务这一常见模式抽象成了uv_queue_work省去自行造轮子。一个惰性斐波那契示例以下示例源自 node.js 生态中的讨论见 queue-work/main.c计算斐波那契数列途中随机休眠 13 秒但整体在独立线程中运行避免阻塞事件循环#include stdio.h #include stdlib.h #include unistd.h #include uv.h #define FIB_UNTIL 25 uv_loop_t *loop; long fib_(long t) { if (t 0 || t 1) return 1; else return fib_(t-1) fib_(t-2); } void fib(uv_work_t *req) { int n *(int *) req-data; if (random() % 2) sleep(1); else sleep(3); long fib fib_(n); fprintf(stderr, %dth fibonacci is %lu\n, n, fib); } void after_fib(uv_work_t *req, int status) { fprintf(stderr, Done calculating %dth fibonacci\n, *(int *) req-data); } int main() { loop uv_default_loop(); int data[FIB_UNTIL]; uv_work_t req[FIB_UNTIL]; int i; for (i 0; i FIB_UNTIL; i) { data[i] i; req[i].data (void *) data[i]; uv_queue_work(loop, req[i], fib, after_fib); } return uv_run(loop, UV_RUN_DEFAULT); }关键点任务函数本身看不出任何将在独立线程中运行的痕迹uv_work_t结构体才是线索。可通过其void *data字段传递任意数据实现与工作线程的双向通信但若两个线程可能同时修改数据必须使用合适的锁。uv_queue_work(loop, req, fib, after_fib)的调用原型见 uv.h工作线程函数fib在独立线程中启动并收到uv_work_t一旦fib返回after_fib就会在事件循环所在线程上被调用并收到同一个uv_work_t结构。为阻塞库编写包装器时常见模式是使用baton接力棒结构来交换数据把入参、输出缓冲、状态等打包进一个结构体通过req-data传入传出。用 uv_cancel 取消待执行任务自 libuv 0.9.4 起提供uv_cancel()可取消工作队列中的任务。只有尚未开始执行的任务可以被取消任务已开始执行或已执行完毕时uv_cancel()会失败。uv_cancel()适合在用户请求终止时清理积压任务。例如音乐播放器可能排队扫描多个目录的音频文件若用户终止程序应当快速退出而不是等待所有积压请求跑完。修改斐波那契示例见 queue-cancel/main.c先注册一个 SIGINT 信号处理器用户在终端按下CtrlC时对所有工作项批量调用uv_cancel()void signal_handler(uv_signal_t *req, int signum) { printf(Signal received!\n); int i; for (i 0; i FIB_UNTIL; i) { uv_cancel((uv_req_t*) fib_reqs[i]); } uv_signal_stop(req); } int main() { loop uv_default_loop(); int data[FIB_UNTIL]; int i; for (i 0; i FIB_UNTIL; i) { data[i] i; fib_reqs[i].data (void *) data[i]; uv_queue_work(loop, fib_reqs[i], fib, after_fib); } uv_signal_t sig; uv_signal_init(loop, sig); uv_signal_start(sig, signal_handler, SIGINT); return uv_run(loop, UV_RUN_DEFAULT); }行为细节uv_cancel()对正在执行或已经完成的任务返回 0即取消失败但不算错误只有真正被成功取消的任务其after回调才会以status UV_ECANCELED被调用void after_fib(uv_work_t *req, int status) { if (status UV_ECANCELED) fprintf(stderr, Calculation of %d cancelled.\n, *(int *) req-data); }uv_cancel()同样适用于uv_fs_t和uv_getaddrinfo_t请求对文件系统一族函数取消后uv_fs_t.errorno会被置为UV_ECANCELED。一个设计良好的程序还应当有办法终止已经开始执行的长任务例如让工作线程周期性检查一个仅由主进程设置的终止标志变量配合互斥锁或原子操作读取。跨线程通信uv_async_send有时需要让正在运行的多个线程互相发送消息。比如用uv_queue_work跑一个长时间任务同时想向主线程汇报进度——一个典型的下载管理器向用户汇报下载状态的应用。完整示例见 progress/main.c。核心结构如下uv_loop_t *loop; uv_async_t async; double percentage; void fake_download(uv_work_t *req) { int size *((int*) req-data); int downloaded 0; while (downloaded size) { percentage downloaded*100.0/size; async.data (void*) percentage; uv_async_send(async); sleep(1); downloaded (200random())%1000; // can only download max 1000bytes/sec, // but at least a 200; } } void after(uv_work_t *req, int status) { fprintf(stderr, Download complete\n); uv_close((uv_handle_t*) async, NULL); } void print_progress(uv_async_t *handle) { double percentage *((double*) handle-data); fprintf(stderr, Downloaded %.2f%%\n, percentage); } int main() { loop uv_default_loop(); uv_work_t req; int size 10240; req.data (void*) size; uv_async_init(loop, async, print_progress); uv_queue_work(loop, req, fake_download, after); return uv_run(loop, UV_RUN_DEFAULT); }要点与语义保证异步通信基于事件循环任何线程都可以作为消息发送方但只有持有 libuv 循环的线程能成为接收方更准确地说是循环本身作为接收者。每当收到消息libuv 就会在循环线程上调用回调print_progress。uv_async_send的语义保证必须精确理解文档以 warning 强调由于发送是异步的回调可能在另一线程调用uv_async_send后立即被触发也可能延迟一段时间libuv 可能把多次uv_async_send合并为一次回调。唯一硬性保证是每次调用uv_async_send之后回调至少会被调用一次。若没有待处理的uv_async_send调用回调不会被调用若连续调用两次或更多且 libuv 尚未执行回调则多次调用可能只触发一次回调回调绝不会因单次事件而被调用两次。uv_async_send本身非阻塞、立即返回声明见 uv.h在下载函数中修改进度指示器后即可排队投递消息。回调采用标准 libuv 模式从 watcherhandle-data中取出数据。结束时务必清理 watcher下载完成后在after回调中调用uv_close((uv_handle_t*) async, NULL)。关于 data 字段的线程安全警告上述示例滥用了uv_async_t的data字段来传递进度数值但社区bnoordhuis早已指出使用data字段传递数据并不线程安全uv_async_send()的本意只是唤醒事件循环。正确的做法是用互斥锁或读写锁保护共享数据保证跨线程访问按正确顺序进行uv_async_send只负责通知有数据更新了。另有一个实用结论互斥锁和读写锁不能在信号处理器内使用而uv_async_send可以。因此若需要在信号处理路径中与事件循环通信uv_async_send是安全的选择。线程亲和性场景为什么 uv_async_send 不可或缺一个典型场景是与其他要求线程亲和性thread affinity的库互操作。例如在 node.js 中v8 引擎实例、context 及其对象都绑定在 v8 实例启动时所在的线程上从其他线程直接操作 v8 数据结构会导致未定义行为。当某个 node.js 模块绑定了一个第三方阻塞库时典型流程如下在 node 中为第三方库注册 JavaScript 回调var lib require(lib); lib.on_progress(function() { console.log(Progress); }); lib.do(); // do other stufflib.do本应非阻塞但第三方库是阻塞的于是绑定层使用uv_queue_work把它放进工作线程工作线程中的实际任务想触发进度回调却不能直接进入 v8 与 JavaScript 交互于是调用uv_async_send异步回调运行在主循环线程即 v8 线程上再由它安全地调用 v8 交互逻辑最终触发 JavaScript 回调。这正是在 TEN-framework 这类事件驱动框架中把uv_async_send与uv_queue_work组合使用的经典模式工作线程干重活主循环线程负责回调与状态更新两者之间只通过唤醒 加锁共享数据来通信。小结libuv 的线程设施虽精简但覆盖了多线程编程的全部核心要素uv_thread_create/uv_thread_join负责线程生命周期互斥锁、递归锁、读写锁、信号量、条件变量、屏障与uv_once覆盖同步需求uv_key_t提供线程局部存储uv_queue_work把阻塞任务与 CPU 密集任务移出事件循环线程并支持uv_cancel取消uv_async_send则提供了从任意线程向事件循环投递唤醒信号的机制。实践时牢记三条原则除uv_async_send外不要让其他线程触碰事件循环跨线程共享数据必须加锁而不要依赖data字段uv_async_send只保证至少唤醒一次回调合并是正常行为而非缺陷。掌握这些 API即可在保持事件循环响应性的同时安全地接入阻塞型第三方库并构建真实的多线程异步应用。延伸阅读libuv 线程指南原始文档线程创建示例 / 读写锁与屏障示例工作队列示例 / 工作队列取消示例跨线程进度通知示例libuv 头文件中的线程与异步 API 声明赞分享人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载相关推荐libuv 线程指南核心线程 API、同步原语与工作队列实战libuv 线程指南核心线程 API、同步原语与工作队列实战 事件循环是 libuv 处理异步 I/O 的核心但真实的计算与阻塞操作仍然发生在线程里。本篇指网络通信异步编程libuv 线程与同步原语完全指南从线程创建到互斥锁、条件变量与屏障libuv 线程与同步原语完全指南从线程创建到互斥锁、条件变量与屏障 libuv 提供了一组跨平台的线程与同步原语 API覆盖线程创建/管理、线程局部存储、网络通信异步编程Napa.js 线程同步指南napa.sync.Lock 跨线程互斥锁的原理与实战Napa.js 线程同步指南 napa.sync.Lock 跨线程互斥锁的原理与实战 Napa.js 是一个多线程 JavaScript 运行时其 sync语言运行时并发编程上一篇完全掌握Path of Building从装备制作小白到大师的终极指南下一篇5个步骤掌握Windows文件元数据管理FileMeta完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表