栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

【Linux】线程安全(看这一篇就够了)

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Linux】线程安全(看这一篇就够了)

目录

线程安全

概念:

举例:

代码:

互斥:

什么是互斥:

互斥锁:

互斥锁的接口:

初始化:

加锁

 解锁:

销毁:

同步

样例引入

条件变量:

 条件变量原理:

条件变量的接口:

条件变量的夺命追问:

条件变量代码:


线程安全

概念:

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

举例:

有一个单核CPU,有两个线程A和B,有一个全局变量n初始值为10。两个线程都要执行n++这个代码,由于两个线程是抢占式执行的,那么可能会有以下情况出现:

假设A先拿到CPU资源,将n的值也就是10读到了寄存器当中,此时由于某些原因(CPU调度),线程A被剥离CPU;此时线程A的PCB中的上下文信息保存了寄存器当中的信息,程序计数器记录了程序下一步要执行的指令。

然后由线程B拿到CPU资源,将n的值从内存中读取到寄存器中,通过CPU计算然后写回到内存中。此时内存中的n的值为11。

线程A重新获得CPU资源后,此时线程A并不会从内存中读取A的值,而是通过程序计数器和上下文信息来恢复现场。因此线程A拿到的值依然是10,执行完毕后,此时内存中的值依旧是11。此时程序就出现了二义性。

线程不安全的本质就是对临界区的非原子性访问导致的。

代码:

互斥:

什么是互斥:

互斥是一种控制线程访问时序的手段。

当多个线程同时能够访问到临界资源的时候,有可能会导致程序执行的结果产生二义性,而互斥就是要保证多个线程在访问同一个临界资源,执行临界区代码的时候,控制访问时序。

因为临界区的代码是非原子性的,也就是说线程在执行临界区代码的时候可能被打断,为了保证不让上述情况的发生,也就需要让线程对临界资源的访问是原子性的,即在执行临界区代码的时候是原子性的。

互斥锁:

原理:

互斥锁的本质就是0/1计数器,计数器的取值只能是0或者1。

        值为1:当前线程可以获取到互斥锁,从而访问临界资源。

        值为0:当前线程不可以获取互斥锁, 从而不能访问临界资源。

注意:并不是说线程不获取互斥锁不能访问临界资源,而是程序员需要在代码当中用同一个互斥锁去约束多个线程。

否则线程A加锁访问,线程B访问临界资源之前不加锁,那也约束不了线程B。

互斥锁的计数器如何保证原子性?

计数器当中值的变化是直接使用寄存器当中的值和计数器内存的值进行交换。该交换过程使用一条汇编指令就能实现,因此是原子性的。 

具体过程:

加锁过程(将寄存器中的值设置为0):

情况一:计数器的值为1,说明锁空闲,没有被线程加锁

情况二:计数器的值为0,说明锁忙碌,被其他线程加锁拿走。

解锁过程:直接使用交换的汇编指令将1和计数器内存中的值进行交换。 

互斥锁的接口:

初始化:

动态初始化:

mutex:传递互斥锁对象

attr:互斥锁属性,一般传递为NULL,表示默认属性

返回值:初始化成功返回0,初始化失败设置errno,并将errno返回 

静态初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

#define PTHREAD_MUTEDX_INITIALIZER{{0,0,0,0.......}}结构体的初始化

加锁

int pthread_mutex_lock(pthread_mutex_t* mutex)

mutex:传递互斥锁对象

特性:阻塞加锁接口;拿不到锁就会阻塞等待,直到拿到锁;拿到锁返回0

int pthread_mutex_trylock(pthread_mutex_t* mutex)

mutex:传递互斥锁对象

特性:非阻塞加锁接口;拿到锁了正常返回0 ;拿不到锁也直接返回,设置errno值

注意:由于是非阻塞的,需要搭配循环来使用;否则加锁失败后,我们没有判断直接访问临界资源,就达不到互斥的目的。

int pthread_mutex_timelock(pthread_mutex_t* restrict mutex, const struct timespec* restrict abs_timeout)

带有超时时间的加锁接口;

锁空闲,直接加锁后返回0值;锁忙碌,等待时间范围内,锁被其他线程释放了,就可以获取互斥锁,函数返回0值;超过了等待时间的范围,锁还没有被其他线程释放,函数直接返回errno设置的值,表示加锁失败。 

验证:

假设这两个线程是线程A和线程B,假设线程A先拿到互斥锁,并没有释放互斥锁,而加锁的接口具有阻塞属性,因此应该会有如下现象:

