【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;
}



🌟结束语 

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



目录
相关文章
|
2月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
92 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
29天前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1
|
2月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
2月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
41 0
|
2月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
104 0
|
2月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
28 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2