【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)

简介: 【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)

【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)    https://developer.aliyun.com/article/1565759



🌙 Linux线程互斥


💫 互斥相关概念

临界资源:

多个执行流进行安全访问的共享资源就叫临界资源

临界区:

多个执行流进行访问临界资源的代码就是临界区

互斥:

任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

原子性:

不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完,这就是原子性。

理解原子性:

一个资源进行的操作如果只用一条汇编语句就能完成,就是原子性的,反之不是原子的。对变量++或者–。在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:


  1. 从内存读取数据到CPU寄存器中
  2. 在寄存器中让CPU进行对应的算逻运算
  3. 写回新的结果到内存中变量的位置


对一个资源访问的时候,要么不做,要么做完,不是原子性的情况:线程A被切换,没做完,有中间状态,不是原子性。实际上对变量做–的时候,对应三条汇编语句,未来会对应三条汇编语句!所以很明显,++、–不是原子性的,不是一条语句。


单纯的++或者++都不是原子的,有可能会有数据一致性的问题。


💫 互斥量mutex

概念:

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互多个线程并发的操作共享变量,会带来问题:数据不一致问题。


要解决线程不安全的情况,保护共享资源:


  • 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
  • 实际上就是需要一把锁,Linux提供的这把锁就叫互斥量,如果一个线程持有锁,那么其他的线程就无法进来访问了。


常见的相关接口:

#include <pthread.h>
// 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 全局
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//成功返回0,失败返回错误码


即可以定义成局部的,也可以定义成全局的。pthread_mutex_t是锁的类型,如果我们定义的锁是全局的,就不要用pthread_mutex_int和pthread_mutex_destroy初始化和销毁了。

#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
 
//如果加锁成功,直接持有锁,加锁不成功,此时立马出错返回(试着加锁,非阻塞获取方式)
int pthread_mutex_trylock(pthread_mutex_t *mutex);
 
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 成功返回0,失败返回错误码


💫 mutex的使用

全局锁的使用:

使用全局锁+4个线程的代码,定义全局锁并初始化PTHREAD_MUTEX_INITIALIZER,同时用pthread_create创建4个线程进行测试,由于此时锁是全局的,我们不需要把锁传给每个线程:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* get_ticket(void* args)
{
    std::string username = static_cast<const char *>(args);
    while(true)
    {
        pthread_mutex_lock(&lock);
        if(tickets>0)
        {
            usleep(11111);
            cout<<username<<"正在抢票 : "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr,  get_ticket, (void *)"thread 1");
    pthread_create(&t2, nullptr,  get_ticket, (void *)"thread 2");
    pthread_create(&t3, nullptr,  get_ticket, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");
 
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}



局部锁的使用:

局部锁+for循环创建线程的代码,此时的锁是局部的,为了把锁传递给每个线程,我们可以定义一个结构体ThreadData,存放着线程名与锁:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 1000;
class ThreadData
{
public:
    ThreadData(const std::string&threadname,pthread_mutex_t *mutex_p)
        :threadname_(threadname),mutex_p_(mutex_p)
    {}
    ~ThreadData(){}
public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};
void* get_ticket(void* args)
{
    ThreadData*td = static_cast<ThreadData *>(args);
    while(true)
    {
        pthread_mutex_lock(td->mutex_p_);
        if(tickets>0)
        {
            usleep(11111);
            cout<<td->threadname_<<"正在抢票 : "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(td->mutex_p_);
        }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);
            break;//注意这里有break
        }
    }
    return nullptr;
}
int main()
{
 #define NUM 4
     pthread_mutex_t lock;
     pthread_mutex_init(&lock,nullptr);
     std::vector<pthread_t> tids(NUM);
     for(int i =0;i<NUM;i++)
     {
         char buffer[64];
         snprintf(buffer,sizeof(buffer),"thread %d",i+1);
         ThreadData *td = new ThreadData(buffer,&lock);
         pthread_create(&tids[i],nullptr,get_ticket,td);
     }
     for(const auto&tid:tids)
     {
        pthread_join(tid,nullptr);
     }
    pthread_mutex_destroy(&lock);
    return 0;
}