程序不会退出,处于阻塞状态;整个工作都是由一个线程完成的,另一个线程并没有参与其中。

 解锁:

int pthread_mutex_unlock(pthread_mutex_t* mutex)

mutex:要解锁的互斥锁变量

返回值:解锁成功返回0;解锁失败,设置errno,并将errno返回

修改上面的代码:

注意:在线程都有可能退出的地方都进行解锁,否则就有可能导致死锁。 

销毁:

pthread_mutex_destroy(pthread_mutex_t* mutex)

mutex:想要销毁的互斥锁

注意:如果是动态初始化互斥锁,需要调用销毁接口;如果是静态初始化接口,不需要调用销毁接口。

完整代码: 

   2 #include
    3 #include
    4 #include
    5 
    6 int g_t = 10;
    7 pthread_mutex_t g_lock; //互斥锁
    8 
W>  9 void* thread_start(void* arg){
   10     //修改全局变量
   11     while(1){
   12         sleep(1); //让程序结果更加明确
   13         //加锁
   14         pthread_mutex_lock(&g_lock);
   15         if(g_t <= 0){
   16             pthread_mutex_unlock(&g_lock);
   17             break;
   18         }
W> 19         printf("I am %p, i got value is %dn", pthread_self(), g_t);
   20         g_t--;
   21         pthread_mutex_unlock(&g_lock);
   22     }
W> 23 }
   24 int main(){
   25     //初始化互斥锁
   26     pthread_mutex_init(&g_lock,NULL);
   27     //创建两个线程
   28     pthread_t tid[2];
   29     for(int i = 0; i < 2; i++){
   30         int ret = pthread_create(&tid[i], NULL, thread_start, NULL);
   31         if(ret < 0){
   32             perror("pthread_create");
   33             return 0;
   34         }
   35     }
   36     //主线程进行线程等待
   37     for(int i = 0; i < 2; ++i){
   38         pthread_join(tid[i], NULL);
          }
   40     //销毁互斥锁
   41     pthread_mutex_destroy(&g_lock);
   42     return 0;
   43 }     

同步

样例引入

现在有如下场景:

有两个人,A和B,有一个碗,A一直向碗里做面,B一直从碗里吃面。这种情况我们看到的结果应该是A做一碗,B吃一碗。

现在有一个全局的临界资源g_bowl,g_bowl = 1表示有面,g_bowl = 0表示没面。有两个线程A和B,分别表示做面和吃面。

    1 #include
    2 #include
    3 #include                                                                                                                                                                                            
    4 
    5 #define THREAD_COUNT 1
    6 
    7 int g_bowl = 0;  //0没有面,1有面
    8 pthread_mutex_t g_lock;
    9 
W> 10 void* eat_thread(void* arg){
   11     while(1){
   12         pthread_mutex_lock(&g_lock);
   13         printf("I am eat thread, eat %dn", g_bowl--);
   14         pthread_mutex_unlock(&g_lock);
   15         usleep(1);
   16     }
   17 }
W> 18 void* make_thread(void* arg){
   19     while(1){
   20         pthread_mutex_lock(&g_lock);
   21         printf("I am make thread, make %dn", g_bowl++);
   22         pthread_mutex_unlock(&g_lock);
   23         usleep(1);
   24     }
   25 }
   26 int main(){
   27     //初始化互斥锁
   28     pthread_mutex_init(&g_lock, NULL);
   29     pthread_t eat[THREAD_COUNT], make[THREAD_COUNT];
   30     //创建吃面和做面线程
   31     for(int i = 0; i < THREAD_COUNT; ++i){
   32         int ret = pthread_create(&eat[i], NULL, make_thread, NULL);
   33         if(ret < 0){
   34             perror("pthread_creat");
   35             return 0;
   36         }
   37         ret = pthread_create(&make[i], NULL, eat_thread, NULL);
   38         if(ret < 0){
   39             perror("pthread_creat");
   40             return 0;
   41         }
   42     }
   43     //主线程等待工作线程
   44     for(int i = 0; i < THREAD_COUNT; ++i){
   45         pthread_join(eat[i], NULL);
   46         pthread_join(make[i], NULL);
   47     }
   48     //销毁互斥锁
   49     pthread_mutex_destroy(&g_lock);
   50     return 0;
   51 }

 

可以看到现在的程序访问碗这个临界资源是不合理的,因为我们只有一个碗,应该做一碗吃一碗。

