【Linux】线程同步与互斥

一. 线程互斥

1.1 进程线程间的互斥

  • 共享资源:多个执行流能看到的同一份公共资源
  • 临界资源: 多线程执行流被保护起来的共享资源叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,叫做临界区,其余叫非临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用(这样访问同一块临界数据的线程就形成了竞争关系,竞争抢占临界资源)
  • 同步:同步用于协调线程间的执行顺序。当一个线程需要等待另一个线程完成某项任务或到达某个状态后才能继续执行时,就需要同步。
  • 原子性:不被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

1.2 互斥量mutex

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

1.2.1 数据不一致问题

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

以上代码是用来模拟抢票的程序

但是为什么会出现票数为负的情况?我们不是已经判断了吗?

在多线程中,对于没有被保护的共享资源,更多的并发和更多的线程切换就会更多的造成数据不一致问题。
线程切换是什么时间点切走当前执行的线程?

  • 时间片结束,触发时钟中断陷入内核
  • I/O阻塞,调用系统调用,陷入内核
  • 执行sleep等函数时线程被挂起,陷入内核
    线程什么时候切换到新线程执行呢?
    当CPU从内核态切换到用户态时,会检测线程是否需要切换,进而根据线程的优先级进行线程的切换调度。

    上面的route叫做不可重入函数,因为访问了全局资源。

要解决以上的问题,需要:

  • 代码必须有互斥行为:当代码执行临界区时,不允许其他线程进入该临界区
  • 如果多个线程同时要求执行临界区代码,且临界区代码没有线程执行,那么只允许一个线程进入该临界区
  • 如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区
    要做到这三点,本质就是需要一把锁。Linux上提供的锁叫做互斥量。

1.2.2 互斥量

  • 全局锁
cpp 复制代码
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 或
pthread_mutex_t lock;

pthread_mutex_lock(&lock);// 加锁
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&lock);// 解锁
        }
  • 动态分配锁
cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const 
pthread_mutexattr_t *restrict attr);
 参数:
 mutex:要初始化的互斥量
 attr:nullptr
  • 加锁和解锁
cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 成功,继续向后运行,访问临界区和临界资源
// 失败,阻塞挂起申请执行流
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_lock 时,可能会遇到以下情况:

• 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

• 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

  • 销毁互斥量
    销毁互斥量需要注意:
    • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁(全局锁退出时自动释放)
    • 不要销毁一个已经加锁的互斥量
    • 已经销毁的互斥量,要确保后面 不会有线程再尝试加锁
cpp 复制代码
 int pthread_mutex_destroy(pthread_mutex_t *mutex);

线程竞争申请锁,多线程都要先看到锁,锁本身也是临界资源!因此,申请锁的过程必须是原子的!!

对临界资源进行保护,本质就是用锁对临界区代码进行保护。

共享资源一旦被加锁,所有的线程都必须遵守锁。

加锁之后,在执行临界区资源的时候,是允许线程切换的,但是锁的申请过程是互斥的,也就是说只有当前线程持有锁,其他线程没有锁,当线程被切换走的时候,其他线程没有锁进入不了临界区,也就只能在等待队列中等待该线程执行完临界区代码后释放锁,然后展开申请锁的竞争,重新申请锁进入临界区。

锁提供的能力本质是:将执行临界区代码的线程由并行执行转换为串行执行,在线程执行期间不被打扰,也是一种变相的原子性表现。

1.2.3 理解锁

锁的原理

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

1.2.4 封装Mutex

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <cstring>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            // if (n == 0)
            // {
            //     std::cout << "加锁成功" << std::endl;
            // }
            // else
            // {
            //     std::cout << "加锁失败" << strerror(n) << std::endl;
            // }
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            // if (n == 0)
            // {
            //     std::cout << "解锁成功" << std::endl;
            // }
            // else
            // {
            //     std::cout << "解锁失败" << strerror(n) << std::endl;
            // }
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex& mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex& _mutex;
    };
}
cpp 复制代码
// RAII风格互斥锁
class Threadlock
{
public:
    std::string name;
    Mutex *lockp;
    Threadlock(std::string n, Mutex &lock)
        : name(n), lockp(&lock)
    {
    }
    ~Threadlock() {}
};

