每周500字linux c/c++杂谈我用 Linux

线程浅析

2019-05-17  本文已影响10人  cpp加油站

1. 线程的基本属性

每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程id,一组寄存器器值,栈,调度优先级和策略,信号屏蔽字,error变量以及线程私有数据。
一个进程的所有信息对该进程的所有线程都是共享的,包括代码段,静态区,堆,栈以及文件描述符。

2. 线程部分函数

int pthread_equal(pthread_t tid1, pthread_t tid2); //比较两个线程id是否相等,相等返回非0数值,否则返回0
pthread_exit //用在子线程中,结束当前子线程
pthread_t pthread_self(void) //用在子线程中,获取线程id
pthread_attr_init //设置pthread_attr_t(堆栈大小、调度优先级、分离状态、栈地址等)结构体属性,必须在pthread_create之前调用
pthread_attr_destory //对应于pthread_attr_init,释放资源
int pthread_create; //第一个参数是线程id取地址,第二个参数是pthread_attr_t*,第三个参数是函数指针,第四个参数是入参指针
int pthread_cancel(pthread_t tid); //用来请求取消同一进程中的其他线程

3. pthread_join和pthread_detach

在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

4. 多线程中使用fork

4.1 先回顾一下fork函数

我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的PID。

4.2 多线程中使用fork

在Linux中多线程中,fork的时候只复制当前调用fork 的线程到子进程,也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。这就是多线程中fork所带来的一切问题的根源所在了。

而互斥锁,就是多线程fork大部分问题的关键部分。

在大多数操作系统上,为了性能的因素,锁基本上都是实现在用户态的而非内核态(因为在用户态实现最方便,基本上就是通过原子操作或者之前文章中提到的memory barrier实现的),所以调用fork的时候,会复制父进程的所有锁到子进程中。

问题就出在这了。从操作系统的角度上看,对于每一个锁都有它的持有者,即对它进行lock操作的线程。假设在fork之前,一个线程对某个锁进行的lock操作,即持有了该锁,然后另外一个线程调用了fork创建子进程。可是在子进程中持有那个锁的线程却"消失"了,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了。

那么如果子进程中的任何一个线程对这个已经被持有的锁进行lock操作话,就会发生死锁。

当然了有人会说可以在fork之前,让准备调用fork的线程获取所有的锁,然后再在fork出的子进程的中释放每一个锁。先不说现实中的业务逻辑以及其他因素允不允许这样做,这种做法会带来一个问题,那就是隐含了一种上锁的先后顺序,如果次序和平时不同,就会发生死锁。

如果你说自己一定可以按正确的顺序上锁而不出错的话,还有一个隐含的问题是你所不能控制的,那就是库函数。

因为你不能确定你所用到的所有库函数都不会使用共享数据,即他们都是完全线程安全的。有相当一部分线程安全的库函数都是在内部通过持有互斥锁的方式来实现的,比如几乎所有程序都会用到的C/C++标准库函数malloc、printf等等。

比如一个多线程程序在fork之前难免会分配动态内存,这就必然会用到malloc函数;而在fork之后的子进程中也难免要分配动态内存,这也同样要用到malloc,可这却是不安全的,因为有可能malloc内部的锁已经在fork之前被某一个线程所持有了,而那个线程却在子进程中消失了。

4.3 exec与文件描述符

exec函数族可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完后,原调用进程的内容除了进程号外,其它全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。

按照上文的分析,似乎多线程中在fork出的子进程中立刻调用exec函数是唯一明智的选择了,但这样在调用exec之前,子进程就只能调用异步信号安全的函数。这样虽然没有锁的问题了,但却限制了在调用exec之前,子进程能做的事情。

4.4 pthread_atfork函数

如果你不幸真的碰到了一个要解决多线程中fork的问题的时候,可以尝试使用pthread_atfork:

int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));

prepare处理函数由父进程在fork创建子进程前调用,这个函数的任务是获取父进程定义的所有锁。

parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的任务是对prepare获取的所有锁解锁。

child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

因为子进程继承的是父进程的锁的拷贝,所有上述并不是解锁了两次,而是各自独自解锁。可以多次调用pthread_atfork函数从而设置多套fork处理程序,但是使用多个处理程序的时候。处理程序的调用顺序并不相同。parent和child是以它们注册时的顺序调用的,而prepare的调用顺序与注册顺序相反。这样可以允许多个模块注册它们自己的处理程序并且保持锁的层次(类似于多个RAII对象的构造析构层次)。

需要注意的是pthread_atfork只能清理锁,但不能清理条件变量。在有些系统的实现中条件变量不需要清理。但是在有的系统中,条件变量的实现中包含了锁,这种情况就需要清理。但是目前并没有清理条件变量的接口和方法。

4.5 总结

在多线程程序中最好只用fork来执行exec函数,不要对fork出的子进程进行其他任何操作。

上一篇下一篇

猜你喜欢

热点阅读