总结分析:

此时的运行结果每次都是能够减到1,但是运行的速度也变慢了。这是因为加锁和加锁的过程是多个线程串行执行的,程序变慢了,同时这里看到每次都是只有一个线程在抢票,这是因为锁只规定互斥访问,并没有规定谁来优先执行所以谁的竞争力强就谁来持有锁。要想解决这个问题:想想抢完票就结束了吗?实际的生活当中,抢完票后还有一些工作需要完成:比如发送订单


💫 Mutex.hpp——mutex的封装

如果我们想简单的使用,该如何进行封装设计 ——做一个简单设计,传入一个锁自动帮我们加锁和解锁,RAII风格加锁:

Mutex.hpp:

//Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p=nullptr):lock_p_(lock_p)
    {}
    
    void lock()
    {
        if(lock_p_) pthread_mutex_lock(lock_p_);
    }
 
    void unlock()
    {
        if(lock_p_) pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {
 
    }
private:
    pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
    {
        mutex_.lock();//在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }
 
private:
    Mutex mutex_;
};


main.cc:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
 
using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *get_ticket(void *args)
{
    std::string username = static_cast<const char *>(args);
    while (true)
    {
        {//代码块,不给usleep加锁
            LockGuard lockguard(&lock);
            if (tickets > 0)
            {
                usleep(1111);
                cout << username << "正在抢票 : " << tickets << endl;
                tickets--;
            }
            else
            {
                break;
            }
        }
        usleep(1000);
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, get_ticket, (void *)"thread 1");
    pthread_create(&t2, nullptr, get_ticket, (void *)"thread 2");
    pthread_create(&t3, nullptr, get_ticket, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");
 
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}



💫 可重入函数&线程安全

线程安全:

  • 在多个执行流中对同一个临界资源进行操作访问, 而不会造成数据二义性 .
  • 也就是说, 在拥有共享数据的多条线程并行执行的程序中, 通过同步和互斥保证各个线程都可以正常且正确的执行, 不会出现数据污染等意外情况, 也就是保证了线程安全 .


重入 :

同一个函数被不同的执行流调用, 当前一个流程还没有执行完, 就有其他的执行流再次进入, 我们称之为重入. 一个函数在重入的情况下, 运行结果不会出现任何不同或者任何问题, 则该函数被称为可重入函数, 否则, 是不可重入函数.

  • 调用了 malloc / free 函数, 因为malloc函数是用全局链表来管理堆的.
  • 调用了标准I/O库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构.
  • 可重入函数体内使用了静态的数据结构.
  • 函数是可重入的, 那就是线程安全的 .
  • 线程安全不一定是可重入的, 而可重入函数则一定是线程安全的 .  


💫 死锁

概念:

多个执行流在对多个锁资源进程争抢操作时, 因为推进顺序不当, 而导致互相等待, 流程无法继续推进的情况.


产生死锁的四个必要条件(要产生死锁, 下面条件缺一不可),死锁这部分内容中所说的执行流可以是进程也可以是线程


  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时, 对已获得的资源保持不放(一个执行流拿着A锁请求B锁, 没有请求到B锁,阻塞等待时并没有释放A锁, 属于占着xx不拉x)
  3. 不可剥夺条件: 一个执行流已获得的资源, 在末使用完之前, 不能强行剥夺(一个执行流加的锁, 其他的执行流不能解锁)
  4. 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系避免 (一个执行流拿着A锁去请求B锁, 另一个执行流拿着B锁请求A锁, 谁也拿不到谁的.)


预防死锁(从三个必要条件入手)


  • 加锁顺序一致 -- 破坏循环等待条件(当多个执行流的加锁顺序一致时, 就不会产生上面所说的循环等待条件了)
  • 避免锁未释放的场景 -- 破坏了请求与保持条件(如果一个执行流拿着A锁, 请求B锁失败,  则该执行流在阻塞等待前必需将所有已经拿到的锁全部解锁)
  • 资源一次性分配 -- 破坏不可剥夺条件(创建执行流时, 要求它申请所需的全部资源, 要么全都获取到, 要么一个也不给)


避免死锁的方法:


银行家算法(有效的避免死锁), 死锁检测算法(检测出死锁后解除死锁)


🌙Linux线程同步

概念引入:


引入一些情景:自习室VIP,先到先得,上厕所时反锁,别人进不去,离资源近竞争力强,一直是你自己,重复放钥匙拿钥匙,造成其他人饥饿状态;再比如抢票系统我们看到一个线程一直连续抢票,造成了其他线程的饥饿,为了解决这个问题:我们在数据安全的情况下让这些线程按照一定的顺序进行访问,这就是线程同步。


  • 饥饿状态:得不到锁资源而无法访问公共资源的线程处于饥饿状态。但是并没有错,但是不合理。
  • 竞态条件:因为时序问题,而导致程序异常,我们称为竞态条件。
  • 线程同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。


💫 条件变量

概念:


当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了,例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中。这种情况就需要用到条件变量。


条件变量通常需要配合互斥锁一起使用。


条件变量的使用:


一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立后唤醒等待的线程。


💫 条件变量接口

条件变量类型:


pthread_cond_t 类型, 是一个结构体。


初始化:


  • 静态初始化 : pthread_cond_t  cont  = PTHREAD_COND_INITIALIZER ; //不需要销毁
  • 动态初始化 : pthread_cond_init( pthread_cond_t* restrict  cond, const pthread_condattr_t*  restrict  attr) //需要释放销毁(因为在堆上, 用完后需要用函数pthread_mutex_destroy来销毁)
  • 功能 : 初始化条件变量


参数 :

  1. cond : 在这个条件变量上等待
  2. attr : 条件的属性, 通常传入NULL,传入NULL为默认属性
  3. restrict关键字 : 只用于限制指针, 告诉编译器, 所有修改该指针指向内存中内容的操作, 只能通过本指针完成. 不能通过除本指针以外的其他变量或指针修改.


  • 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno


等待:


  • 原型 : pthread_cond_wait(pthread_cond_t* restrict  cond,  pthread_mutex_t*  restrict  mutex)
  • 功能 : 等待条件满足,完成了三步操作: 解锁 -> 等待(加入等待的PCB队列) -> 被唤醒后重新加锁. (其中解锁和休眠是一个原子操作)其中, 解锁是为了让资源可以被别的执行流访问(其他执行流可能会产生可用资源), 当可用资源产生后, 再唤醒这个执 行流, 唤醒之后需要保证操作的原子性, 又要加锁.
  • 参数 :


  1. cond : 在这个条件变量上等待
  2. mutex : 给判断条件加的互斥锁


  • 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno


唤醒:


  • 原型 : pthread_cond_signal(pthread_cond_t* restrict cond)
  • 功能 : 唤醒至少一个条件变量等待队列中的执行流(可能唤醒一个, 也可能是多个)
  • 参数 :cond : 在这个条件变量上等待
  • 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno

  • 原型 : pthread_cond_broadcast(pthread_cond_t* restrict cond)
  • 功能 : 广播唤醒所有条件变量等待队列中的执行流
  • 参数 :cond : 在这个条件变量上等待
  • 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno


销毁:


  • 原型 : pthread_cond_destroy(pthread_cond_t* cond)
  • 功能 : 在条件变量不再使用之后, 销毁释放资源(只针对init函数初始化的条件变量)
  • 参数 : 要销毁的条件变量的地址
  • 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno


💫 理解条件变量

举个例子:公司进行招聘:应聘者要面试,大家不能同时进入房间进行面试,但是没有由于没有组织,上一个人面试完之后,面试官打开门准备面试下一个,一群人在外面等待面试,但是有人抢不过别人,人太多了,面试官记不住谁面试过了,所以有可能一个人面试完之后又去面试了,造成其他人饥饿问题,这时候效率很低,后来hr重新进行管理:设立一个等待区,先排队去等待区进行等待面试,现在每个人都进行排队,都有机会面试了,而这个等待区就是条件变量,如果一个人想面试,先得去排队等待区等待,未来所有应聘者都要去条件变量。


💫 条件变量的使用

一次唤醒一个线程:(创建2个线程,通过条件变量一秒唤醒一个线程(或者全部唤醒)

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        //判断省略
        cout<< name <<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 1");
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 2");
 
    while(true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        cout<<"main thread wakeup one thread..."<<endl;
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
 
    return 0;
}



一次唤醒一大批线程:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        //判断省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_t t[5];
    for(int i = 0;i<5;i++)
    {
        char*name = new char[64];
        snprintf(name,64,"thread %d",i+1);
        pthread_create(t+i,nullptr,start_routine,name);
    }
    while(true)
    {
        sleep(1);
        pthread_cond_broadcast(&cond);
        cout<<"main thread wakeup one thread..."<<endl;
    }
    for(int i = 0;i<5;i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}



🌟结束语 

      今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。



目录
相关文章
|
15天前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
34 0
|
9天前
|
Java 调度 开发者
Java并发编程:解锁多线程同步的奥秘
在Java的世界里,并发编程是提升应用性能的关键所在。本文将深入浅出地探讨Java中的并发工具和同步机制,带领读者从基础到进阶,逐步掌握多线程编程的核心技巧。通过实例演示,我们将一起探索如何在多线程环境下保持数据的一致性,以及如何有效利用线程池来管理资源。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你对Java并发编程有更深入的理解和应用。
|
20天前
|
安全 Java 程序员
Java 并发编程:解锁多线程同步的奥秘
【7月更文挑战第30天】在Java的世界里,并发编程是一块充满挑战的领域。它如同一位严苛的导师,要求我们深入理解其运作机制,才能驾驭多线程的力量。本文将带你探索Java并发编程的核心概念,包括线程同步与通信、锁机制、以及并发集合的使用。我们将通过实例代码,揭示如何在多线程环境中保持数据的一致性和完整性,确保你的应用程序既高效又稳定。准备好了吗?让我们一同踏上这段解锁Java并发之谜的旅程。
26 5
|
20天前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
22天前
|
安全 Java API
Java并发编程的艺术:解锁多线程同步与协作的秘密
【7月更文挑战第28天】在Java的世界中,并发编程如同一场精心编排的交响乐,每一个线程都是乐团中的乐手,而同步机制则是那指挥棒,确保旋律的和谐与统一。本文将深入探讨Java并发编程的核心概念,包括线程的创建、同步机制、以及线程间的通信方式,旨在帮助读者解锁Java多线程编程的秘密,提升程序的性能和响应性。
28 3
|
4天前
|
Java UED
基于SpringBoot自定义线程池实现多线程执行方法,以及多线程之间的协调和同步
这篇文章介绍了在SpringBoot项目中如何自定义线程池来实现多线程执行方法,并探讨了多线程之间的协调和同步问题,提供了相关的示例代码。
23 0
|
4天前
|
NoSQL Redis
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
|
22天前
|
存储 缓存 算法
同时使用线程本地变量以及对象缓存的问题
【7月更文挑战第15天】同时使用线程本地变量和对象缓存需小心处理以避免数据不一致、竞争条件及内存泄漏等问题。线程本地变量使各线程拥有独立存储,但若与对象缓存关联,可能导致多线程环境下访问旧数据。缺乏同步机制时,多线程并发修改缓存中的共享对象还会引起数据混乱。此外,若线程结束时未释放对象引用,可能导致内存泄漏。例如,在Web服务器场景下,若一更新缓存而另一线程仍获取旧数据,则可能返回错误信息;在图像处理应用中,若多线程无序修改算法对象则可能产生错误处理结果。因此,需确保数据一致性、避免竞争条件并妥善管理内存。
|
1天前
|
Java
多线程线程同步
多线程的锁有几种方式
|
8天前
|
调度 Python