lookupdns
程序的流程图如下:
lookupdns
程序有 3 个重点,如下:
1, st_thread_create(do_resolve,...)
创建协程
什么是协程?你可以把协程理解成一个函数,do_resolve()
函数就是一个协程,在 lookupdns
里面创建了两个 do_resolve()
协程。
虽然一个线程里面可以有多个协程,但是任何一个协程都是独占 线程的时间片的。假设 lookupdns
里面有两个协程 A 跟 B,协程A 在 0.02~0.04
的时候运行,然后协程 A 阻塞了。协程 A 发送完 DNS 请求之后就阻塞了,在等待服务器回复。这时候,协程B 就在 0.04~0.06
的时间片运行,来发送 DNS 请求。
任何一个时候,都只有一个协程在运行。
而且 StateThreads 的协程,可以说是由我们的程序主动切换的,是我们主动切换的协程,就是在 st_recvfrom()
函数里面进行切换协程。
st_recvfrom()
是一个阻塞函数,但是不要认为阻塞就是坏事,无论是线程阻塞,还是协程阻塞,阻塞都不代表计算机停下来不工作了,而是计算机切换到另个一个地方去工作了,所以在当前的任务看起来就是阻塞的状态。
例如 线程A 阻塞了,CPU 就会切换到 线程B 进行工作。协程A 阻塞了,CPU 就会切换到 协程B 进行工作。这是同样的原理来的。
阻塞是因为需要等待 文件IO 到来,或者 网络IO 到了,程序必须等待 读取到文件数据 或者 读取到网络的IO数据 才能往下走,这时候就需要阻塞。
回到我们主题,StateThreads 的协程是主动切换的,就在 st_recvfrom()
函数里面进行切换协程。主动切换有一个好处,就是多协程操作一些全局变量的时候会变得非常方便,不需要加锁。
多线程 针对一个 int num
进行 ++
操作的时候,通常需要加锁,或者采用一些别的方法技巧,否则就会导致数据错乱。
多协程 针对一个 int num
进行 ++
操作的时候,就完全不需要加锁,伪代码如下:
scss
int num = 0;
void *do_resolve(void *host){
num++; //全局变量 num ++
...省略逻辑...
st_sendto();
st_recvfrom();
...省略逻辑...
}
上面的代码里,即使两个 do_resolve()
协程跑起来,num
的递增也是正确的,最后的结果一定是 2 。为什么呢?
答:虽然 num++
这行 C 代码翻译成汇编是 3 条指令,如下:
perl
movl num(%rip), %eax
addl $1, %eax
movl %eax, num(%rip)
上面的代码先把 num
的值放到 eax 寄存器,然后对 eax
寄存器加1,最后把 eax 的值拷贝回去 num 的内存地址。
这 3 条指令是存在中间状态的,但是只有执行到后面 st_recvfrom()
函数才会切换协程,所以这 3 条指令肯定是一起执行完的,中间不会切换到其他协程。所以不会有可见性问题。
而如果是多线程来运行 do_resolve
,线程的切换是操作系统控制的,所以可能会在 num++
的 3 条指令中间切开。
例如当线程A执行到 第1条指令的时候,刚读取 0 到 eax
的时候。就切换到 线程B。这时候,线程 A 看到的 num 内存数据的值是 0 ,线程 B 看见的 num 内存的数据也是 0,当线程 B 把 eax + 1 之后,把 eax 的值拷贝回去 num 的内存地址 之后。操作系统再切换回 线程A,因为线程A 之前已经读取 0 到 eax
了,所以此时线程 A 的 eax 寄存器是 0 ,而不是 线程 B 修改后的 1。
线程 A 把 eax + 1 之后,再把 eax 的值拷贝回去 num 的内存地址。
所以最终 线程A+B 运行完之后, num 的内存地址是 1,而不是 2,这就是线程间的可见性问题。
但是多协程是没有这个可见性问题的,因为不会在 num++
的中间被切走。这就是 StateThreads
用户层主动切换协程的好处。
提醒:每个线程也是有自己的独立的 eax 等通用寄存器的。
2,st_netfd_open_socket() 打开 socket
上图的 socket()
函数是操作系统的函数,是用来创建一个 UDP socket 的,如果不了解网络编程,请先阅读一遍《Unix网络编程》。
我们知道 UDP socket 其实是不需要 open 的,只需创建一个 UDP socket ,然后 sendto 直接往这个 UDP socket 发送数据就行了。
那疑问来了,这个 st_netfd_open_socket()
是干什么的?
st_netfd_open_socket()
函数的作用其实就是创建一个 StateThread
自己的 socket,也就是 _st_netfd
,_st_netfd
是对 UDP socket 进行了一层包装,方便使用,如下:
arduino
typedef struct _st_netfd {
int osfd; /* Underlying OS file descriptor */
int inuse; /* In-use flag */
void *private_data; /* Per descriptor private data */
_st_destructor_t destructor; /* Private data destructor function */
void *aux_data; /* Auxiliary data for internal use */
struct _st_netfd *next; /* For putting on the free list */
} _st_netfd_t;
这里需要注意,StateThread
协程的切换,就是根据 socket fd 进行切换的,只有某个 fd 阻塞了,才会进行协程切换。
当然你可以用 st_usleep()
让出来当前协程的时间片,st_usleep()
也会导致切换到其他协程运行。这个函数后面会讲到。
st_recvfrom()
进行协程切换的地方如下图所示:
st_netfd_poll()
里面协程管理的逻辑是非常复杂的,这里简单剧透一下,总之就是一个线程里面需要执行的指令有很多很多,这些指令又分给线程里面的各个协程。
协程有自己的上下文,可以理解为协程有自己的寄存器跟栈空间。当开始切换到协程B的时候,会把协程A的 eax 寄存器等等信息先保存到内存里面,然后再把协程B的寄存器数据换进来,协程B 的寄存器信息之前在内存里。
协程B的寄存器数据换进来之后,协程B 就能继续跑了。协程B 阻塞之后,StateThread
的管理器就会看看还有没其他协程需要运行,如果有,就切换到其他协程运行。例如 协程B 阻塞的时候,协程A 的网络IO 已经有数据到了,那协程A 就可以激活继续跑。
st_netfd_poll()
里面干的就是这么一个事情。
本文是《 SRS原理 》一书中的文章,如需观看更多内容,请购买本书。