结论:

1、多个线程保证了互斥,也就保证了线程能够独占访问临界资源了。但是,并不能保证线程访问临界资源的合理性。

2、同步存在的意义就是保证多个线程对临界资源访问的合理性,这个合理性建立在互斥的基础上。 

应该如何实现同步呢?我们可以通过判断的方式解决。

  

结果貌似符合要求,但是存在问题,将代码改一下:

要解决这个问题,我们就需要了解条件变量,使用条件变量接口来解决。

条件变量:

 条件变量原理:

条件变量本质上是一个PCB等待队列,存放着等待的线程的线程PCB。

线程在加锁之后,先判断临界资源是否可用,如果可用,直接访问临界资源;如果不可用,调用条件变量的等待接口,让线程进行等待。

条件变量的接口:

 1、初始化接口

动态初始化:

int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);

cond:条件变量的指针

attr:表示条件变量的属性信息,一般传递NULL,表示默认属性

返回值:初始化成功返回0,初始化失败设置errno并返回

静态初始化:

pthread_cond_t  cond = PTHREAD_COND_INITIALIZER

2、等待接口(哪个线程调用就将哪个线程放到条件变量对应的PCB等待队列中)

int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex)

cond:条件变量的指针

mutex:互斥锁

 3、唤醒接口

int pthread_cond_broadcast(pthread_cond_t* cond);

唤醒PCB等待队列当中的所有线程。

int pthread_cond_signal(pthread_cond_t* cond);

唤醒PCB等待队列当中的至少一个线程

4、销毁接口

int pthread_cond_destroy(pthread_cond_t* cond);

参数:要销毁的条件变量

返回值:销毁成功,返回0;销毁失败,设置errno并返回。

   1 #include
    2 #include
    3 #include
    4 
    5 #define THREAD_COUNT 1
    6                                                                                                                                                                                                                                                                                                                                                                                    
    7 int g_bowl = 0;  //0没有面,1有面
    8 pthread_mutex_t g_lock; //互斥锁
    9 pthread_cond_t g_cond; //条件变量
   10 
W> 11 void* eat_thread(void* arg){
   12     while(1){
   13         pthread_mutex_lock(&g_lock);
   14         if(g_bowl == 0){
   15             printf("我是吃面人,碗里面没面我就不吃了n");
   16             pthread_cond_wait(&g_cond, &g_lock);
   17             //pthread_mutex_unlock(&g_lock);
   18             //continue;
   19         }
   20         printf("I am eat thread, eat %dn", g_bowl--);
   21         //解锁
   22         pthread_mutex_unlock(&g_lock);
   23         //通知做面线程
   24         pthread_cond_signal(&g_cond);
   25        // usleep(1);
   26     }
   27 }
W> 28 void* make_thread(void* arg){
   29     while(1){
   30         pthread_mutex_lock(&g_lock);
   31         if(g_bowl == 1){
   32             printf("我是做面人,碗里面有面,我就不做了。。。n");
   33             pthread_cond_wait(&g_cond, &g_lock);
   34             //pthread_mutex_unlock(&g_lock);
   35             //continue;
   36         }
   37         printf("I am make thread, make %dn", g_bowl++);
   38         //解锁
   39         pthread_mutex_unlock(&g_lock);
   40         //通知吃面线程
   41         pthread_cond_signal(&g_cond);
   42         //usleep(1);
   43     }
   44 }
   45 int main(){
   46     //初始化互斥锁
   47     pthread_mutex_init(&g_lock, NULL);
   48     //初始化条件变量
   49     pthread_cond_init(&g_cond,  NULL);
   50 
   51     pthread_t eat[THREAD_COUNT], make[THREAD_COUNT];
   52     //创建吃面和做面线程
   53     for(int i = 0; i < THREAD_COUNT; ++i){
   54         int ret = pthread_create(&eat[i], NULL, make_thread, NULL);
   55         if(ret < 0){
   56             perror("pthread_creat");
   57             return 0;
   58         }
   59         ret = pthread_create(&make[i], NULL, eat_thread, NULL);
   60         if(ret < 0){
   61             perror("pthread_creat");
   62             return 0;
   63         }
   64     }
   65     //主线程等待工作线程
   66     for(int i = 0; i < THREAD_COUNT; ++i){
   67         pthread_join(eat[i], NULL);
   68         pthread_join(make[i], NULL);
   69     }
   70     //销毁互斥锁
   71     pthread_mutex_destroy(&g_lock);
   72     //销毁条件变量
   73     pthread_cond_destroy(&g_cond);
   74     return 0;
   75 }

