Linux多线程(上)

本文详细介绍了线程的基本概念,包括线程的共享与独有资源,以及线程的创建、终止、等待和分离。重点讨论了线程安全问题,通过示例展示了线程不安全的代码以及如何使用互斥锁实现线程安全。通过实际代码运行,解释了线程控制的重要性以及在多线程环境下可能导致的问题和解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

线程概念

什么是线程

线程的共享与独有

线程独有

线程共享

线程优缺点

优点

缺点

线程控制

线程创建

 线程终止

线程等待

线程分离

线程安全(上)

线程不安全代码演示

互斥

互斥的概念

互斥锁

互斥锁接口

代码演示


线程概念

什么是线程

当我们需要在一个进程中同时运行多个执行流时,我们并不可以开辟多个进程执行我们的操作(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,多个线程并行执行,那么结果会更加离谱。

互斥

互斥的概念

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥锁

互斥锁原理:互斥锁的本质是0/1计数器,计数器的取值只能是0或1。计数器的值为1:表明当前线程可以获取到互斥锁,从而去访问临界资源。计数器的值为0:表明当前线程不可以获取到互斥锁,从而不能访问临界资源
注意:并不是说线程不获取互斥锁不能访问临界区资源,而是程序员要在代码中用同一个互斥锁去约束多个线程。
互斥锁计数器如何保证原子性操作?
如果互斥锁计数器的值从1到0或者从0到1不是原子性的,那么就无法保证互斥。那又如何保证计数器的加1和减1操作是原子性的?直接使用寄存器当中的值和内存中的值进行交换,而交换是一条汇编指令就可以完成的,一开始寄存器中的值为1,而计数器中的值是0,那么此时判断是否加加锁成功就只用判断线程寄存器中的值是否为1就可以了。

互斥锁接口

动态初始化
调用pthread_mutex_init可以动态初始化互斥锁

静态初始化

给互斥锁对象直接赋值,进行静态初始化

加锁

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情况下不存在以上问题。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值