linux C多线程编程

目录

互斥锁(Mutex)

信号量(Semaphore)

[条件变量(Condition Variable)](#条件变量(Condition Variable))

[pthread_cond_wait 为什么必须配 mutex?](#pthread_cond_wait 为什么必须配 mutex?)

多线程问题本质是三件事:

  1. 互斥(Mutual Exclusion)
    • 多个线程不能同时访问共享资源
    • 典型:修改全局变量、队列、文件
  2. 同步(Synchronization)
    • 线程之间"按顺序发生"
    • 典型:A必须先完成,B才能继续
  3. 通信(Communication)
    • 一个线程通知另一个线程"条件已满足"

互斥锁(Mutex)

1. 作用 :保证同一时刻只有一个线程进入临界区

cpp 复制代码
pthread_mutex_t mutex;

pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);

示例代码

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

int counter = 0;
pthread_mutex_t mutex;

void* add(void* arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, add, NULL);
    pthread_create(&t2, NULL, add, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_mutex_destroy(&mutex);

    printf("counter = %d\n", counter);
    return 0;
}

如果不加锁,.结果通常 < 200000(丢失更新)

pthread_create

├── &t1 → 线程ID输出

├── NULL → 线程属性(默认)

├── add → 线程入口函数

└── NULL → 传给线程的参数

pthread_join(t2, NULL);表示不接受线程的返回值。

信号量(Semaphore)

1. 作用 :信号量 = 计数型资源管理 + 线程同步

类比:

  • 10个停车位
  • 来一辆车 -1
  • 走一辆车 +1

2. 两种类型

(1)二值信号量(类似 mutex)

  • 值:0 / 1
  • 可实现互斥

(2)计数信号量(更常用)

  • 控制资源数量

API(Linux POSIX)

cpp 复制代码
#include <semaphore.h>

sem_t sem;

sem_init(&sem, 0, value); // value 初始值
sem_wait(&sem);           // P操作:-1(阻塞)
sem_post(&sem);           // V操作:+1
sem_destroy(&sem);

//sem_wait() 会尝试把信号量减 1
如果当前值为 0,则不会失败返回,而是阻塞等待,直到信号量变成 >0(sem_post)

sem_init 的 value 表示信号量初始可用资源数量,

pshared=0 表示线程间共享(同一个进程),如果不等于0 表示进程间共享内存。

示例:限制最多3个线程同时访问资源

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

sem_t sem;

void* worker(void* arg) {
    int id = *(int*)arg;
    //void *temp 就是:一个"类型未知的指针变量 temp",可以临时指向任何类型的数据,但使用前必须转换类型。
    sem_wait(&sem);  // 进入资源区(-1)

    printf("Thread %d is working...\n", id);
    sleep(2);

    printf("Thread %d done\n", id);

    sem_post(&sem);  // 释放资源(+1)

    return NULL;
}

int main() {
    pthread_t t[10];
    int id[10];

    sem_init(&sem, 0, 3); // 最多3个线程同时运行

    for (int i = 0; i < 10; i++) {
        id[i] = i;
        pthread_create(&t[i], NULL, worker, &id[i]);
    }

    for (int i = 0; i < 10; i++) {
        pthread_join(t[i], NULL);
    }

    sem_destroy(&sem);
    return 0;
}

解释:

这段程序通过信号量 sem 控制线程并发数量。初始化时将信号量设为 3,表示同一时刻最多允许 3 个线程进入临界区执行任务。

程序创建了 10 个线程,每个线程在进入"工作区"之前都会调用 sem_wait() 申请资源。如果当前信号量大于 0,线程可以继续执行;如果已经为 0,则必须等待其他线程释放资源。

进入临界区后,线程打印信息并 sleep 2 秒模拟工作,完成后调用 sem_post() 释放资源,使其他等待线程可以继续进入。

存在问题;

多个线程是同时运行的 ,但 i 是在循环里变化的。

所以可能出现这种情况:

  • 线程A还没来得及读 id[i]
  • 主线程已经把 i 改成下一个值了

结果:线程可能拿到"错的 id"

条件变量(Condition Variable)

举个例子:

你在等快递:

  • 你先放下手里的钥匙(unlock)
  • 去沙发睡觉
  • 快递员来了叫你
  • 你醒来再拿回钥匙(lock)

1. 作用

用于"线程等待某个条件成立" 不是互斥,而是:

线程睡眠 + 被通知唤醒

2. 必须搭配 mutex 使用

cpp 复制代码
pthread_mutex_t mutex;
pthread_cond_t cond;

3. 核心API

cpp 复制代码
pthread_cond_wait(&cond, &mutex);
pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);


/*wait = 解锁睡觉
signal = 叫醒一个
broadcast = 叫醒全部*/

cond_wait 不是单纯"睡觉"

它其实是:

先解锁 mutex + 再睡觉 + 被叫醒后再加锁"

复制代码
wait = 解锁 + 睡觉 + 被唤醒 + 再加锁

为什么一定要这样设计?

因为如果它不先解锁:

别的线程就永远进不来修改条件, 那你就永远醒不过来(死锁)

4.示例代码

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

int data_ready = 0;
int data = 0;

pthread_mutex_t mutex;
pthread_cond_t cond;

void* producer(void* arg) {
    for (int i = 1; i <= 5; i++) {
        pthread_mutex_lock(&mutex);

        data = i;
        data_ready = 1;

        printf("Producer produced: %d\n", data);

        pthread_cond_signal(&cond); // 通知消费者

        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* consumer(void* arg) {
    for (int i = 1; i <= 5; i++) {
        pthread_mutex_lock(&mutex);

        while (data_ready == 0) {
            pthread_cond_wait(&cond, &mutex);
        }

        printf("Consumer consumed: %d\n", data);
        data_ready = 0;

        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t p, c;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_create(&p, NULL, producer, NULL);
    pthread_create(&c, NULL, consumer, NULL);

    pthread_join(p, NULL);
    pthread_join(c, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

consumer 被唤醒后,必须等 producer 释放 mutex,然后再去抢锁(竞争 mutex)

五、pthread_cond_wait 为什么必须配 mutex?

这是重点面试题:

原子(atomic)在计算机里的意思,不是物理里的"不可分割的原子",而是一个非常重要的并发概念

原子操作 = 要么完全执行完成,要么完全不执行,中间不会被打断,也不会出现"执行一半"的状态。

它做了三件事(原子操作):

  1. 解锁 mutex

  2. 线程进入睡眠

  3. 被唤醒后重新加锁 mutex