线程安全与互斥锁(访问控制)

文章探讨了线程安全问题,由于多个线程共享资源可能导致的不一致性。通过抢票的示例说明了临界区的概念,并展示了如何通过互斥锁解决线程安全问题。同时,文章提到了死锁现象,介绍了死锁的四个必要条件,并提出了避免死锁的方法。
摘要由CSDN通过智能技术生成

线程安全问题

因为多个线程是共享地址空间的,也就是很多资源都是共享的。

  • 优点:通信方便
  • 缺点:缺乏访问控制

因为一个线程的操作问题,给其他线程造成了不可控,或者引起崩溃,异常,逻辑不正确等这种现象:线程安全。

image-20230530225440030

创建一个函数没有线程安全问题的话,尽量不要使用全局,stl,malloc,new等会在全局内有效的数据。

使用的话,需要访问控制

线程有自己的独立栈结构。
线程崩溃的影响一定是有限的,因为线程在进程内部,而进程具有独立性。

访问控制之一 —— 互斥

image-20230531165606851

由于线程安全问题,需要引入访问控制:互斥、同步。

几个概念。 临界资源、临界区、互斥、原子性、同步

image-20230531171641321

通过下面这个抢票的例子,发现线程安全问题

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;

//抢票逻辑,1000张票,5线程同时抢

int tickes = 1000;

void *ThreadRoutine(void *args){
    // string tname = (char*)args;
    int id = *(int*)args;
    delete (int*)args;

    while(true){
        //临界区
        if(tickes > 0){
            //抢票
            usleep(10000); //usleep 微秒  1s = 1000ms 1ms = 1000us
            cout << "我是[" << id << "]我要抢的票是:" << tickes << endl;
            tickes--;
        }
        else{
            //没有票
            break;
        }
        // cout << tname << "is running ... " << endl;
        // sleep(1);
    }

}
int main(){
    pthread_t tid[5];
    for(int i = 0; i < 5; i++){
        int *id = new int(i); 
        pthread_create(tid+i, nullptr, ThreadRoutine, id); //i值可能会被主线程修改,所以此处用在堆区新建的id
        // pthread_create(tid+i, nullptr, ThreadRun, (void *)"thread 1");
    }

    for(int i = 0; i < 5; i++)
        pthread_join(tid[i], nullptr); //等待

    return 0;
}

image-20230531190342903

tickets--在此处并不安全

image-20230531202114714

因此,需要对临界区进行加锁。

mutex 互斥锁

image-20230531203848844

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;

//抢票逻辑,1000张票,5线程同时抢
//对临界区进行加锁
class Ticket{
private:
    int tickets;
    pthread_mutex_t mtx;
public:
    Ticket():tickets(1000){
        pthread_mutex_init(&mtx, nullptr);
    }

    bool GetTicket(){
        pthread_mutex_lock(&mtx);
        //执行这部分代码的执行流就是互斥的,串行执行的
        if(tickets > 0){
            //抢票
            usleep(1000); //usleep 微秒  1s = 1000ms 1ms = 1000us
            cout << "我是[" << pthread_self() << "]我要抢的票是:" << tickets << endl;
            tickets--; 
        }
        else{
            cout << "票已经被抢空了" << endl;
            return false;
        }
        pthread_mutex_unlock(&mtx);
        return true;
    }
    ~Ticket(){
        pthread_mutex_destroy(&mtx);
    }
};


void *ThreadRoutine(void *args){
    // string tname = (char*)args;
    // int id = *(int*)args;
    // delete (int*)args;

    Ticket *t = (Ticket*)args;

    while(true){
        if(!t->GetTicket()) break; //抢票失败,退出
    }

}
int main(){
    Ticket *t = new Ticket();

    pthread_t tid[5];
    for(int i = 0; i < 5; i++){
        // int *id = new int(i); 
        pthread_create(tid+i, nullptr, ThreadRoutine, (void*)t); 
        // pthread_create(tid+i, nullptr, ThreadRun, (void *)"thread 1");
    }

    for(int i = 0; i < 5; i++)
        pthread_join(tid[i], nullptr); //等待

    return 0;
}

