考虑到系统性能的问题,现在很多流程都使用了异步机制,这样可以更快的返回去做另外的事情,但是异步也存在一个很大的问题,就是不知道何时会完成之前的事情,很多流程就变得不够可控,现在在项目中就碰到过因为此问题而产生的bug,因为不可控,所以多线程之间资源的互斥和保护就变的异常重要,否则一不小心就会因为修改了本部应该修改的线程或进程资源而导致系统crush掉。
现在使用的是libuv,它是一个第三方的异步机制的开源库。接触了有一段时间了,接下来把我从这开源代码的流程和感想写一下。
首先uv库中充满了回调函数,而且很多都回调函数里面再调用回调函数,uv库中核心的结构体是uv_loop_t
// uv_loop_t
struct uv_loop_s {
/* User data - use this for whatever. */
void* data;
/* Loop reference counting. */
uint32_t active_handles;
void* handles_queue[2];
void* active_reqs[2];
/* Internal flag to signal loop stop. */
uint32_t stop_flag;
/* platform dependent fields */
UV_LOOP_PRIVATE_FIELDS
};
在真正使用该变量时需要调用uv_loop_init去初始化部分的成员变量,再去调用uv_run,然后uv库就会自己去做别人给他注册的回调函数了,从我们的角度来看就是就去调用本地文件接口或者网络流接口就行了。
接下来主要说一下,uv_run里面的最重要的函数uv__io_poll
void uv__io_poll(uv_loop_t* loop, int timeout) {
struct pollfd pfd;
struct pollfd* pe;
QUEUE* q;
uv__io_t* w;
uint64_t base;
uint64_t diff;
int nevents;
int count;
int nfd;
int i;
if (loop->nfds == 0) {
assert(QUEUE_EMPTY(&loop->watcher_queue));
return;
}
while (!QUEUE_EMPTY(&loop->watcher_queue)) {
q = QUEUE_HEAD(&loop->watcher_queue);
QUEUE_REMOVE(q);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, watcher_queue);
assert(w->pevents != 0);
assert(w->fd >= 0);
assert(w->fd < (int)loop->nwatchers);
pfd.fd = w->fd;
pfd.events = w->pevents;
uv__add_pollfd(loop, &pfd);
w->events = w->pevents;
}
assert(timeout >= -1);
base = uv_now(loop);
count = 5;
for (;;) {
nfd = poll(loop->pollfds, loop->npollfds, timeout);
SAVE_ERRNO(uv_update_time(loop));
if (nfd == 0) {
assert(timeout != -1);
return;
}
if (nfd == -1) {
int err = get_errno();
if (err == EAGAIN ) {
set_errno(0);
}
else if ( err != EINTR) {
TDLOG("uv__io_poll abort for errno(%d)", err);
ABORT();
}
if (timeout == -1) {
continue;
}
if (timeout == 0) {
return;
}
goto update_timeout;
}
nevents = 0;
for (i = 0; i < loop->npollfds; ++i) {
pe = &loop->pollfds[i];
if (pe->fd < 0)
continue;
w = loop->watchers[pe->fd];
if (!(pe->revents & (UV__POLLIN | UV__POLLOUT | UV__POLLHUP)))
continue;
//w = loop->watchers[pe->fd];
if (w) {
w->cb(loop, w, pe->revents);
++nevents;
}
else {
pe->fd = -1;
}
}
uv__cleanup_pollfds(loop);
if (nevents != 0) {
if (--count != 0) {
timeout = 0;
continue;
}
return;
}
if (timeout == 0) {
return;
}
if (timeout == -1) {
continue;
}
update_timeout:
assert(timeout > 0);
diff = uv_now(loop) - base;
if (diff >= (uint64_t)timeout) {
return;
}
timeout -= diff;
}
}
实际上该函数就是使用了poll,只要有本地数据,网络数据可读或者可写,就会直接去执行callback函数。需要注意的是如果调用了uv_async_send函数,也会在poll中成功返回。
w->cb中的回调函数主要是通过void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd)函数去注册,uv库里面与三个注册回调函数:
1. uv__io_init(&wa->io_watcher, uv__async_io, pipefd[0]);看接口也知道,最后一个参数是文件描述符fd,而该文件描述符是管道,如果要在poll中有数据可读写,直接向pipefd[1]操作即可。该回调函数是也是三个回调函数中比较重要的函数。
static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
struct uv__async* wa;
char buf[1024];
unsigned n;
ssize_t r;
int err;
n = 0;
for (;;) {
r = read(w->fd, buf, sizeof(buf));
if (r > 0)
n += r;
if (r == sizeof(buf))
continue;
if (r != -1)
break;
err = get_errno();
if (err == EAGAIN || err == EWOULDBLOCK)
break;
err = get_errno();
if (err == EAGAIN || err == EWOULDBLOCK)
break;
if (err == EINTR)
continue;
TDLOG("uv__async_io abort for errno(%d)", err);
ABORT();
}
wa = container_of(w, struct uv__async, io_watcher);
#if defined(__linux__)
if (wa->wfd == -1) {
uint64_t val;
assert(n == sizeof(val));
memcpy(&val, buf, sizeof(val)); /* Avoid alignment issues. */
wa->cb(loop, wa, val);
return;
}
#endif
//LOGD("uv async", "get send event true");
wa->cb(loop, wa, n);
}
最终的核心,该函数还是会去调用注册好的回调函数static void uv__async_event(uv_loop_t* loop, struct uv__async* w, unsigned int nevents);
该函数还是调用回调函数,之后就是各自的回调函数不再统一了。
都是通过int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb)函数注册的。
NOTE:需要注意的是该函数在uv自己体系里面会去初始化注册一部分自己的回调函数,同时我们自己编写的代码也可能会去注册自己的回调函数,这两者之间的执行先后顺序是不一定的。最终是int uv_async_send(uv_async_t* handle)来触发自己相应的回调函数去执行,所以需要注意的是注册的各个回调函数共用的全局变量被同时使用导致内核crush的问题。我在做项目的过程中就遇到了类似的问题。(主要是在使用uv本地文件读写的接口时有使用到)。
2. uv__io_init(&handle->io_watcher, uv__poll_io, fd);
3.uv__io_init(&stream->io_watcher, uv__stream_io, -1);该注册的回调函数主要是个网络流使用的,一开始最后一个参数为-1,当创建socket成功之后,就会被重新赋值。
因为我这里接触本地文件读的情况比较多,所以写一下uv_fs_read的详细流程
int uv_fs_read(uv_loop_t* loop, uv_fs_t* req,
uv_file file,
const uv_buf_t bufs[],
unsigned int nbufs,
int64_t off,
uv_fs_cb cb) {
INIT(READ);
req->file = file;
req->nbufs = nbufs;
req->bufs = req->bufsml;
if (nbufs > ARRAY_SIZE(req->bufsml))
req->bufs = (uv_buf_t*)malloc(nbufs * sizeof(*bufs));
if (req->bufs == NULL)
return -ENOMEM;
memcpy(req->bufs, bufs, nbufs * sizeof(*bufs));
req->off = off;
POST;
}
一开始就是初始化一些结构体变量,表明这是UV_FS类型的req,当然文件相关的fd,读取的buffer地址和长度也需要告知。
实际上关键的是POST函数
#define POST \
do { \
if ((cb) != NULL) { \
uv__work_submit((loop), &(req)->work_req, uv__fs_work, uv__fs_done); \
return 0; \
} \
else { \
uv__fs_work(&(req)->work_req); \
uv__fs_done(&(req)->work_req, 0); \
return (req)->result; \
} \
} \
while (0)
如果是使用同步接口的话,就直接执行else分支,如果使用异步接口的话就使用上面的分支,无论怎么样最终都会执行uv__fs_work和uv__fs_done这两个函数。
我们一般使用异步接口,uv__work_submit函数实际上就是插入队列中,其中之前说过的会有两个线程,线程做的就是不断地从插入的队列中取出来,然后执行相应的回调函数。
static void post(QUEUE* q) {
uv_mutex_lock(&_mutex);
QUEUE_INSERT_TAIL(&_wq, q);
uv_mutex_unlock(&_mutex);
uv_cond_signal(&_cond);
}
这个是插入队列并且发送线程信号通知worker线程去取队列里面的req。
static void worker(void* arg) {
struct uv__work* w;
QUEUE* q;
(void) arg;
for (;;) {
uv_mutex_lock(&_mutex);
while (QUEUE_EMPTY(&_wq) && QUEUE_EMPTY(&_wq_low)) {
uv_cond_wait(&_cond, &_mutex);
}
if (!QUEUE_EMPTY(&_wq))
q = QUEUE_HEAD(&_wq);
else
q = QUEUE_HEAD(&_wq_low);
if (q == &_exit_message)
uv_cond_signal(&_cond);
else {
QUEUE_REMOVE(q);
QUEUE_INIT(q); /* Signal uv_cancel() that the work req is
executing. */
}
uv_mutex_unlock(&_mutex);
if (q == &_exit_message)
break;
w = QUEUE_DATA(q, struct uv__work, wq);
w->work(w);
uv_mutex_lock(&w->loop->wq_mutex);
w->work = NULL; /* Signal uv_cancel() that the work req is done
executing. */
QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex);
}
}
Note:需要注意的是uv_fs_read函数,可能会被不同的线程调用,一个是worker线程,一个自己的线程,两个线程对于全局变量内存的使用需要格外的注意!