linux(8)(下)

https://blog.csdn.net/qscftqwe/article/details/156202943

上节课链接,强烈推荐看一下球求拉!

一.POSIX信号量(线程和进程信号量)

POSIX 信号量和 System V 信号量作用相同,都是用于同步操作 ,达到无冲突地访问共享资源的目的。

POSIX 信号量既可用于线程间同步 (无名信号量,pshared=0),也可用于进程间同步 (有名信号量或 pshared≠0 的无名信号量);而 System V 信号量仅用于进程间同步

进一步了解信号量

  • 信号量的本质是一个计数器 ,用于描述可用资源的数量
  • 申请信号量(如 sem_wait)会原子地判断并减少计数值,从而间接判断资源是否就绪。
  • 当信号量初值为 1 时,称为二元信号量 ,其行为类似于互斥锁

二.信号量的操作

1.sem_init

cpp 复制代码
// 初始化一个未命名信号量(可用于线程或进程间同步)
// pshared 为 0 时用于线程间 pshared 非 0 且支持时用于进程间
#include <semaphore.h>

int sem_init(
    sem_t *sem,
    int pshared,
    unsigned int value
);

// 参数:
//   sem     - 指向信号量变量
//   pshared - 0 表示线程间共享 非 0 表示进程间共享(需在共享内存中)
//   value   - 信号量初始值(通常为 0 或 1)

// 返回值:
//   成功:返回 0
//   失败:返回 -1 并设置 errno

// 案例:
    // 初始化一个二元信号量 初始可用
    sem_init(&sem, 0, 1);

2.sem_destroy

cpp 复制代码
// 销毁一个未命名信号量 释放其占用的资源
// 调用前应确保没有线程或进程在等待该信号量
#include <semaphore.h>

int sem_destroy(sem_t *sem);

// 参数:
//   sem - 指向已初始化的信号量

// 返回值:
//   成功:返回 0
//   失败:返回 -1 并设置 errno

// 案例:
    sem_destroy(&sem);

3.sem_wait

cpp 复制代码
// 对信号量执行 P 操作(申请资源)
// 若信号量值 > 0 则减 1 并立即返回
// 若信号量值 == 0 则阻塞当前线程 直到其他线程调用 sem_post
#include <semaphore.h>

int sem_wait(sem_t *sem);

// 参数:
//   sem - 指向已初始化的信号量

// 返回值:
//   成功:返回 0
//   失败:返回 -1 并设置 errno

// 案例:
    sem_wait(&sem);

4.sem_post

cpp 复制代码
// 对信号量执行 V 操作(释放资源)
// 将信号量值加 1 并唤醒一个等待该信号量的线程(如有)
#include <semaphore.h>

int sem_post(sem_t *sem);

// 参数:
//   sem - 指向已初始化的信号量

// 返回值:
//   成功:返回 0
//   失败:返回 -1 并设置 errno

// 案例:
    sem_post(&sem);

三.环形队列的生产消费模型

认识信号量的帮助

  • 环形队列中,起始和结束状态相同(head == tail),难以区分空与满 ,可通过预留一个空位使用计数器 解决;若使用两个信号量(empty 和 full) ,可自然区分空满状态,无需额外判断
  • headtail 重合时,队列可能为空或满,此时需通过同步机制(如信号量)控制生产与消费的顺序
  • 多生产者-多消费者场景 中,通常需要两把锁 :一把保护生产者对 tail 的修改,一把保护消费者对 head 的修改;同时配合两个信号量(empty 和 full)信号量的 wait 操作应在加锁前,post 操作在解锁后

四.实现环形队列的生产消费模型

4.1函数复习

1.pthread_mutex_lock

cpp 复制代码
// 对互斥锁加锁 若已被其他线程持有 则阻塞当前线程
// 用于进入临界区 保证共享数据访问的原子性
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

// 参数:
//   mutex - 指向已初始化的互斥锁

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

// 案例:
    pthread_mutex_lock(&mutex);

2.pthread_mutex_unlock

cpp 复制代码
// 对互斥锁解锁 唤醒其他等待该锁的线程
// 必须由持有锁的线程调用 否则行为未定义
#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 参数:
//   mutex - 指向已加锁的互斥锁

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

// 案例:
    pthread_mutex_unlock(&mutex);

3.pthread_cond_wait

cpp 复制代码
// 阻塞当前线程并自动释放关联的互斥锁 等待条件变量被唤醒
// 唤醒后会重新 acquire 锁 再返回(因此返回时仍持有锁)
#include <pthread.h>

int pthread_cond_wait(
    pthread_cond_t *cond,
    pthread_mutex_t *mutex
);

// 参数:
//   cond  - 指向条件变量
//   mutex - 指向与该条件变量关联的互斥锁(必须已加锁)

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

// 案例:
    pthread_cond_wait(&cond, &mutex);

4.pthread_cond_signal