对于多个吃面,结果又是如何呢?

可以看到当多个吃面线程和做面线程的时候,又出现了临界区资源访问不合理的现象。要想弄清楚这里的原由,我们需要先搞清楚pthread_cond_wait这个接口都做了些什么。

条件变量的夺命追问:

1、条件变量的等待接口第二个参数为什么会有互斥锁?

前提:在线程访问临界资源之前,一定是加锁访问的,因为要保证互斥。

将互斥锁传递给pthread_cond_wait接口,目的就是让该接口在内部执行解锁操作。

 为什么要解锁呢?以一个吃面线程和一个做面线程为例:

 假设临界资源g_bowl的初始值为0并且吃面线程先拿到互斥锁,则此时做面线程被阻塞在加锁逻辑中等待加锁。

此时吃面线程判断g_bowl的值为0,因此吃面线程就调用pthread_cond_wait接口,将自己放在PCB等待队列中。

如果说等待接口不将互斥锁进行释放,那么也就意味着做面线程一直拿不到互斥锁,一直被阻塞在加锁接口中。那也就是说,做面线程不会执行到pthread_cond_signal接口去通知吃面线程,那么程序就会被卡死。

实践的时候,并没有出现卡死的情况,所以等待接口一定在内部将互斥锁进行了解锁。

只有在互斥锁被释放的情况下,做面线程才有机会拿到互斥锁去访问临界资源,进而向下执行通知吃面线程。

2、pthread_cond_wait的内部针对互斥锁做了什么操作?先释放互斥锁还是先将线程放到PCB等待队列?

1、pthread_cond_wait接口在内部对互斥锁进行了解锁

2、先将线程放到PCB等待队列,再释放互斥锁。

 假设临界资源g_bowl的初始值为0并且吃面线程先拿到互斥锁,则此时做面线程被阻塞在加锁逻辑中等待加锁。

此时吃面线程判断g_bowl的值为0,因此吃面线程就调用pthread_cond_wait接口,将自己放在PCB等待队列中。现在就存在两种情况:

情况一:先释放互斥锁,再将吃面线程放到PCB等待队列中

吃面线程还没有放到PCB等待队列中,做面线程就已经将互斥锁抢到,并且已经去通知了PCB等待队列中的吃面线程。但是此时PCB等待队列中什么都没有,而做面线程在通知完之后,它自己又循环到最初的加锁代码处。而后吃面线程才进入到等待队列中。此时并没有任何一个线程去抢互斥锁,也就是说做面线程会再一次拿到互斥锁访问临界资源,发现碗里什么也没有,做面线程也就调用pthread_cond_wait接口。最终造成的结果就是两个工作线程全都放在PCB等待队列中,永远没有线程去唤醒他们。因此,这种情况是不可取的。

情况二:先将吃面线程放到PCB等待队列中,再释放互斥锁。此时做面线程拿到互斥锁后访问临界资源,然后解锁,通知PCB等待队列,吃面线程被唤醒。整个程序可以正常推进。因此,这种情况可取。

3、线程被唤醒之后会执行什么代码?

pthread_cond_wait函数在唤醒之后一定会在其内部进行加锁操作。当然加锁的权限和其他不在PCB等待队列中的线程是一样的,也就是说,它并不一定能够抢到锁。

1、抢到锁了:pthread_cond_wait函数就真正执行完毕了,函数返回。

2、没有抢到锁:

pthread_cond_wait函数没有真正的执行完毕,还处于内部抢锁的逻辑当中,还会继续抢锁,直到抢到互斥锁后才返回。 

条件变量代码:

 现在来分析为什么吃面线程和做面线程从1变为2后,程序就出错了:

假设有两个吃面线程A和B,两个做面线程C和D

下面的场景是有可能存在的:

假设吃面线程A先拿到互斥锁发现g_bowl的值为0,然后把自己放到了PCB等待队列中并释放互斥锁,假设此时吃面线程B又一次拿到了互斥锁,则它也会把自己放到PCB等待队列中。此时PCB等待队列中有两个线程:吃面线程A和吃面线程B。

此时做面线程C拿到互斥锁,将g_bowl的值增加为1,并通过唤醒接口去唤醒PCB等待队列中的至少一个线程。

