一、什么是线程
1、进程与线程
进程:一个正在执行的程序,它是OS资源分配的最小单位/基本单位。
进程中的事情需要按照一定的顺序逐个进行,那么如何让一个进程中的一些事情同时执行?
线程:有时又称轻量级进程,程序执行的最小单位,系统独立调度和分派cpu的基本单位,它是进程中的一个实体。一个进程中可以有多个线程,这些线程共享进程的所有资源,线程本身只包含一点必不可少的资源。
进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
二、一些术语
1、并发
并发是指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上看起来具有多个进程同时执行的效果。
看起来是同时发生的,是一种假象,针对单核处理器。
2、并行
并行是指在同一时刻,有多条指令在多个处理器上同时执行。
真正的同时发生,针对多核处理器。
3、同步
同步:彼此有依赖关系的调用不应该“同时发生”,而同步就是要阻止那些“同时发生”的事情。通过锁,来阻止互相有依赖的事情同时发生。
4、异步
异步的概念和同步相对,任何两个彼此独立的操作是异步的,它表明事情的独立发生。
世界上任何两个东西都是异步的,但因为外部强加的条件,会变为同步。
在谈恋爱前,2个人都是异步的;谈恋爱以后,就变成同步的了。
三、多线程的优势
- 在多处理器中开发程序的并行性
- 在等待慢速IO操作时,程序可以执行其他操作,提高并发性
- 模块化的编程,能更清晰地表达程序中独立事件的关系,结构清晰
- 占用较少的系统资源(多进程占用很多的系统资源)
多线程不一定要多处理器。
四、线程id
pthread_t
:结构体(FreeBSD5.2、Mac OS10.3)
$ vi /usr/include/bits/pthreadtypes.h
// Thread identifiers
typedef unsigned long int pthread_t;
获取线程ID:pthread_self()
$ man pthread_self
#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread. // 编译的时候,要用-pthread选项,即用到pthread这个工具
例1:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
int main()
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("pid is %u, tid is %lu\n", pid, tid);
return 0;
}
思考:线程ID出了进程范围还有效吗?
无效。
五、创造线程
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg)
第一个参数:新线程的id,如果成功则新线程的id回填充到tidp指向的内存
第二个参数:线程属性(调度策略,继承性,分离性...)
第三个参数:回调函数(新线程的启动函数;新线程要执行的函数)
第四个参数:回调函数的参数(传递给新线程)
返回值:成功返回0,失败则返回错误码
编译时需要连接库libpthread
六、初始线程/主线程
1、当c程序运行时,首先运行main函数。在线程代码中,这个特殊的执行流被称作初始线程或者主线程。你可以在初始线程中做任何普通线程可以做的事情。
2、主线程的特殊性在于,它在main函数返回的时候,会导致进程结束,进程内所有的线程也将会结束。这可不是一个好的现象,你可以在主线程中调用pthread_exit函数,这样进程就会等待所有线程结束时才终止。
3、主线程接受参数的方式是通过argc和argv,而普通的线程只有一个参数void*
4、在绝大多数情况下,主线程在默认堆栈上运行,这个堆栈可以增长到足够的长度。而普通线程的堆栈是受限制的,一旦溢出就会产生错误。
- 主线程是随着进程的创建而创建
- 其他线程可以通过调用函数来创建,主要调用pthread_create
- 请注意,新线程可能在当前线程从函数pthread_create返回之前就已经运行了,甚至新线程可能在当前线程从函数pthread_create返回之前就已经运行完毕了。
七、线程的状态
1、线程的4个基本状态
就绪:当线程刚被创建时就处于就绪状态,或者当线程被解除阻塞以后也会处于就绪状态。就绪的线程在等待一个可用的处理器,当一个运行的线程被抢占时,它立刻又回到就绪状态。
运行:当处理器选中一个就绪的线程执行时,它立刻变成运行状态。
阻塞:线程会在以下情况下发生阻塞:试图加锁一个已经被锁住的互斥量,等待某个条件变量,调用singwait等待尚未发生的信号,执行无法完成的I/O信号,由于内存页错误。
终止:线程通常启动函数中返回来终止自己,或者调用pthread_exit退出,或者取消线程
2、回收
线程的分离属性:
分离一个正在运行的线程并不影响它,仅仅是通知当前系统该线程结束时,其所属的资源可以回收。一个没有被分离的线程在终止时会保留它的虚拟内存,包括他们的堆栈和其他系统资源,有时这种线程被称为“僵尸线程”。创建线程时默认是非分离的。
如果线程具有分离属性,线程终止时会被立刻回收,回收将释放掉所有在线程终止时未释放的系统资源和进程资源,包括保存线程返回值的内存空间、堆栈、保存寄存器的内存空间等。
终止被分离的线程会释放所有的系统资源,但是你必须释放由该线程占有的程序资源。由malloc或者mmap分配的内存可以在任何时候由任何线程释放,条件变量、互斥量、信号灯可以由任何线程销毁,只要他们被解锁了或者没有线程等待。但是只有互斥量的主人才能解锁它,所以在线程终止前,你需要解锁互斥量。
八、线程基本控制
1、线程终止
(1)exit函数是危险的
如果进程中的任意一个线程调用了exit函数族:exit,_Exit,_exit,那么整个进程就会终止。
(2)不终止进程的退出方式
普通的单个线程有以下3种方式退出,这样不会终止进程
1)从启动例程中返回,返回值是线程的退出码
2)线程可以被同一进程中的其他线程取消
3)线程调用pthread_exit(void *rval)函数,rval是退出码
2、线程连接
int pthread_join(pthead_t tid, void **rval)
调用该函数的线程会一直阻塞,直到指定的线程tid调用pthread_exit退出,或者从启动例程返回或者被取消。
参数tid就是指定线程的id
参数rval是指定线程的返回码,如果线程被取消,那么rval被置为PTHREAD_CANCELED
该函数调用成功会返回0,失败返回错误码
调用pthread_join会使指定的线程处于分离状态,如果指定线程已经处于分离状态,那么调用就会失败。
pthread_detach可以分离一个线程,线程可以自己分离自己。
int pthread_detach(pthread_t thread);
成功返回0,失败返回错误码
3、线程取消
(1)取消函数
int pthread_cancle(pthread_t tid)
取消tid指定的线程,成功返回0。
取消只是发送一个请求,并不意味着等待线程终止,而且发送成功也不意味着tid一定会终止。
(2)取消状态
取消状态,就是线程对取消信号的处理方式,忽略或者响应。线程创建时默认响应取消信号。
int pthread_setcancelstate(int state, int *oldstate)
设置本线程对Cancel信号的反应,state有两种值:
PTHREAD_CANCEL_ENABLE(缺省)和
PTHREAD_CANCEL_DISABLE,
分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;
old_state如果不为NULL则存入原来的Cancel状态以便恢复。
(3)取消类型
取消类型,是线程对取消信号的响应方式,立即取消或者延时取消。
线程创建时默认延时取消。
int pthread_setcanceltype(int type, int *oldtype)
设置本线程取消动作的执行时机,type由两种取值:
PTHREAD_CANCEL_DEFFERED(延时取消)和
PTHREAD_CANCEL_ASYCHRONOUS(立即取消),仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入传来的取消动作类型值。
(4)取消点
取消一个线程,它通常需要被取消线程的配合。线程在很多时候会查看自己是否有取消请求。
如果有就主动退出, 这些查看是否有取消的地方称为取消点。
很多地方都是包含取消点,包括
pthread_join()、 pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()、write、read,大多数会阻塞的系统调用。
man pthreads里面有取消点的函数列表
4、向线程发送信号
(1)pthread_kill
int pthread_kill(pthread_t thread, int sig);
别被名字吓到,pthread_kill可不是kill,而是向线程发送signal。还记得signal吗,大部分signal的默认动作是终止进程的运行,所以,我们才要用sigaction()去抓信号并加上处理函数。
向指定ID的线程发送sig信号,如果线程代码内不做处理,则按照信号默认的行为影响整个进程,也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。
如果要获得正确的行为,就需要在线程内实现sigaction了。
所以,如果int sig的参数不是0,那一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则,就会影响整个进程。
如果int sig是0呢,这是一个保留信号,其实并没有发送信号,作用是用来判断线程是不是还活着。
(2)信号处理
进程信号处理:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
给信号signum设置一个处理函数,处理函数在sigaction中指定
act.sa_mask 信号屏蔽字
act.sa_handler 信号集处理程序
int sigemptyset(sigset_t *set); 清空信号集
int sigfillset(sigset_t *set); 将所有信号加入信号集
int sigaddset(sigset_t *set, int signum); 增加一个信号到信号集
int sigdelset(sigset_t *set, int signum); 删除一个信号到信号集
多线程信号屏蔽处理
/* int sigprocmask(int how, const sigset_t *set, sigset_t oldset)/
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
how = SIG_BLOCK:向当前的信号掩码中添加set,其中set表示要阻塞的信号组。
SIG_UNBLOCK:向当前的信号掩码中删除set,其中set表示要取消阻塞的信号组。
SIG_SETMASK:将当前的信号掩码替换为set,其中set表示新的信号掩码。
在多线程中,新线程的当前信号掩码会继承创造它的线程的信号掩码
一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如 SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。
5、清除操作
线程可以安排它退出时的清理操作,这与进程的可以用atexit函数安排进程退出时需要调用的函数类似。这样的函数称为线程清理处理程序。线程可以建立多个清理处理程序,处理程序记录在栈中,所以这些处理程序执行的顺序与他们注册的顺序相反
pthread_cleanup_push(void (*rtn)(void*), void *args)//注册处理程序
pthread_cleanup_pop(int excute)//清除处理程序
二者要成对出现。
当执行以下操作时调用清理函数,清理函数的参数由args传入
1、调用pthread_exit
2、响应取消请求
3、用非零参数调用pthread_cleanup_pop