cpp 复制代码
// 唤醒等待该条件变量的**一个**线程(通常为等待队列中的第一个)
// 若无线程等待 则无操作
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);

// 参数:
//   cond - 指向条件变量

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

// 案例:
    pthread_cond_signal(&cond);

5.pthread_mutex_init

cpp 复制代码
// 初始化一个互斥锁 用于保护临界区
// 默认属性下锁是非递归的 同一线程重复加锁会导致死锁
#include <pthread.h>

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

// 参数:
//   mutex - 指向要初始化的互斥锁变量
//   attr  - 锁属性(通常设为 NULL 表示默认属性)

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

// 案例:
    pthread_mutex_init(&mutex, NULL);

6.pthread_cond_init

cpp 复制代码
// 初始化一个条件变量 用于线程间同步(常与互斥锁配合使用)
// 条件变量本身不存储状态 仅用于阻塞/唤醒等待特定条件的线程
#include <pthread.h>

int pthread_cond_init(
    pthread_cond_t *cond,
    const pthread_condattr_t *attr
);

// 参数:
//   cond - 指向要初始化的条件变量
//   attr - 属性(通常设为 NULL 表示默认)

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

// 案例:
    pthread_cond_init(&cond, NULL);

7.pthread_mutex_destroy

cpp 复制代码
// 销毁一个互斥锁 释放其占用的资源
// 锁必须处于未锁定状态 否则行为未定义
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 参数:
//   mutex - 指向要销毁的互斥锁(必须已初始化)

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

// 案例:
    pthread_mutex_destroy(&mutex);

8.pthread_cond_destroy

cpp 复制代码
// 销毁一个条件变量 释放其占用的资源
// 调用前应确保没有线程在等待该条件变量
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);

// 参数:
//   cond - 指向要销毁的条件变量(必须已初始化)

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

// 案例:
    pthread_cond_destroy(&cond);

9.pthread_create

cpp 复制代码
// 创建一个新的线程 执行指定函数
// 新线程与调用线程共享进程地址空间(如全局变量、堆)
#include <pthread.h>

int pthread_create(
    pthread_t *thread,
    const pthread_attr_t *attr,
    void *(*start_routine)(void *),
    void *arg
);

// 参数:
//   thread         - 指向 pthread_t 变量 用于保存新线程 ID
//   attr           - 线程属性(通常设为 NULL 表示默认属性)
//   start_routine  - 新线程要执行的函数(返回 void* 接收 void*)
//   arg            - 传递给 start_routine 的参数(可为 NULL)

// 返回值:
//   成功:返回 0
//   失败:返回非 0 错误码(不设 errno)

// 案例:
    pthread_create(&tid, NULL, task, data);

10.pthread_join

cpp 复制代码
// 等待指定线程终止 并回收其资源(类似进程的 wait)
// 调用者会阻塞 直到目标线程结束
#include <pthread.h>

int pthread_join(
    pthread_t thread,
    void **retval
);

// 参数:
//   thread  - 要等待的线程 ID
//   retval  - 若非 NULL 则接收线程的返回值(即 start_routine 的返回值)

// 返回值:
//   成功:返回 0
//   失败:返回非 0 错误码(如线程不可 join 或已 join)

// 案例:
    pthread_join(tid, NULL);

4.2 完整代码

cpp 复制代码
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h> // 用于线程休眠,避免打印刷屏

#define NUM 16 // 队列默认容量

// 阻塞队列:线程安全的生产者-消费者模型
class BlockQueue
{
    std::queue<int> _q;             // 存储数据的队列
    int _cap;                       // 队列容量
    pthread_mutex_t _lock;          // 互斥锁:保护队列访问
    pthread_cond_t _not_full;       // 条件变量:队列满时生产者等待
    pthread_cond_t _not_empty;      // 条件变量:队列空时消费者等待

private:
    void LockQueue()
    { // 加锁
        pthread_mutex_lock(&_lock);//(1)
    }
    void UnLockQueue()
    { // 解锁
        pthread_mutex_unlock(&_lock);//(2)
    }
    void ProductWait()
    { // 生产者等待队列有空间
        pthread_cond_wait(&_not_full, &_lock);//(3)
    }
    void ConsumeWait()
    { // 消费者等待队列有数据
        pthread_cond_wait(&_not_empty, &_lock);
    }
    void NotifyConsume()
    { // 通知消费者(队列有数据)
        pthread_cond_signal(&_not_empty);//(4)
    }
    void NotifyProduct()
    { // 通知生产者(队列有空间)
        pthread_cond_signal(&_not_full);
    }
    bool IsEmpty()
    { // 检查队列是否为空
        return _q.empty();
    }
    bool IsFull()
    { // 检查队列是否已满
        return _q.size() == _cap;
    }

public:
    BlockQueue(int _cap = NUM) 
        : _cap(_cap)
    { // 初始化队列
        pthread_mutex_init(&_lock, NULL);//(5)
        pthread_cond_init(&_not_full, NULL);//(6)
        pthread_cond_init(&_not_empty, NULL);
    }