假设将PCB等待队列中的两个线程都唤醒了。此时线程A和B会进行抢占式加锁。假设线程A加锁成功,此时将g_bowl变为0。假设接下来线程B抢到了互斥锁,那么线程B将g_bowl变为了-1。

所以出错的原因为:在pthread_cond_wait接口返回的时候,并没有判断临界资源时候可用就直接去访问了,可能会导致临界区代码访问的不合理性。

 

线程能够从PCB等待队列中被唤醒,就表明临界资源一定是可用的,为什么还要判断?

原因是在唤醒线程的时候,有可能唤醒多个线程。而临界资源可能会被唤醒的其中某一个线程使用了,然后其他线程去访问的时候,就有可能导致临界资源访问的不合理性。

解决方法:只需要将线程入口函数处的条件判断改为while,在等待接口退出的时候就会循环上去判断临界资源是否可用,进而保证临界资源访问的合理性:

用pstack查看以下线程的状态:

 

可以看到工作线程都处在了PCB等待队列中。

原因是:在线程通知PCB等待队列中的线程的时候,将同种类的线程通知出来了,然后判断临界资源是不可用状态,因此刚被通知出来的线程什么也没做,就又进入PCB等待队列中了。

如何解决:

只需要将吃面线程和做面线程分开就行。这样吃面线程通知的永远是做面线程,做面线程统治的永远是吃面线程。

代码:

    1 #include                                                                                                                                                                                                                                                                                                                                                                  
    2 #include
    3 #include
    4 
    5 #define THREAD_COUNT 2
    6 
    7 int g_bowl = 0;  //0没有面,1有面
    8 pthread_mutex_t g_lock; //互斥锁
    9 pthread_cond_t g_eat_cond; //条件变量
   10 pthread_cond_t g_make_cond;
   11 
W> 12 void* eat_thread(void* arg){
   13     while(1){
   14         pthread_mutex_lock(&g_lock);
   15         while(g_bowl == 0){
   16             printf("我是吃面人,碗里面没面我就不吃了n");
   17             pthread_cond_wait(&g_eat_cond, &g_lock);
   18             //pthread_mutex_unlock(&g_lock);
   19             //continue;
   20         }
   21         printf("I am eat thread, eat %dn", g_bowl--);
   22         //解锁
   23         pthread_mutex_unlock(&g_lock);
   24         //通知做面线程
   25         pthread_cond_signal(&g_make_cond);
   26        // usleep(1);
   27     }
   28 }
W> 29 void* make_thread(void* arg){
   30     while(1){
   31         pthread_mutex_lock(&g_lock);
   32         while(g_bowl == 1){
   33             printf("我是做面人,碗里面有面,我就不做了。。。n");
   34             pthread_cond_wait(&g_make_cond, &g_lock);
   35             //pthread_mutex_unlock(&g_lock);
   36             //continue;
   37         }
   38         printf("I am make thread, make %dn", g_bowl++);
   39         //解锁
   40         pthread_mutex_unlock(&g_lock);
   41         //通知吃面线程
   42         pthread_cond_signal(&g_eat_cond);
   43         //usleep(1);
   44     }
   45 }
   46 int main(){
   47     //初始化互斥锁
   48     pthread_mutex_init(&g_lock, NULL);
   49     //初始化条件变量
   50     pthread_cond_init(&g_eat_cond,  NULL);
   51     pthread_cond_init(&g_make_cond, NULL);
   52 
   53     pthread_t eat[THREAD_COUNT], make[THREAD_COUNT];
   54     //创建吃面和做面线程
   55     for(int i = 0; i < THREAD_COUNT; ++i){
   56         int ret = pthread_create(&eat[i], NULL, make_thread, NULL);
   57         if(ret < 0){
   58             perror("pthread_creat");
   59             return 0;
   60         }
   61         ret = pthread_create(&make[i], NULL, eat_thread, NULL);
   62         if(ret < 0){
   63             perror("pthread_creat");
   64             return 0;
   65         }
   66     }
   67     //主线程等待工作线程
   68     for(int i = 0; i < THREAD_COUNT; ++i){
   69         pthread_join(eat[i], NULL);
   70         pthread_join(make[i], NULL);
   71     }
   72     //销毁互斥锁
   73     pthread_mutex_destroy(&g_lock);
   74     //销毁条件变量
   75     pthread_cond_destroy(&g_eat_cond);
   76     pthread_cond_destroy(&g_make_cond);
   77     return 0;
                     

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/866258.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号