void *route(void *arg)
{
    Threadlock *td = static_cast<Threadlock *>(arg);
    while (1)
    {
        {
            LockGuard mg(*(td->lockp));// RAII,循环一次创建和析构一次,对应加锁解锁一次
            if (ticket > 0)
            {
                usleep(1000);
                printf("%s sells ticket:%d\n", td->name.c_str(), ticket);
                ticket--;
            }
            else
            {
                break;
            }
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    Mutex lock;

    Threadlock *tl1 = new Threadlock("thread1", lock);
    pthread_create(&t1, NULL, route, tl1);
    Threadlock *tl2 = new Threadlock("thread2", lock);
    pthread_create(&t2, NULL, route, tl2);
    Threadlock *tl3 = new Threadlock("thread3", lock);
    pthread_create(&t3, NULL, route, tl3);
    Threadlock *tl4 = new Threadlock("thread4", lock);
    pthread_create(&t4, NULL, route, tl4);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

二. 线程同步

对于上面的代码,我们发现到最后只有一个线程在疯狂的执行,这样就会导致其他线程访问不到临界资源,拿不到锁,就造成线程饥饿的问题。而且这样对于程序的效率会降低,对于其他拿不到锁的线程也不公平,想要解决这个问题,就不能让某一个线程一直占据锁,应该让这个线程执行完代码后进入队列等待二次申请锁,也就是让线程按照一定的顺序访问资源!

线程的互斥是让多个线程由并行执行转化为串行执行,而线程的同步就是让串行执行的线程按照一定的顺序执行代码。

2.1 条件变量

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

没有条件变量之前,一个线程可能会多次申请锁访问临界资源但是并没有对临界资源做什么事情,因为临界资源没有就绪,然后释放锁,这样会造成效率降低;有了条件变量之后,这个线程访问一次临界资源发现临界资源不满足自己的需求,进而直接到等待队列中等待临界资源就绪,当临界资源就绪的时候,条件变量就会唤醒这个挂起的线程。

2.2 同步概念和竞态条件

  • 同步:在保证数据安全的情况下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

2.3 cond接口

cpp 复制代码
pthread_cond_t // 条件变量的类型
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 全局条件变量不需要初始化和销毁
  • 初始化和销毁
cpp 复制代码
#include <pthread.h>

       int pthread_cond_destroy(pthread_cond_t *cond);
       int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);
       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 等待
cpp 复制代码
#include <pthread.h>

       int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime);
       int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);
  • 唤醒
cpp 复制代码
       #include <pthread.h>

       int pthread_cond_broadcast(pthread_cond_t *cond);// 唤醒所有线程
       int pthread_cond_signal(pthread_cond_t *cond);// 唤醒某一个线程

demo

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>

pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

int cnt = 100;

void *routine(void *args)
{
    std::string name = static_cast<char *>(args);
    while (true)
    {
        pthread_mutex_lock(&glock);
        pthread_cond_wait(&gcond, &glock);
        std::cout << name << "计算 : " << cnt << std::endl;
        cnt++;
        pthread_mutex_unlock(&glock);
    }
}

int main()
{
    std::vector<pthread_t> tds;
    for (int i = 0; i < 5; i++)
    {
        pthread_t td;
        char name[64];
        snprintf(name, 64, "thread-%d", i);
        pthread_create(&td, nullptr, routine, name);
        tds.push_back(td);
        sleep(1);
    }
    sleep(3);

    while (true)
    {
        std::cout << " 唤醒线程" << std::endl;
        pthread_cond_signal(&gcond);
        sleep(1);
    }

    for (auto &ts : tds)
    {
        pthread_join(ts, nullptr);
    }
    return 0;
}
相关推荐
舰长1152 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
AI视觉网奇2 小时前
FBX AnimSequence] 动画长度13与导入帧率30 fps(子帧0.94)不兼容。动画必须与帧边界对齐。
笔记·学习·ue5
zmjjdank1ng2 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.2 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
科技林总2 小时前
使用Miniconda安装Jupyter
笔记
VekiSon2 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
zl_dfq3 小时前
Linux 之 【进程信号】(signal、kill、raise、abort、alarm、Core Dump核心转储机制)
linux
woodykissme3 小时前
倒圆角问题解决思路分享
笔记·学习·工艺
laplace01233 小时前
Clawdbot 部署到飞书(飞连)使用教程(完整版)
人工智能·笔记·agent·rag·clawdbot