    void PushData(const int &data)
    { // 生产数据:阻塞直到队列有空间
        LockQueue();
        while (IsFull())
            ProductWait(); // 等待队列有空间
        _q.push(data);
        NotifyConsume(); // 通知消费者
        UnLockQueue();
    }

    void PopData(int &data)
    { // 消费数据:阻塞直到队列有数据
        LockQueue();
        while (IsEmpty())
            ConsumeWait(); // 等待队列有数据
        data = _q.front();
        _q.pop();
        NotifyProduct(); // 通知生产者
        UnLockQueue();
    }

    ~BlockQueue()
    { // 销毁同步原语
        pthread_mutex_destroy(&_lock);//(7)
        pthread_cond_destroy(&_not_full);//(8)
        pthread_cond_destroy(&_not_empty);
    }
};

// 消费者线程:从队列取数据并打印
void *consumer(void *arg)
{
    BlockQueue *bqp = (BlockQueue *)arg;
    int data;
    for (;;)
    {
        bqp->PopData(data); // 阻塞等待数据
        std::cout << "Consume data done: " << data << std::endl;
        usleep(100000); // 休眠避免打印刷屏
    }
}

// 生产者线程:生成并放入队列
void *producter(void *arg)
{
    BlockQueue *bqp = (BlockQueue *)arg;
    static int data = 1; // 从1开始,每次递增
    for (;;)
    {
        bqp->PushData(data);      // 阻塞等待队列空间
        std::cout << "Product data done: " << data << std::endl;
        ++data;                   // 下一次生产下一个数字
        usleep(50000);            // 休眠避免刷屏
    }
}
int main()
{
    BlockQueue bq;    // 创建默认容量16的阻塞队列
    pthread_t c, p;   // 消费者和生产者线程ID

    pthread_create(&c, NULL, consumer, (void *)&bq);  // 启动消费者//(9)
    pthread_create(&p, NULL, producter, (void *)&bq); // 启动生产者

    pthread_join(c, NULL);    // 等待消费者线程结束(实际会永远运行)//(10)
    pthread_join(p, NULL);    // 等待生产者线程结束
    return 0;
}

我会讲解其中两个函数,其余函数还请大家结合函数复习与注释进行观看!

cpp 复制代码
 void PushData(const int &data)
    { // 生产数据:阻塞直到队列有空间
        LockQueue();
        while (IsFull())
            ProductWait(); // 等待队列有空间
        _q.push(data);
        NotifyConsume(); // 通知消费者
        UnLockQueue();
    }

    void PopData(int &data)
    { // 消费数据:阻塞直到队列有数据
        LockQueue();
        while (IsEmpty())
            ConsumeWait(); // 等待队列有数据
        data = _q.front();
        _q.pop();
        NotifyProduct(); // 通知生产者
        UnLockQueue();
    }

这两个函数分别用于数据生成和消费。在生产者-消费者模型中,我们遵循"321原则"。但环形队列的不同之处在于:生产者和消费者可以并发执行(一方持续发送数据,另一方持续接收,只要不出现位置重叠就能持续并发)。当出现位置重叠时,就会触发互斥机制。

因此,当缓冲区满时,生产必须暂停;反之,当缓冲区空时,消费必须暂停。暂停的目的是为了唤醒对方进行生产或消费。

**五.**STl、智能制造和线程安全

STL和线程安全

STL容器不具备多线程安全

智能指针和线程安全

智能指针具有线程安全(因为一个不可以共享自然没有线程安全问题,一个采用引用计数也可以引发)

好了线程内容就差不多讲到这把,首先关于线程这部分我讲解的都是重点的部分,这节课主要重点是信号量操作搞明白,然后就是学一下基于环形队列的生产消费模型。

相关推荐
谢平康1 天前
通过nfs方式做目录限额方法
linux·服务器·网络
Hcoco_me1 天前
大模型面试题37:Scaling Law完全指南
人工智能·深度学习·学习·自然语言处理·transformer
刘一说1 天前
Windows 与 Linux 跨平台自动化 MySQL 8 备份:专业级脚本设计与实战指南
linux·数据库·windows·mysql·自动化
I · T · LUCKYBOOM1 天前
2.2yum安装--单服务器托管多网站
linux·运维·服务器
郝学胜-神的一滴1 天前
线程同步:并行世界的秩序守护者
java·linux·开发语言·c++·程序人生
龘龍龙1 天前
Python基础学习(十)
服务器·python·学习
im_AMBER1 天前
Leetcode 95 分割链表
数据结构·c++·笔记·学习·算法·leetcode·链表
A9better1 天前
嵌入式开发学习日志47——任务创建与就绪列表
单片机·嵌入式硬件·学习
北岛寒沫1 天前
北京大学国家发展研究院 经济学原理课程笔记(第十八课 国内生产总值与物价水平)
经验分享·笔记·学习