Linux操作系统7- 线程同步与互斥1(POSIX互斥锁的使用详解)

上篇文章:Linux操作系统6- 线程4(POSIX线程的简单封装)-CSDN博客

本篇代码仓库:myLerningCode/l30 · 橘子真甜/Linux操作系统与网络编程学习 - 码云 - 开源中国 (gitee.com)

目录

一. 线程不互斥造成的结果        

二. pthread_mutex_t 互斥锁操作

2.1 互斥锁的定义与初始化

a 动态分配初始化

b 静态分配

2.2  加解锁操作

三. 互斥锁的使用

四. 互斥锁总结

4.1 谁来保护锁?

4.2 加解锁的原子性问题 


一. 线程不互斥造成的结果        

        线程是进程内的执行流,是CPU调度的基本单位,线程之间有共享资源。比如代码区,常量区,数据段(.data和.bss)以及堆区,进程栈区的资源。

        加入一个线程A正要修改某一个全局变量的时候,因为CPU的调度离开了。然后另一个线程B来访问这个变量并且修改,线程A再次访问这个变量就会导致资源错误。

代码举例:

#include <iostream>
#include <memory>

#include <unistd.h>
#include "Thread.hpp"
using namespace std;

int tickets = 1000;

void *get_tickets(void *args)
{
    string name = static_cast<const char *>(args);
    while (true)
    {
        if (tickets > 0)
        {
            usleep(1000); // 模拟抢票
            cout << "线程" << name << "正在获取票,票号为" << tickets-- << endl;
        }
        else
        {
            cout << "无票可抢" << endl;
            break;
        }
    }

    return nullptr;
}

int main()
{
    std::unique_ptr<Thread> t1(new Thread(get_tickets, (void *)"thread 1", 0));
    std::unique_ptr<Thread> t2(new Thread(get_tickets, (void *)"thread 2", 0));
    std::unique_ptr<Thread> t3(new Thread(get_tickets, (void *)"thread 3", 0));
    std::unique_ptr<Thread> t4(new Thread(get_tickets, (void *)"thread 4", 0));

    t1->start();
    t2->start();
    t3->start();
    t4->start();

    t1->join();
    t2->join();
    t3->join();
    t4->join();
    return 0;
}

 运作结果如下:

 可以看到,线程取到了负数的票。

        这是因为--操作在汇编是三条指令

1 从内存读取到cpu中

2 cpu对根据命令执行计算和逻辑操作

3 将结果写回内存中

        如果线程A执行完命令1后就被切换走了,线程B执行了一个完整--操作。然后切换回线程A的时候,线程A根据上下文操作继续执行计算命令。这样就会导致一个数据被多次--了。从而导致数据错误

         保护共享资源最简单的方式就是对临界资源加锁

二. pthread_mutex_t 互斥锁操作

2.1 互斥锁的定义与初始化

a 动态分配初始化

//头文件
#include <pthread.h>

//定义
pthread_mutex_t mutex;

//动态分配
pthread_mutex_t mutex = INITIALIZER

        动态分配一般用于定义全局变量和静态变量,动态分配初始化的互斥锁不需要销毁。

b 静态分配

//头文件
#include <pthread.h>

//定义
pthread_mutex_t mutex;

//初始化函数
pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)

//参数
mutex:输入输出型参数,调用该接口之后对mutex进行初始化
attr:用于控制锁的属性,一般设置为nulpptr

//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *restrict mutex)
//与初始化一样,用于销毁一个锁

注意静态分配的时候:

不要销毁已经加锁的锁

2 不要销毁了锁之后,继续对这个锁加锁

2.2  加解锁操作

        使用pthread_mutex_lock进行加锁 pthread_mutex_unlock进行解锁。

//所需头文件
#include <pthread.h>

//用于对一个已经初始化的锁加锁
pthread_mutex_lock(pthread_mutex_t *mutex);
//所需头文件
#include <pthread.h>

//对加锁的mutex进行解锁
pthread_mutex_unlock(pthread_mutex_t *mutex);

三. 互斥锁的使用

        我们对线程获取票这个操作进行加锁。如果我们定义的锁不是全局锁,需要通过传参的方式将锁传递给线程进行加锁解锁。当然全局锁就不需要了

这里以非全局锁举例

#include <iostream>
#include <vector>
#include <memory>

#include <unistd.h>
#include "Thread.hpp"
#include <bits/unique_ptr.h>
using namespace std;

const int thread_num = 5;
int tickets = 1000;

struct ThreadData
{
    ThreadData(const string &name = string(), pthread_mutex_t *lock = nullptr)
        : _name(name), _lock(lock) {}

    string _name;
    pthread_mutex_t *_lock;
};

// 通过参数的方式传递锁
void *get_tickets(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    string name = td->_name;
    pthread_mutex_t *mtx = td->_lock;
    while (true)
    {
        // 加锁
        pthread_mutex_lock(mtx);
        if (tickets > 0)
        {
            usleep(1000); // 模拟抢票
            cout << "线程" << name << "正在获取票,票号为" << tickets-- << endl;
            // 解锁
            pthread_mutex_unlock(mtx);
        }
        else
        {
            cout << "无票可抢" << endl;
            // 由于break会导致无法解锁,这里需要解锁
            pthread_mutex_unlock(mtx);
            break;
        }
    }
    return nullptr;
}

int main()
{
    // 定义域初始化锁
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, 0);

    vector<Thread> tds;
    for (int i = 0; i < thread_num; i++)
    {
        string name = "thread-";
        name += to_string(i);
        ThreadData *td = new ThreadData(name, &lock);

        tds.push_back(Thread(get_tickets, td, 0));
    }

    for (int i = 0; i < tds.size(); i++)
        tds[i].start();

    for (int i = 0; i < tds.size(); i++)
        tds[i].join();

    // 销毁锁
    pthread_mutex_destroy(&lock);

    return 0;
}

运行结果如下:

        可以看到,通过加锁操作可以保护共享资源的访问 

        加锁解锁之间的区域称为临界区,线程在临界区需要访问的资源称为临界资源。加解锁虽然能保护线程访问资源,但是会加解锁操作和线程竞争锁会导致效率降低

四. 互斥锁总结

4.1 谁来保护锁?

        锁可以保护我们访问共享资源,但是锁本身也是一个共享资源,为什么我们加锁解锁不需要保护呢?因为加锁解锁操作是原子的,只需执行必定执行完

        申请锁成功的线程会继续执行代码,申请锁失败的线程在干什么?此时线程会阻塞等待加锁的线程释放锁。

        就像下图,只有一个线程在临界区中执行

     

4.2 加解锁的原子性问题 

        一个线程申请锁成功后,在临界区中能否被切换走?(可以切换走,并且其他线程仍无法访问临界区,因为我是带着锁走的,锁没有被释放)

        那么这样会不会导致程序的效率过低(其他线程都在阻塞等待)?会导致效率降低,所以在编码的时候尽量保证临界区比较小,将无关的代码放到临界区外部。

        即尽量保证锁的粒度比较小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘子真甜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值