Libuv 线程通讯

很多时候,你希望正在运行的线程之间能够相互发送消息。例如你在运行一个持续时间长的任务(可能使用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, UV_RUN_DEFAULT);
}

因为异步的线程通信是基于event-loop的,所以尽管所有的线程都可以是发送方,但是只有在event-loop上的线程可以是接收方(或者说event-loop是接收方)。在上述的代码中,当异步监视者接收到信号的时候,libuv会激发回调函数(print_progress)。

注意
应该注意: 因为消息的发送是异步的,当uv_async_send在另外一个线程中被调用后,回调函数可能会立即被调用, 也可能在稍后的某个时刻被调用。libuv也有可能多次调用uv_async_send,但只调用了一次回调函数。唯一可以保证的是: 线程在调用uv_async_send之后回调函数可至少被调用一次。 如果你没有未调用的uv_async_send, 那么回调函数也不会被调用。 如果你调用了两次(以上)的uv_async_send, 而 libuv 暂时还没有机会运行回调函数, 则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) {
    double percentage = *((double*) handle->data);
    fprintf(stderr, "Downloaded %.2f%%\n", percentage);
}

函数print_progress是标准的libuv模式,从监视器中抽取数据。最后最重要的是把监视器回收。

void after(uv_work_t *req, int status) {
    fprintf(stderr, "Download complete\n");
    uv_close((uv_handle_t*) &async, NULL);
}

在例子的最后,我们要说下data域的滥用,bnoordhuis指出使用data域可能会存在线程安全问题,uv_async_send()事实上只是唤醒了event-loop。可以使用互斥量或者读写锁来保证执行顺序的正确性。

互斥量和读写锁不能在信号处理函数中正确工作,但是uv_async_send可以。

一种需要使用uv_async_send的场景是,当调用需要线程交互的库时。例如,举一个在node.js中V8引擎的例子,上下文和对象都是与v8引擎的线程绑定的,从另一个线程中直接向v8请求数据会导致返回不确定的结果。但是,考虑到现在很多nodejs的模块都是和第三方库绑定的,可以像下面一样,解决这个问题:
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.在另外一个线程中完成任务想要调用progress的回调函数,但是不能直接与v8通信,所以需要uv_async_send函数。 4.在主线程(v8线程)中调用的异步回调函数,会在v8的配合下执行javscript的回调函数。(也就是说,主线程会调用回调函数,并且提供v8解析javascript的功能,以便其完成任务)。