libuv的线程功能的值得注意的方面是它是一个libuv内部自包含的部分。然而其它的特性密切依赖事件循环和回调原则,线程是完全不知的,他们按需求阻塞,信号错误直接通过返回值和,如第一个例子所示,甚至不需要一个运行的事件循环。
libuv的线程API也非常有限,因为在所有平台上线程的语义和语法都是不同的,和不同级别的完整性。
这一章作如下假设:只有一个事件循环,运行在一个线程(主线程)。没有其他线程与事件循环互动(除了使用uv_async_send)。多事件循环包括运行事件循环在不同的线程和管理它们。
核心线程操作
这里没有太多,你只是用 uv_thread_create()启动一个线程,使用uv_thread_join()等待它关闭。
thread-create/main.c
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; }
小贴士:
在Unix上uv_thread_t只是pthread_t的一个别名,但这是一个实现细节,避免依赖于它总是成立的。
第二个参数是函数作为线程的入口点,最后一个参数是一个void *参数可通过自定义参数到线程。函数hare现在将运行在一个单独的线程,由操作系统优先安排:
thread-create/main.c
void hare(void *arg) { int tracklen = *((int *) arg); while (tracklen) { tracklen--; sleep(1); fprintf(stderr, "Hare ran another step\n"); } fprintf(stderr, "Hare done running!\n"); }
不像pthread_join()那样允许目标线程通过使用第二个参数返回一个值给调用线程,uv_thread_join() 不允许。返回值需要使用 Inter-thread communication。
同步原语
Mutexes互斥锁
互斥函数直接映射到pthread的等价物。
libuv 互斥函数
*处理编写到控制台。SIGWINCH可能并不总是及时交付;libuv只会在当光标被移动时检测尺寸变化。当一个可读的uv_tty_handle用在原始模式,调整控制台缓冲也将触发一个SIGWINCH信号
uv_mutex_init()和uv_mutex_trylock()函数成功时将返回0,错误时返回-1而不是错误代码。
如果libuv已经启用调试编译,uv_mutex_destroy(), uv_mutex_lock() 和 uv_mutex_unlock() 将被错误中止。同样如果错误是非EAGAIN的其它错误,uv_mutex_trylock()将中止。
一些平台支持递归互斥,但你不应该依靠他们。BSD互斥锁的实现,如果已锁定一个互斥量的再次的尝试锁定,将抛出一个错误。例如:
uv_mutex_lock(a_mutex); uv_thread_create(thread_id, entry, (void *)a_mutex); uv_mutex_lock(a_mutex); // more things here
可以用来等待直到另一个线程初始化一些东西然后解锁互斥,但如果在调试模式下会导致程序崩溃,或第二个调用uv_mutex_lock()返回一个错误。
注意:
互斥锁在linux支持的属性为一个递归的互斥锁,但API不是通过libuv暴露。
锁定
读写锁是一个更细粒度的访问机制。两个读操作可以同时访问共享内存。一个写操作可能不获取锁,当它是由一个读操作持有。一位读或写可能不会获得一个锁当一个写操作持有它。读写锁在数据库经常用到。这是一个简单例子。
locks/main.c - simple rwlocks
#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; }
运行这个和观察读操作有时会重叠。对于多个写操作,调度程序通常会给他们更高的优先级,因此,如果您添加两个写操作,你就会看到,两个写操作倾向于先完成然后读操作有机会再次运行。
其它
libuv还支持信号量,条件变量和障碍的api非常类似于他们的pthread对应的内容。
对于条件变量,libuv也有一个等候超时,平台特定的怪癖[1]。
此外,libuv提供了一个方便的函数 uv_once()(不要uv_run_once()。多个线程可以尝试以一个给定的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。
libuv 工作队列
uv_queue_work()是一个很方便的功能,它允许一个应用程序在一个单独的线程运行一个任务,并有一个回调函数,当任务完成了触发。一个看似简单的功能,使uv_queue_work()诱人的是它允许潜在的任何第三方库用于与事件循环模式。当你使用事件循环,必须确保当执行I / O或是严重的CPU操作时在循环线程块没有定期运行的功能,因为这意味着循环减慢和在满负荷下事件没有得到处理。
但很多现有的代码以阻塞函数为特色(例如一个在hood下执行I/O的程序)来用于线程如果你想响应(典型的“一个客户端一个线程的”服务器模型),以及让他们运行于一个事件循环库,这些库通常包含滚动自己的运行的任务在一个单独的线程的系统。libuv只是为此提供了一个方便的抽象。
这是一个简单的例子受node.js激发的cancer。我们要计算斐波纳契数列,但运行它在一个单独的线程,以便阻止和CPU绑定的任务并不阻止事件循环执行其他活动。
queue-work/main.c - lazy fibonacci
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) { fprintf(stderr, "Done calculating %dth fibonacci\n", *(int *) req->data); }
实际的任务函数很简单,没有什么表明它将运行在一个单独的线程。uv_work_t 的结构是线索。你可以通过使用void *数据字段传递任意数据和用它来和线程交流。但是如果你正在改变的内容通透式被两个可能运行线程使用到,要确保使用适当的锁。
触发者是uv_queue_work:
queue-work/main.c
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_work_t,一旦函数返回时,后续函数会被调用,同样以相同的结构。
编对于写包装器来封装库,一个常见的模式是使用一个baton来交换数据。
线程间通讯
有时你想要很多的线程来运行时实际地相互发送消息。例如你可能会在一个单独的线程运行一些长时间的任务(也许使用uv_queue_work),但想通知进展给主线程。这是一个简单的例子,有一个下载管理器通知用户下载的状态。
progress/main.c
uv_loop_t *loop; uv_async_t async; 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); }
异步线程通信工作于循环,所以尽管任何线程可以做消息发送者,但只有libuv循环的线程与可以做接收器(或者说循环是接收器)。libuv将调用回调(print_progress)于异步观察者,每当它接收一条消息。
警告:
重要的是要意识到,消息发送是异步的,回调可能在uv_async_send在另一个线程被调用后立即被调用,或过一会调用。libuv也可以结合多个调用到uv_async_send,并调用回调只有一次。唯一的保证libuv做的是——回调函数在调用uv_async_send后至少调用一次。如果你没有将要调用uv_async_send,回调将不会被调用。如果你有两个或两个以上的调用,和libuv尚未有机会运行回调,但是它也许可以调用你的回调函数只有一次,对多个uv_async_send的调用。你的回调将从来不会为一个时间被调用两次。
progress/main.c
void fake_download(uv_work_t *req) { int size = *((int*) req->data); int downloaded = 0; double percentage; while (downloaded < size) { percentage = downloaded*100.0/size; async.data = (void*) &percentage; uv_async_send(&async); sleep(1); downloaded += (200+random())%1000; // can only download max 1000bytes/sec, // but at least a 200; } }
在下载功能我们修改进度指示器和将消息放入队列,以便使用uv_async_send发送。记住:uv_async_send也非阻塞并将立即返回。
progress/main.c
void print_progress(uv_async_t *handle, int status /*UNUSED*/) { double percentage = *((double*) handle->data); fprintf(stderr, "Downloaded %.2f%%\n", percentage); }
回调函数是一个标准的libuv模式,从观察者提取数据。
最后重要的是要记住清理观察者。
progress/main.c
void after(uv_work_t *req) { fprintf(stderr, "Download complete\n"); uv_close((uv_handle_t*) &async, NULL); }
在这个例子中,显示了滥用数据字段,bnoordhuis指出,使用数据字段不是线程安全的,而且 uv_async_send()实际上是只为了唤醒事件循环。使用互斥锁或rwlock确保访问以正确的顺序执行。
警告:
mutexes和rwlocks不工作在一个signal handler内部,而uv_async_send可以。
一个需要用到uv_async_send 的用例是当和库交互,为他们的功能要求线程关联。例如在 node.js,一个v8引擎实例中,上下文及其对象绑定到v8的实例起始的线程中。从另外一个线程和v8的数据结构互动会导致未定义的结果。现在考虑一些node.js模块结合了第三方库。它可能会是这样的:
1,在node中,第三方库设置有一个JavaScript回调函数来调用更多信息:
var lib = require('lib'); lib.on_progress(function() { console.log("Progress"); }); lib.do(); // do other stuff
2,lib.do应该是非阻塞而第三方部分是阻塞的,所以绑定使用uv_queue_work。
3,在一个单独的线程中已完成的工作想要调用进度回调,但不能直接调用v8 JavaScript交互。所以它使用uses uv_async_send。
4,异步回调,在v8线程的主循环线程中调用,然后与v8引擎交互来调用JavaScript回调。
[1] https://github.com/joyent/libuv/blob/master/include/uv.h#L1853