libuv介绍--多线程

原文: http://nikhilm.github.com/uvbook/threads.html

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

猜你喜欢

转载自flxchy4.iteye.com/blog/1775891