运行结果:

image-20230531211212953

因为使用了互斥锁,线程之间不会造成访问干扰和重入问题。

除了使用原生线程库里的锁,也可使用C++提供的库内的锁,包含在头文件#include <mutex>内,修改代码如下

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <mutex>
using namespace std;

//抢票逻辑,1000张票,5线程同时抢
//对临界区进行加锁
class Ticket{
private:
    int tickets;
    pthread_mutex_t mtx;  //原生线程库,系统级别
    mutex mytex;  //C++语言级别
public:
    Ticket():tickets(1000){
        pthread_mutex_init(&mtx, nullptr);
    }

    bool GetTicket(){
        // pthread_mutex_lock(&mtx);
        mytex.lock();
        //执行这部分代码的执行流就是互斥的,串行执行的
        if(tickets > 0){
            //抢票
            usleep(1000); //usleep 微秒  1s = 1000ms 1ms = 1000us
            cout << "我是[" << pthread_self() << "]我要抢的票是:" << tickets << endl;
            tickets--; 
        }
        else{
            cout << "票已经被抢空了" << endl;
            return false;
        }
        // pthread_mutex_unlock(&mtx);
        mytex.unlock();
        return true;
    }
    ~Ticket(){
        pthread_mutex_destroy(&mtx);
    }
};


void *ThreadRoutine(void *args){
    // string tname = (char*)args;
    // int id = *(int*)args;
    // delete (int*)args;

    Ticket *t = (Ticket*)args;

    while(true){
        if(!t->GetTicket()) break; //抢票失败,退出
    }

}
int main(){
    Ticket *t = new Ticket();

    pthread_t tid[5];
    for(int i = 0; i < 5; i++){
        // int *id = new int(i); 
        pthread_create(tid+i, nullptr, ThreadRoutine, (void*)t); //i值可能会被主线程修改,所以此处用在堆区新建的id
        // pthread_create(tid+i, nullptr, ThreadRun, (void *)"thread 1");
    }

    for(int i = 0; i < 5; i++)
        pthread_join(tid[i], nullptr); //等待

    return 0;
}

运行效果与上述相同。

也可定义静态锁,如下,静态锁的使用。

class Ticket{
private:
    int tickets;
    // pthread_mutex_t mtx;  //原生线程库
    // mutex mytex;  //C++语言级别
public:
    Ticket():tickets(1000){
        // pthread_mutex_init(&mtx, nullptr);
    }

    bool GetTicket(){
        static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定义一把静态锁
        //锁本身也是临界资源,如何保证其是安全的?
        //lock、unlock是原子性的
        //一行汇编,即为原子
        pthread_mutex_lock(&mtx);
        // mytex.lock();
        //执行这部分代码的执行流就是互斥的,串行执行的
        if(tickets > 0){
            //抢票
            usleep(1000); //usleep 微秒  1s = 1000ms 1ms = 1000us
            cout << "我是[" << pthread_self() << "]我要抢的票是:" << tickets << endl;
            tickets--; 
        }
        else{
            cout << "票已经被抢空了" << endl;
            return false;
        }
        pthread_mutex_unlock(&mtx);
        // mytex.unlock();
        return true;
    }
    ~Ticket(){
        // pthread_mutex_destroy(&mtx);
    }
};

访问临界资源的时候,需要先访问mtx,前提是所有线程必须得看到它。所以,锁本身也是临界资源。lockunlock原子的,故而,保证了锁本身是安全的。

通过接下来的内容了解互斥锁(互斥量)的原理。

互斥锁实现原理探究

一行汇编,即是原子的。

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

下面是lock以及unlock底层实现的伪代码:

image-20230531223914986

死锁
  • 一个线程造成死锁:在一个加锁程序中,又进行一次加锁操作,并且这个锁不是可重入锁,就会导致死锁。内部锁在等外部锁释放,外部锁需要走完整个流程才能释放锁。

  • 多个线程造成死锁:一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁四个必要条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁算法

  • 死锁检测算法
  • 银行家算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值