目录
线程概念
什么是线程
当我们需要在一个进程中同时运行多个执行流时,我们并不可以开辟多个进程执行我们的操作(32位机器里每个进程认为它 独享 4G的内存资源),此时便引入了线程,线程就是进程中的一条执行流,是cpu调度的基本单位,但是这个执行流在linux下是通过pcb实现的,因此实际上linux下的线程就是一个pcb,并且在linux下的pcb共用一个虚拟地址空间(进程的虚拟地址空间),共享了大部分资源。相比进程更加的轻量化,所以线程也被称为轻量级进程,一个进程可拥有多个线程,它的执行力度比进程更加细致。
线程与进程的关系如下图:
线程的共享与独有
线程独有
1.标识符:唯一的标识符用来区分线程
2.栈:独有的函数调用栈,防止栈混乱
3.寄存器:其实就是pcb中的上下文数据、程序计数器等
4.信号屏蔽字:阻塞信号集合。信号会打断当前操作,但是这个线程不想被打断,就可以将这个信5.号阻塞,让其他线程去处理这个信号。
6.errno: 由于同一个进程中可能有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程也进行系统调用设置errno值,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的errno。
线程共享
线程之间共享代码段和数据段、文件描述符表、用户id、 用户组id、 信号处理方式等
线程优缺点
优点
1.线程间通信更加灵活(可以通过全局变量和函数传参的方式)线程的创建销毁成本更低
2.线程的调度成本更低
3.能充分利用多处理器的可并行数量
4.在等待慢速I/O操作结束的时间,线程可执行其他的计算任务
5.计算(CPU)密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现并行处理, 提高效率
6.I/O密集型应用, 为了提高性能, 将I/O操作重叠. 线程可以同时等待不同的I/O操作.
缺点
1. 如果CPU密集型线程的数量比可用的处理器多, 那么可能会有较大的性能损失, 这里的性能损失指的是增加了额外的同步和调度开销, 而可用的资源(CPU)不变.
2.编写多线程需要更全面更深入的考虑,在一个多线程程序里,任一线程奔溃会导致整个进程崩溃
3.多个线程对同一个数据同时进行操作时,可能出现线程安全问题,导致数据发生错误
4.多线程程序调试困难
线程控制
线程创建
pthread
该函数用来创建线程
来验证一下这个函数:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 void* rout(void* s){
5 while(1){
6 printf("%s\n",(char*)s);
7 sleep(1);
8 }
9 }
10 int main(){
11 pthread_t tid;
12 pthread_create(&tid,NULL,rout,(void*)"i am thread");//创建线程
13 while(1){
14 printf("i am main\n");
15 sleep(1);
16 }
17 return 0;
18 }
对代码进行编译,注意:由于pthread库不是Linux系统默认的库,所以在使用gcc进行编译的时候加上"-lpthread",表示链接pthread库。
运行后:
观察现象,此时主函数与回调函数中的语句都被执行了, 说明此时有两个执行流,线程创建成功。
使用命令ps -aL来查看当前线程:
注意理解以下概念:
pid: 进程ID
lwp: 线程ID
tid: 线程ID,等于lwp
tgid: 线程组ID,等于pid
观察上图,可以发现其中主线程LWP和进程PID是相同的,而其他线程的LWP和进程PID不同。
我们再来用一用这个函数,如下代码中将创建出5个线程执行同一段代码,看看会发生什么现象:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 void* my_thread(void* arg){
5 int* a=(int*)arg;
6 printf("i am my_thread:%d\n",*a);
7 sleep(1);
8 }
9 int main(){
10 pthread_t tid;
11 int i=0;
12 for(;i<5;i++){//用循环创建5个线程
13 pthread_create(&tid,NULL,my_thread,(void*)&i);
14 }
15 while(1){
16 sleep(1);
17 printf("i am main thread\n");
18 }
19 return 0;
20 }
编译运行:
竟然打印出来了5个5,按道理来说应该是创建出来的线程应该分别打印0到4这些值,但是为什么会出现上面的现象呢?
因为此时i++的速度大于线程创建的速度,也就是说i已经加到5了,循环都结束了,这5个线程才创建出来,所以打印出来5个5,对以上代码做出改进:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 #include <malloc.h>
5 void* my_thread(void* arg){
6 int* a=(int*)arg;
7 printf("i am my_thread:%d\n",*a);
8 free(a);
9 sleep(1);
10 }
11 int main(){
12 pthread_t thr;
13 int i=0;
14 int* arr=NULL;//定义int类型指针
15 for(;i<5;i++){
16 arr=(int*)malloc(sizeof(int));//每次给其申请空间用来存放i的值
17 *arr=i;
18 pthread_create(&thr,NULL,my_thread,(void*)arr);
19 }
20 while(1){
21 sleep(1);
22 printf("i am main thread\n");
23 }
24 return 0;
25 }
运行后:
观察运行结果,达到了预期结果。
线程终止
1.pthread_exit
哪个线程调用该函数,就会终止当前线程
代码验证:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 void* my_thread(void* arg){
5 while(1){
6 printf("i am my_thread\n");
7 pthread_exit(NULL);//终止当前线程
8 }
9 }
10 int main(){
11 pthread_t tid;
12 pthread_create(&tid,NULL,my_thread,(void*)NULL);
13 while(1){
14 sleep(1);
15 printf("i am main thread\n");
16 }
17 return 0;
18 }
运行后:
2.pthread_cancel
调用该函数,可以终止其它线程
代码验证:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 void* my_thread(void* arg){
5 while(1){
6 printf("i am my_thread\n");
7 sleep(1);
8 pthread_cancel(pthread_self());//终止当前线程
9 }
10 }
11 int main(){
12 pthread_t tid;
13 pthread_create(&tid,NULL,my_thread,(void*)NULL);
14 while(1){
15 sleep(1);
16 printf("i am main thread\n");
17 }
18 return 0;
19 }
以上代码中pthread_self()函数的作用是获取当前线程标识符,这个标识符其实是线程控制块的首地址。运行代码:
此时语句"i am my_thread"被打印了两次,按理来说该语句应该被打印出一次后,创建的线程就被终止掉。 其实是因为pthread_cancle()函数在终止线程时有一个过程,不是立即终止。
线程等待
线程被创建出来的默认属性是joinable,退出的时候依赖其他线程回收资源。线程的僵尸状态难以观察,以下并没有观察线程的状态。
pthread_join
用来等待回收其它线程退出状态信息
代码验证:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 void* my_thread(void* arg){
5 sleep(10);
6 printf("%s\n",(char*)arg);
7 pthread_exit((void*)10);
8 }
9 int main(){
10 pthread_t tid;
11 void* ret=NULL;
12 pthread_create(&tid,NULL,my_thread,(void*)("i am my_thread"));
13 pthread_join(tid,&ret);
14 printf("%p\n",ret);
15 return 0;
16 }
编译运行:
创建出来的线程是10秒后才退出的,观察运行结果,主线程等待到了工作线程,这就说明pthread_join是一个阻塞调用接口
线程分离
pthread_detach
该函数用于设置分离属性,一旦线程设置了分离属性,则线程退出的时候,不需要其它线程回收资源,操作系统自动回收
线程安全(上)
线程不安全代码演示
如下代码给定全局变量ticket,用多线程模拟抢票,抢到票后打印线程标识符与票的编号:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <pthread.h>
4 #include <fcntl.h>
5 int ticket=0xeffff;//票数
6 void* my_thread(void* arg){
7 (void)arg;
8 while(ticket>0){
9 printf("i am my_thread:%lu,%d\n",pthread_self(),ticket);//某个线程抢到了票,并打印票的编号
10 ticket--;
11 }
12 return (void*)10;
13 }
14 int main(){
15 void* ret=NULL;
16 int fd=open("1.txt",O_WRONLY|O_CREAT,0664);//以读写方式打开文件1.txt,不存在则创建
17 if(fd<0){
18 perror("open");
19 return 0;
20 }
21 dup2(fd,1);//将标准输出重定向到文件1.txt
22 pthread_t thread[5];//用来获取线程标识符
23 int i=0;
24 for(;i<5;i++){//创建五个工作线程
25 pthread_create(&thread[i],NULL,my_thread,NULL);
26 }
27 for(i=0;i<5;i++){//等待五个工作线程(阻塞)
28 pthread_join(thread[i],&ret);
29 }
30 printf("%d\n",ticket);//最后在打印一下票的数量
31 return 0;
32 }
代码编译运行:
打开1.txt文件,查看抢票情况,直接来到文件的末尾:
观察运行结果,出现了两个奇怪的现象。第一点:明明票的数量都被抢完了,但是其它四个线程却打印出来很大的票的编号,第二点:票的数量最后变成了负值。按照以往的思路,在while循环中条件判断是ticket>0,如果ticket是负数,那么循环根本进不去,也就是说ticket的最小值会是0,那么又该如何解释上述现象呢?
因为当前这台机器是单核CPU的,所以说同一时间拿到CPU资源的只能是某一个线程。以上程序中,以标识符为140378110699264的线程为例,当它拿到CPU资源后,刚进入while循环,还没来得及打印,线程就被切换了,直到最后标识符为140378127484672的线程执行完代码退出后,它才又拿到CPU资源,该线程这时候应该执行打印票数的代码,但是该线程不会从内存中获取ticket的值,它会直接拿出自己的寄存器中保存的值930724进行打印,打印完毕后,才会从内存中获取ticket的值进行减一操作并写回内存,此时ticket的值已经变成了-1,然后执行while循环的判断语句,发现不满足条件,此时线程退出,然后就轮到其他三个线程执行抢票函数,最后将ticket减到了-4,分析过程和上述过程相似。
仅仅是单核CPU,多个线程并发执行,程序的执行就出现了不可预测的结果,如果是多核CPU,多个线程并行执行,那么结果会更加离谱。
互斥
互斥的概念
互斥锁
互斥锁接口

