漫话linux:线程锁

#哪个编程工具让你的工作效率翻倍?#

1.锁的接口:

        1.锁也是一个数据类型,它的类型是pthread_mutex_t

        2.初始化:

                1.静态分配:phread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER(不需要销毁)

                2.动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)

                        参数分析:

                                1.mutex:需要初始化的互斥量

                                2.attr:初始化的互斥量的属性,一般设置为nullptr

                        返回值:成功返回0,失败返回错误码

2.锁的五个基本属性

加锁的时候要注意锁的粒度,越小越好

//定义
 pthread_mutex_t mtx;
//初始化
pthread_mutex_init(&mtx, nullptr);
//销毁
pthread_mutex_destroy(&mtx);
//给线程上锁
 int n = pthread_mutex_lock(td->_pmtx);
//解锁
 n = pthread_mutex_unlock(td->_pmtx);

 示例场景(参考上一篇博客)

#define THREAD_NUM 5
 
int tickets = 10000;
 
class ThreadData
{
public:
    ThreadData(const string &name, pthread_mutex_t *pm)
    : _name(name)
    , _pmtx(pm)
    {}
public:
    string _name;
    pthread_mutex_t *_pmtx;
};
 
void *GetTickets(void *args)
{
    ThreadData *td = (ThreadData *)args;
    while(true)
    {
        int n = pthread_mutex_lock(td->_pmtx);
        assert(n == 0);
 
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s:%d\n", td->_name.c_str(), tickets);
            tickets--;
            n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);
        }
        else
        {
            n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);
            break;
        }
    }
 
    delete td;
    return nullptr;
}
 
int main()
{
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);
 
    // 多线程抢票逻辑
    pthread_t t[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++)
    {
        string name = "thread";
        name += to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mtx);
        pthread_create(t + i, nullptr, GetTickets, (void *)td);
    }
 
    for (int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(t[i], nullptr);
    }
 
    pthread_mutex_destroy(&mtx);
    return 0;
}

加锁的部分:

pthread_mutex_t mtx;  
pthread_mutex_init(&mtx, nullptr);

这是锁的初始化

ThreadData *td = new ThreadData(name, &mtx);

这是结构体带锁的初始化

int n = pthread_mutex_lock(td->_pmtx);  
assert(n == 0);

 申请锁成功了,才能往后执行,不成功,阻塞等待

n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);

在离开临界区的时候线程要释放锁,以便被其他线程使用


    pthread_mutex_destroy(&mtx);

最后是主线程释放锁

加锁的本质:时间换安全

加锁的表现:线程对于临界区代码串执行

加锁的原则:尽量减小临界区代码

临界区:加锁和解锁之间的代码

3.锁的问题:

        1.线程对于锁的竞争力可能不同,刚执行完离锁近容易再次拿到锁(那个线程一直在拿)

        2.拿到锁了要做其他动作

        3.纯互斥时如果锁分配不合理可能会导致问题1

解决方法依照两点原则:所有线程都需排队,从临界区出来的线程都要重新排队

按照一定顺序获取资源:同步(锁本身就是共享资源,所以申请和释放是原子的)

临界区中,线程可以切换,在线程被切出去的时候,是持有锁被切走的。我不在期间,照样没有人能进入临界区访问临界资源,对于其他线程来讲,一个线程要么么有锁,要么释放锁,当前线程访问临界区的过程,对于其他线程是原子的

4.锁的原理

为了

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

交换的本质:把内存中的数据(共享),交换到 CPU 的寄存器中,其实是换到 CPU 此时执行的线程的硬件上下文中,数字 1 (锁)只有一个,随着上下文走了

把一个共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中--当前线程持有锁了

5.锁的封装:降低耦合度

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock):lock_(lock)
    {}
    void Lock()
    {
        pthread_mutex_lock(lock_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(lock_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *lock_;
};

封装:利用初始化来对锁进行启动

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock):mutex_(lock)
    {
        mutex_.Lock();
    }
    ~LockGuard()
    {
        mutex_.Unlock();
    }
private:
    Mutex mutex_;
};

调用

#include "LockGuard.hpp"
while (true)
    {
        {
            LockGuard lockguard(&lock); // 临时的LockGuard对象, RAII风格的锁!
            if (tickets > 0)
            {
                usleep(1000);
                printf("who=%s, get a ticket: %d\n", name, tickets); // ?
                tickets--;
            }
            else
                break;
        }
        usleep(13); // 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
    }

while 之后会自动解锁,利用了对象的生命周期

6.可重入与线程安全

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

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

常见的线程不安全现象:

1.不保护共享变量的函数

2.函数状态随着被调用,状态发生变化的函数

3.返回指向静态变量指针的函数

4.调用线程不安全函数的函数

常见的线程安全现象:

1.每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般 来说这些线程是安全的

2.类或者接口对于线程来说都是原子操作

3.多个线程之间的切换不会导致该接口的执行结果存在二义性

常见的不可重入的情况:

1.调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的

2.调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构,例如stl

3.可重入函数体内使用了静态的数据结构

常见的可重入的情况

1.不使用全局变量或静态变量

2.不使用用 malloc 或者 new 开辟出的空间

3.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全的联系

 1.函数是可重入的,那就是线程安全的

2.函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入与线程安全的区别

1.可重入函数是线程安全函数的一种

2.线程安全不一定是可重入的,而可重入函数则一定是线程安全的

3.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入 函数若锁还未释放则会产生死锁,因此是不可重入的

死锁,代码就不会推进了:在第一次调用 pthread_mutex_lock 成功锁定后,第二次调用将阻塞,因为锁已经被当前线程持有。这种死锁情况会导致线程永远等待自己释放锁,从而阻止线程继续执行后面的代码

 while (true)
    {
        pthread_mutex_lock(&lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。
        pthread_mutex_lock(&lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。
        if(tickets > 0)
        {
            usleep(1000);
            ...}
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值