当前位置:首页 > 技术学院 > 技术前线
[导读]很多开发者初学并发编程时,只会背一句“线程是CPU调度的基本单位,进程是资源分配的基本单位”,对线程之间的资源共享边界始终模糊不清:有的开发者误以为同一个进程里的所有线程什么资源都能直接共用,写出大量数据竞争的Bug;还有的开发者误以为线程之间的资源完全独立,浪费大量精力去做完全没必要的进程间通信逻辑。实际上Linux系统下同一个进程内的所有线程,既不是完全资源共享,也不是完全资源隔离,内核在设计时做了极其精细的资源划分,一部分资源被所有线程完全共享,另一部分资源则是每个线程完全私有的,理清这个清晰的共享边界,是写出安全可靠的多线程代码的前置基础。

很多开发者初学并发编程时,只会背一句“线程是CPU调度的基本单位,进程是资源分配的基本单位”,对线程之间的资源共享边界始终模糊不清:有的开发者误以为同一个进程里的所有线程什么资源都能直接共用,写出大量数据竞争的Bug;还有的开发者误以为线程之间的资源完全独立,浪费大量精力去做完全没必要的进程间通信逻辑。实际上Linux系统下同一个进程内的所有线程,既不是完全资源共享,也不是完全资源隔离,内核在设计时做了极其精细的资源划分,一部分资源被所有线程完全共享,另一部分资源则是每个线程完全私有的,理清这个清晰的共享边界,是写出安全可靠的多线程代码的前置基础。

首先所有线程100%共享的核心资源,是整个进程的虚拟地址空间。同一个进程的所有线程,共用同一套页表映射关系,所有线程看到的虚拟地址空间完全一致,这也是线程之间通信效率远高于进程间通信的核心根源。任意一个线程,都可以直接读写进程虚拟地址空间里的任意合法内存区域:全局变量、堆中动态分配的内存、动态链接库的代码段和数据段,所有这些内存资源,不需要任何特殊的IPC机制,线程之间直接通过指针就可以互相访问。这种天然的内存共享能力,让线程之间交换数据的开销几乎可以忽略不计,不需要像进程间通信那样走管道、消息队列这类需要内核中转的复杂机制,但也带来了最常见的并发问题:多个线程同时修改同一个共享内存变量时,如果没有加同步保护,就会出现数据竞争,导致变量的值出现完全不符合预期的错乱。

除了虚拟地址空间,所有线程还完全共享进程的所有文件描述符表。当任意一个线程打开一个文件、创建一个套接字、申请一个管道时,返回的文件描述符会被加入整个进程的文件描述符表中,其他所有线程都可以直接使用这个文件描述符,执行读写操作。甚至任意一个线程把这个文件描述符关闭,其他所有线程对应的这个文件句柄也会立刻失效。这种共享特性带来了很多容易踩坑的场景:比如多个线程同时往同一个文件描述符里写入数据,如果没有加锁保护,不同线程的写入内容会互相穿插,文件里的内容就会出现完全混乱的交错状态。很多开发者误以为每个线程打开的文件是独立的,实际上文件描述符是全局共享的,底层的文件打开状态、文件读写偏移量也都是所有线程共享的,任意一个线程修改了文件的读写偏移,其他线程后续的读写位置也会跟着变化。

进程的其他全局系统资源,也全部被所有线程共享。所有线程共用同一个用户ID和用户组ID,共用进程的所有权限凭据,没有任何一个线程可以单独修改自己的权限身份,所有线程的权限完全一致。进程的工作目录、umask掩码、环境变量列表、信号处理函数表,这些全局属性也全部是所有线程共享的,任意一个线程修改了环境变量,其他线程立刻就能看到新的值,任意一个线程修改了某个信号的处理函数,所有线程收到这个信号时都会执行同一个处理逻辑。甚至进程的资源限制、CPU亲和性的全局配置,也全部是所有线程共享的,一个线程修改了全局的资源限制,所有线程的资源配额都会同步更新。

但线程之间也不是完全没有私有资源,操作系统为每个线程单独分配了一批私有资源,保证每个线程的执行流可以完全独立运行。最核心的线程私有资源就是线程栈,每个线程都有自己完全独立的栈空间,栈上的局部变量、函数调用参数、函数返回地址,其他线程完全不能直接随意访问。哪怕多个线程执行同一个函数,每个函数的局部变量也会在各自的私有栈上分配,互相之间完全不会干扰,这也是多线程可以并发执行同一段业务逻辑的基础保障。除了栈之外,每个线程还有自己独立的寄存器上下文,包括程序计数器、通用寄存器的状态,CPU调度切换线程时,内核会把当前线程的所有寄存器状态保存到线程私有的上下文中,恢复下一个线程的寄存器状态,不同线程的执行流完全独立,不会互相打断。

除此之外,每个线程还有自己独立的线程ID、信号掩码、调度优先级、errno变量。信号掩码决定了当前线程会屏蔽哪些信号,同一个进程里的不同线程可以设置完全不同的信号掩码,一个线程屏蔽的信号,其他线程可以正常处理。Linux内核向进程发送信号时,会把信号随机递交给任意一个没有屏蔽该信号的线程处理,开发者经常遇到的信号被莫名其妙的线程吃掉的疑难问题,根源就是不同线程的信号掩码配置不一致。每个线程的errno变量也是完全独立的,一个系统调用出错修改了当前线程的errno值,完全不会影响其他线程的errno状态,这也是标准库的errno实现线程安全的底层基础。

理清线程之间的资源共享与私有边界,你才能彻底避开多线程编程中的各类隐性陷阱:所有共享资源的并发访问必须加同步保护,所有线程私有资源不需要额外同步,既不会写出有数据竞争的Bug,也不会做多余的无效同步操作,写出既安全又高性能的多线程代码。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