静态初始化
给互斥锁对象直接赋值,进行静态初始化
加锁
1.调用pthread_mutex_lock函数可以进行加锁,该函数具有阻塞特性
2.pthread_mutex_trylock也可以进行加锁,只是该函数没有阻塞的特性。在进行拿锁的时候,如果拿到锁了,正常返回,没有拿到锁,也直接返回。此函数需要搭配循环来使用,否则加锁失败后就会直接访问临界区资源,达不到互斥的目的。
解锁
调用pthread_mutex_unlock可以进行解锁
销毁
动态初始化后,进程结束调用pthread_mutex_destory销毁互斥锁
代码演示
讲了这么多互斥锁有关的接口,那么如何使用呢?
要使用互斥锁,先得定义互斥锁对象,然后初始化,初始化完成后,多个线程访问临界区资源时首先要进行加锁, 防止其它线程同时访问这块资源,当某一个加锁的线程退出时,要进行解锁,如果互斥锁采用动态初始化,最后整个进程退出时要销毁互斥锁。
有了互斥锁后,对以上代码进行改进,加锁处理:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <pthread.h>
4 #include <fcntl.h>
5 int ticket=0xeffff;//票数
6 pthread_mutex_t g_lock;//互斥锁对象
7 void* my_thread(void* arg){
8 pthread_mutex_lock(&g_lock);
9 while(ticket>0){
10 printf("i am my_thread:%lu,%d\n",pthread_self(),ticket);//某个线程抢到了票,并打印票的编号
11 ticket--;
12 }
13 pthread_mutex_unlock(&g_lock);
14 }
15 int main(){
16 pthread_mutex_init(&g_lock,NULL);//初始化互斥锁
17 void* ret=NULL;
18 int fd=open("2.txt",O_WRONLY|O_CREAT,0664);//以读写方式打开文件1.txt,不存在则创建
19 if(fd<0){
20 perror("open");
21 return 0;
22 }
23 dup2(fd,1);//将标准输出重定向到文件1.txt
24 pthread_t thread[5];//用来获取线程标识符
25 int i=0;
26 for(;i<5;i++){//创建五个工作线程
27 pthread_create(&thread[i],NULL,my_thread,NULL);
28 }
29 for(i=0;i<5;i++){//等待五个工作线程(阻塞)
30 pthread_join(thread[i],&ret);
31 }
32 printf("%d\n",ticket);
33 pthread_mutex_destroy(&g_lock);//解锁
34 return 0;
35 }
编译运行产生"2.txt"文件后,直接来到文件的最后,观察有没有出现异常情况:
票数被成功减到了0,没有再出现多个线程拿到同一个数字,以及出现负数的情况。
看似代码好像执行没有,但是再来仔细看一看代码,会发现其实从头到尾只有一个线程在访问临界区资源,因为当某一个线程拿到这把互斥锁后,就会一直在while循环中执行代码,就算在执行的过程中被CPU剥离下去,也不会释放这把锁,直到将ticket减为0。在此过程中,其他线程获取不到这把锁,也就没法访问临界区资源,达不到目的。
加锁处理应该放到while循环里面,这样多个线程才能在每一次while循环开始时去抢占这把锁,达到多个线程都可以访问临界区资源的目的,代码再次改进,只需要改动一部分,:
运行后:
进程此时为什么不退出了?
同一时刻永远只有一个线程能拿到这把锁,在ticket等于0时,无论是哪个进程此时在访问ticket,都会因为break跳出循环,此时该线程就终止了,它将这把锁也就带走了,永远都不会在归还,而pthread_mutex_lock函数又具有阻塞特性,其它线程就一直在等待拿锁,所以就造成了进程不退出,这种情况称为死锁
如何解决?
在线程所有可能退出的地方都进行解锁。上述代码还需要改进:
编译运行,不会出现阻塞的情况,打开"2.txt"文件,查看文件开头与结尾,分别由不同的线程访问临界区资源,且此时ticket的值并没有出现异常情况:
在单核CPU的机器下,观察到上述情况是不容易的,因为在某一个线程占有CPU时,在其时间片范围内,并没有其它线程在执行。也就是说该线程拿到锁执行完代码在将锁归还后,并没有其它线程来抢夺这把锁,下一次这把锁还是会被原有线程拿到。只有一种情况才会使得其它线程拿到这把锁,那就是该线程刚把锁归还,恰好被CPU剥离下去,其它线程就会拿到这把锁。当然,在多核CPU情况下不存在以上问题。