Linux之线程进阶(一)

同进程内多个线程共享地址空间、共享进程资源

多个线程同时访问同一份共享资源

典型现象:多线程同时打印到显示器(stdout,fd=1),输出乱序、文字穿插干扰

本质问题:并发访问共享资源带来的数据不一致问题

一个现象

cpp 复制代码
#include <iostream>
#include "Thread.hpp"
#include <unistd.h>
#include <string>
#include <vector>
using namespace std;

int tickets = 1000;

void GetTicket()
{
    char name[64];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    while (1)
    {
        if (tickets > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name, tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{

    ThreadModul::Thread t1(GetTicket);
    ThreadModul::Thread t2(GetTicket);
    ThreadModul::Thread t3(GetTicket);
    ThreadModul::Thread t4(GetTicket);
    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();
    sleep(10);
    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();
    return 0;
}

初始 tickets = 1

  1. 线程 2:if(tickets>0) → 1>0,条件成立,进入代码块,usleep 休眠、让出 CPU
  2. 线程 1:if(tickets>0) → 1>0,条件成立,进入代码块,usleep 休眠

重点:两个线程都已经通过了 if(tickets>0) 的校验! 此时票还没减!

  1. 调度唤醒线程 2:打印 ticket:1 → tickets-- → tickets=0
  2. 调度唤醒线程 1:打印 ticket:0 → tickets-- → tickets=-1

于是就出现:票已经 0 了,还能继续卖出,出现负数 你截图里 -1 -2 就是多轮这种叠加导致的

同步

  • VIP 自习室 = 临界资源(同一时间只允许 1 个人用)
  • 人 = 线程
  • 自习的过程 = 临界区
  • 钥匙 = 互斥锁(pthread_mutex)

规则:

  1. 错了吗?没错
  2. 问题?裸竞争不合理,效率太低
  3. 管理员规则:
    • 归还钥匙之后,不能立即申请,如果要再用,就必须重新排队
    • 外部的人必须排队

线程同步定义:在临界资源安全的前提下,让访问临界资源具有一定的顺序性!
💡区分:

  • 互斥:保证同一时刻,只有一个线程进入临界区(解决安全问题,最基础)
  • 同步:在互斥基础上,控制线程访问的先后顺序(解决顺序问题)

生产者消费者模型

这是多线程协同的一种模式,提高协作效率,本质是一种通信工具

3 种关系:生产者和生产者,消费者和消费者,生产者和消费者

  • 生产者 ↔ 生产者:互斥
  • 消费者 ↔ 消费者:互斥
  • 生产者 ↔ 消费者:同步 + 互斥

2 种角色:生产者线程、消费者线程

1 一个交易场所:超市(缓冲区 / 内存空间),一般用队列、环形队列这类数据结构实现

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间 不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔 给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

✨生产者消费者模型的优势
  1. 解耦:生产者、消费者代码逻辑分离,互不依赖,易于维护扩展
  2. 并发:生产、消费线程并行执行,提升系统吞吐量
  3. 削峰缓冲:瞬时流量先存入缓冲区,保护下游消费者不被突发流量打垮
  4. 速率适配:自动协调快慢,生产快就缓存、消费快就等待,减少无效 CPU 占用

理解条件变量

条件变量,就是让线程「等某个条件满足再干活」,不满足就休眠,不瞎循环空耗 CPU;必须搭配互斥锁一起使用!

互斥锁:管能不能进临界区 (互斥) 条件变量:管什么时候可以干活(等待 + 唤醒,同步)

例子

宿舍饮水机:

  • 互斥锁 = 饮水机钥匙,同一时间只能 1 个人接水
  • 条件变量 = "有水吗" 这个信号
    1. 你来接水,拿到钥匙,发现没水 → 不一直反复拧钥匙空等,直接休眠,释放钥匙(pthread_cond_wait自动解锁 + 休眠)
    2. 送水工(另一个线程)换水完成,发信号唤醒等待的人(pthread_cond_signal
    3. 你被唤醒后,自动重新拿到锁,再检查有没有水,有水再接水

代码

cpp 复制代码
void* Print(void* args)
{
    string name=static_cast<const char*>(args);
    while(1)
    {
        cout<<"我是新线程:"<<name<<endl;
       
    }
    return nullptr;
}


int main()
{
    pthread_t tids[4];
    for(int i=0;i<4;i++)
    {
        char *name=new char[64];

        snprintf(name,64,"thread-%d",i+1);
        pthread_create(tids+i,nullptr,Print,name);
    }
    for(int i=0;i<4;i++)
    {
        void *retval;
        pthread_join(tids[i],&retval);
    }
    return 0;
}

上锁

阻塞等待

cpp 复制代码
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;

void* Print(void* args)
{
    string name=static_cast<const char*>(args);
    while(1)
    {
        pthread_mutex_lock(&gmutex);        // 1.抢锁
        cout<<"我是新线程:"<<name<<endl;    // 2.打印
        pthread_cond_wait(&gcond,&gmutex); // 3.【重点】释放锁 + 阻塞休眠,等唤醒信号
        pthread_mutex_unlock(&gmutex);     // 4.被唤醒后,抢到锁才走到这里解锁
    }
    return nullptr;
}

唤醒

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
using namespace std;

pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;

void* Print(void* args)
{
    string name=static_cast<const char*>(args);
    while(1)
    {
        pthread_mutex_lock(&gmutex);
        cout<<"我是新线程:"<<name<<endl;
        pthread_cond_wait(&gcond,&gmutex);
        pthread_mutex_unlock(&gmutex);
        sleep(1);
    }
    return nullptr;
}


int main()
{
    pthread_t tids[4];
    for(int i=0;i<4;i++)
    {
        char *name=new char[64];

        snprintf(name,64,"thread-%d",i+1);
        pthread_create(tids+i,nullptr,Print,name);
    }


    
    while(1)
    pthread_cond_signal(&gcond);
    for(int i=0;i<4;i++)
    {
        void *retval;
        pthread_join(tids[i],&retval);
    }
    return 0;
}

阻塞

整体结构说明
  1. BlockQueue<T>:模板类阻塞队列(内部封装pthread_mutex_t + pthread_cond_t
  2. ProdurorRoutine:生产者线程入口函数,不断往阻塞队列放数据
  3. ConsumerRoutine:消费者线程入口函数,不断从阻塞队列取数据(空队列时阻塞等待)
  4. main:创建阻塞队列、生产者 / 消费者子线程,pthread_join等待回收
相关推荐
互联网中的一颗神经元2 小时前
01. Go 内存管理全景架构
java·jvm·golang
互联网中的一颗神经元3 小时前
02. 核心概念与术语表
java·jvm·spring
名字还没想好☜16 小时前
Java 线上内存泄漏排查实战:jmap 导堆、MAT 找 GC Roots 与四类常见泄漏
java·开发语言·jvm·内存泄漏
淡海水1 天前
01-07-运行时-GC深度剖析-内存分配回收与结构选择
jvm·windows·unity·gc
2602_959960921 天前
电商大厂Java面试:从Spring Boot、JPA、微服务到Redis、Kafka、Spring Security与监控,谢飞机的爆笑三轮问答
java·jvm·spring boot·redis·面试题
一水1 天前
AI 时代审查思维:审查第一篇
java·jvm·数据库·spring
それども1 天前
JVM MetaspaceSize 参数作用
jvm
java_upp2 天前
JVM内存溢出和内存泄漏的区别及排查方法
jvm·内存泄漏·内存溢出
城管不管2 天前
MySQL 慢查询完整排查
java·服务器·jvm·数据库·mysql·spring·面试