基础总结-进程和线程
[TOC]
线程
线程创建
pthread_create创建线程,创建之后,就开始执行相关线程函数;
//线程创建原型
int pthread_create(pthread_t* thread,
pthread_attr_t *attr,
void *(*start_routine)(void),
void* arg
);
//实例
void* thr_fn(void* arg)
{
return (void*)0;
}
pthread_t ntid;
int err = pthread_create(&ntid, NULL, thr_fn, NULL);
线程退出
- 线程只是从启动例程中返回,返回值就是线程的退出码;
在线程函数运行结束,线程也会随着这个退出;这是线程退出的一种方式;
- 线程可以被同一个进程中的其他线程取消;
线程可以通过调用pthread_cancel函数来请求取消同一个进程中的其他线程;该函数不等待线程终止,它仅仅是提出请求;
- 线程调用pthread_exit;
这种方式是线程退出的主动行为;
pthread_join
int pthread_join(pthread_t thread, void **retval);
调用该函数可以把当前的线程挂起,等待其参数指定的线程结束,这个函数是阻塞函数,有点类似进程中的wait/wait_pid;
注意:在线程执行函数内部,一定不要调用exit()这些函数,因为这样会把整个进程退出;
线程退出清理函数
void pthread_cleanup_push(void (*rtn)(void*), void* arg);
void pthread_cleanup_pop(int execute);
- 线程退出时候我们可以安排它退出的时候调用的函数,用于清理线程内的相关资源;
- 线程可以建立多个清理处理程序;处理程序记录在栈中,执行顺序和注册顺序相反;
- 一下情况下才会调用清理函数:
- 调用pthread_exit时;
- 相应取消请求时;
- 用非零的execute参数调用pthread_clean_pop时候;
- 如果线程从启动例程中返回而终止的话,那么清理处理程序就不会被调用;
关于线程joinable状态和unjoinable(分离)状态说明
- linux线程执行和windows不同,pthread有两种状态joinable状态和unjoinable状态;
- 一个线程默认的状态是joinable,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。
- unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。如果线程状态为joinable,需要在之后适时调用pthread_join;
线程和进程原语对比
线程同步
- 互斥量
互斥量封装:
//线程锁
class Mutex : boost::noncopyable
{
public:
Mutex()
{
pthread_mutex_init(&mutex_, NULL);
}
~Mutex()
{
pthread_mutex_destroy(&mutex_);
}
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_lock(&mutex_);
}
private:
pthread_mutex_t mutex_;
};
- 读写锁
- 读写锁有三种状态:读模式下加锁,写模式下加锁,不加锁状态;
- 一次只有一个线程可以占用写模式下的读写锁;但是多个线程可以同时占用读模式下的读写锁;
- 写加锁状态,所有试图对这个锁加锁的线程都会阻塞;
- 读加锁状态下,所有试图以读模式加锁的线程都可以得到访问权限,所有以写模式加锁的线程,必须等待所有读线程释放所有的读锁;
- 如果当前是读锁模式下,此时有线程要添加写锁,读写锁会阻塞随后到来的读模式锁请求,以免读模式锁长期占用;
- 使用场景:非常适合于对数据结构读的次数大于写的情况;
- 条件变量
- 条件变量和互斥量一起使用时,允许线程以无竞争的方式等待待定的条件发生;
- 条件本身是有互斥量保护的;线程在改变条件状态前必须首先锁住互斥量; ``` /* * 条件变量 */ class Condition : boost::noncopyable { public: explicit Condition(MutexLock& mutex) :mutex_(mutex) { pthread_cond_init(&pcond_, NULL); } ~Condition() { pthread_cond_destroy(&pcond_); } void Wait() { pthread_cond_wait(&pcond_, mutex_.GetThreadMutex()); } void Notify() { pthread_cond_signal(&pcond_); } void NotifyAll() { pthread_cond_broadcast(&pcond_); }
private: pthread_cond_t pcond_; MutexLock& mutex_; };
## 线程和信号
* 每个线程都有自己的信号屏蔽字;信号处理是进程中所有线程共享的;当线程修改了某个信号的处理行为后,所有线程都必须共享这个处理行为的改变;
* 线程忽略某个处理信号,而其他线程可以恢复信号的默认处理,或者设置一个新的处理程序;
* 调用*pthread_sigmask*来阻止信号发送;
* 线程可以通过调用*sigwait*来等待多个或者一个信号发生;
* 多线程在调用sigwait等待的是同一个信号,那么就会出现线程阻塞;
* 要把信号发送到线程可以使用*pthread_kill*;
重要:
***多线程模式下,使用signal的第一个原则就是以下情况不适用signal***:
1. 不适用signal作为IPC的手段;
2. 也不要用基于signal的实现的定时函数;包括:alarm、ualarm、setitimer、timer_create、sleep、usleep;
3. 不主动处理各种异常信号(sigterm,sigint等)只用默认的语义:结束进程;有一个例外就是sigpipe 服务端程序需要忽略此信号;
4. 在没有别的替代方式情况下,把异步信号转换为同步的文件描述事件;
## 线程和fork
* 当线程调用fork时候, 就为整个子进程创建了整个进程地址空间的副本;
* 子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁、条件变量的状态;
* 父进程包含多个线程,子进程在fork之后,如果紧接着不是马上调用exec的话,就必须清理锁的状态;
* *子进程仅仅包含一个线程,它是父进程调用fork线程的副本构成的;*
* 清除锁的状态可以通过使用***pthread_afork***实现;
***注意:非常不建议多线程模式下,使用fork;当然如果非要调用fork,那么调用fork之后,立即调用exec,彻底隔断子进程和父进程的关系***
## 线程与IO
* 进程的所有线程共享相同的文件描述符;
* 多线程下使用原子读写pread和pwrite
## 线程限制
Linux系统线程的限制可以通过函数sysconf修改;
可以修改的限制主要包括:
* 线程退出时候OS试图销毁线程私有数据的最大次数;
* 进程可以创建的键的最大数目;
* 一个线程的栈可用的最小字节数;
* 进程可以创建的最大线程数;
## 线程的属性
* 线程的分离状态属性;
* 线程栈末尾的警戒缓冲区的大小;
* 线程栈的最低地址;
* 线程栈的大小;
## 线程的私有数据
线程的私有数据:是存储和查询与某个线程相关数据的一种机制;线程私有数据使得每一线程可以独立访问数据副本,而不需要担心与其他线程同步访问的问题;
1. 相关创建函数:
pthread_key_create; pthread_key_destory; ```
- __thread是GCC内置的线程局部存储设施,它的效率非常的高效,比pthread_key_t高很多,
- __thread使用规则:只能用于修饰POD类型,不能修饰class类型,因为无法自动调用构造函数和析构函数;
- __thread可用于修饰全局变量,函数内的静态变量,不能修饰函数局部变量,或者class的普通成员变量;
- __thread变量初始化,只能够使用编译器常量;
- __thread变量是每个线程有一份独立实体,各个线程的变量互不干扰;
线程使用经验
- 线程是宝贵的,一个程序可以使用几个或者十几个线程;具体个数可以参考CPU的核数;但是一台机器不能同时运行几百个、几千个用户进程,增加内核的调用负担,降低整体性能;
- 线程的创建和销毁是有代价的,一个进程最好一开始就创建所需的线程,并一直反复使用;
- 每个线程应该有明确的职责,例如IO线程,计算线程等等;
- 线程之间的交互应该尽量简单;最好线程之间只用消息传递;
- 要预先考虑清楚一个mutable shared对象将会暴露给那些线程;每个线程是读还是写,读写有无可能并发进行;
孤儿进程和僵尸进程
孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
僵尸和孤儿进程的危害
首先要理解一点:每个进程退出的时候,内核会释放该进程的所有的资源,包括打开的文件,占用的内存等等;但是仍然为其保留了一些信息如进程的PID,退出的状态,运行的时间等等;知道父进程通过wait/waitpid来取的时候才释放。
僵尸进程危害: 如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
孤儿进程危害: 理论上孤儿进程没有什么危害,孤儿进程是没有父进程的进程;每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init进程,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程结束了其生命周期的时候,init进程就会处理它的一切善后工作。
解决方法
僵尸进程的解决方式:
- 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
- fork两次,原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程;
父子进程共享
- fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
- 在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
- 当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。
- 而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
-
fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
- fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。
重要:
- fork之后,子进程继承了父进程的几乎全部状态;
- 子进程继承地址空间和文件描述符;
- 子进程不会继承的有:
- 父进程的内存锁 mlock mlockall
- 父进程的文件锁 fcntl
- 父进程的某些定时器 setitimer,alarm,timer_create;
- 使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。子进程与父进程的区别在于: 1、父进程设置的锁,子进程不继承(因为如果是排它锁,被继承的话,矛盾了) 2、各自的进程ID和父进程ID不同 3、子进程的未决告警被清除; 4、子进程的未决信号集设置为空集。
进程之间通信方式
- 消息队列
- 共享内存
- socket
- 信号量
- 信号
- eventfd
注释: 如果是父子进程可以使用共享内存 + 事件(eventfd); 如果不是父子进程,建议只使用socket;
cuipf
