C语言线程同步机制

pthread(POSIX 线程库)中,共享同步机制是一套用于协调多个线程对共享资源访问的核心工具 。它们的根本目的是防止数据竞争,确保多线程程序的正确性和稳定性。

以下是 pthread 提供的主要同步机制及其关键特性:

一、互斥锁 (Mutex)

互斥锁是最基本的同步原语,用于保护临界区(Critical Section) ,确保同一时刻只有一个线程能访问被保护的共享资源。

  • 核心操作

  • 初始化pthread_mutex_init() 或静态初始化 PTHREAD_MUTEX_INITIALIZER

  • 加锁pthread_mutex_lock()(阻塞) 或 pthread_mutex_trylock()(非阻塞)。

  • 解锁pthread_mutex_unlock()

  • 销毁pthread_mutex_destroy()

  • 适用场景:绝大多数需要互斥访问的场景,是最通用的选择。

  • 下面是一个使用 pthread 互斥锁 (Mutex) 的完整 C 语言示例。程序创建了两个线程,它们共享一个全局计数器,并通过互斥锁来保证对计数器操作的原子性,避免数据竞争。

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

#define NUM_THREADS 2
#define INCREMENT_COUNT 1000000

/* 共享资源 */
long counter = 0;

/* 互斥锁变量 (全局) */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;   // 静态初始化

/* 线程执行的函数 */
void* thread_func(void* arg) {
    int tid = *(int*)arg;
    for (int i = 0; i < INCREMENT_COUNT; ++i) {
        // 1. 加锁(保护临界区)
        pthread_mutex_lock(&mutex);
        // 2. 访问共享资源(临界区开始)
        counter++;
        // 3. 解锁
        pthread_mutex_unlock(&mutex);
    }
    printf("线程 %d 完成工作\n", tid);
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    int tids[NUM_THREADS];

    // 也可以使用动态初始化(若之前未静态初始化)
    // pthread_mutex_init(&mutex, NULL);

    // 创建线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        tids[i] = i;
        if (pthread_create(&threads[i], NULL, thread_func, &tids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待所有线程结束
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 输出最终结果
    printf("counter = %ld (期望值: %d)\n", counter, NUM_THREADS * INCREMENT_COUNT);

    // 销毁互斥锁(不再使用)
    pthread_mutex_destroy(&mutex);

    return 0;
}
  • 注意事项 :使用不当极易造成死锁(Deadlock) 。务必注意锁的粒度,并在所有可能的退出路径(如 return 前)释放锁。

二、条件变量 (Condition Variable)

条件变量本身不是锁,它通常与互斥锁配合使用 ,允许线程在特定条件不满足时阻塞等待 ,并在条件满足时被唤醒

  • 核心操作

  • 初始化pthread_cond_init() 或静态初始化 PTHREAD_COND_INITIALIZER

  • 等待pthread_cond_wait()(需传入已加锁的互斥锁)。

  • 唤醒pthread_cond_signal()(唤醒一个等待线程)或 pthread_cond_broadcast()(唤醒所有等待线程)。

  • 销毁pthread_cond_destroy()

  • 适用场景:需要基于共享状态的变化来唤醒线程,例如"生产者-消费者"模型。

  • 下面是一个使用 pthread 条件变量 (Condition Variable) 的完整 C 语言示例。程序模拟了一个简单的 生产者-消费者 场景:一个线程生产数据,另一个线程消费数据,通过条件变量来实现 "等待直到有数据可用" 的同步逻辑。

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

/* 共享资源 */
int data_available = 0;      // 1 表示有数据,0 表示无数据
int shared_data = 0;         // 实际的数据

/* 同步原语 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

/* 生产者线程:生成数据并通知消费者 */
void* producer(void* arg) {
    for (int i = 1; i <= 5; ++i) {
        /* 1. 加锁 */
        pthread_mutex_lock(&mutex);
        
        /* 2. 生产数据(模拟工作) */
        shared_data = i;
        data_available = 1;
        printf("生产者: 生产了数据 %d\n", shared_data);
        
        /* 3. 唤醒等待的消费者 */
        pthread_cond_signal(&cond);
        
        /* 4. 解锁 */
        pthread_mutex_unlock(&mutex);
        
        /* 模拟生产间隔 */
        sleep(1);
    }
    
    return NULL;
}

/* 消费者线程:等待数据到来并消费 */
void* consumer(void* arg) {
    for (int i = 0; i < 5; ++i) {
        /* 1. 加锁 */
        pthread_mutex_lock(&mutex);
        
        /* 2. 检查条件,如果不满足则等待 */
        while (data_available == 0) {
            printf("消费者: 无数据,等待中...\n");
            /*
             *  pthread_cond_wait() 会原子地:
             *    - 释放 mutex
             *    - 阻塞本线程,等待被唤醒
             *  当被唤醒时,它会重新获取 mutex 然后返回
             */
            pthread_cond_wait(&cond, &mutex);
        }
        
        /* 3. 条件满足,消费数据(临界区) */
        printf("消费者: 消费了数据 %d\n", shared_data);
        data_available = 0;   // 重置标志
        
        /* 4. 解锁 */
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t prod, cons;
    
    /* 创建生产者和消费者线程 */
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);
    
    /* 等待线程结束 */
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
    
    /* 销毁同步对象 */
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    
    return 0;
}
要点 说明
必须与互斥锁配合 条件变量本身无状态,必须用互斥锁保护共享条件的检查和修改。
pthread_cond_wait() 必须在持有锁时调用 它会原子地释放锁并阻塞 ;被唤醒后重新获取锁再返回。
while 而不是 if 检查条件 防止虚假唤醒 (spurious wakeups)------即使没有被信号唤醒,pthread_cond_wait 也可能返回。用 while 循环可以确保条件真正满足时才继续。
唤醒后条件可能再次变化 多个消费者可能竞争资源,所以唤醒后仍需检查条件。
使用 pthread_cond_signal 唤醒一个等待线程 如果有多个等待线程,只唤醒一个(通常用于只满足一个资源的情况)。
使用 pthread_cond_broadcast 唤醒所有等待线程 当条件状态变化影响所有等待线程时(如"所有数据已就绪"),使用广播。
  • 注意事项pthread_cond_wait() 被唤醒后,需要重新检查条件,因为可能被虚假唤醒(Spurious Wakeup)。

三、读写锁 (Read-Write Lock)

读写锁允许多个线程同时读取 共享资源,但在写入时只允许一个线程独占访问。

  • 核心操作

  • 初始化pthread_rwlock_init()

  • 加读锁pthread_rwlock_rdlock()

  • 加写锁pthread_rwlock_wrlock()

  • 解锁pthread_rwlock_unlock()

  • 销毁pthread_rwlock_destroy()

  • 适用场景读操作远多于写操作的场景,能显著提高并发性能。

  • 下面是一个使用 pthread 读写锁 (Read-Write Lock) 的完整 C 语言示例。该程序模拟了一个共享数据(整数)的并发访问场景:多个读线程可以同时读取,而写线程在写入时独占访问。

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

/* 共享资源 */
int shared_data = 0;

/* 读写锁 */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

/* 读线程函数:读取共享数据并打印 */
void* reader(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 3; ++i) {
        /* 加读锁 */
        pthread_rwlock_rdlock(&rwlock);
        
        /* 读取数据(临界区) */
        printf("读者 %d: 读取到数据 %d\n", id, shared_data);
        
        /* 解锁 */
        pthread_rwlock_unlock(&rwlock);
        
        /* 模拟读操作间隔 */
        usleep(500000);  // 0.5 秒
    }
    return NULL;
}

/* 写线程函数:修改共享数据并打印 */
void* writer(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 3; ++i) {
        /* 加写锁 */
        pthread_rwlock_wrlock(&rwlock);
        
        /* 写入数据(临界区) */
        shared_data++;
        printf("写者 %d: 将数据改为 %d\n", id, shared_data);
        
        /* 解锁 */
        pthread_rwlock_unlock(&rwlock);
        
        /* 模拟写操作间隔 */
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t readers[3], writers[2];
    int ids[5];  // 用于传递线程 ID

    /* 初始化读写锁(也可用静态初始化) */
    // pthread_rwlock_init(&rwlock, NULL);

    /* 创建 3 个读线程 */
    for (int i = 0; i < 3; ++i) {
        ids[i] = i + 1;
        pthread_create(&readers[i], NULL, reader, &ids[i]);
    }

    /* 创建 2 个写线程 */
    for (int i = 0; i < 2; ++i) {
        ids[3 + i] = i + 1;
        pthread_create(&writers[i], NULL, writer, &ids[3 + i]);
    }

    /* 等待所有线程结束 */
    for (int i = 0; i < 3; ++i) {
        pthread_join(readers[i], NULL);
    }
    for (int i = 0; i < 2; ++i) {
        pthread_join(writers[i], NULL);
    }

    /* 销毁读写锁 */
    pthread_rwlock_destroy(&rwlock);

    return 0;
}
  • 注意事项:如果写操作频繁,可能导致读线程"饥饿"(Starvation)。

四、自旋锁 (Spinlock)

自旋锁在无法获取锁时,会忙等待(Busy-Wait),即在一个循环中不断检查锁是否可用,而不会让出CPU。

  • 核心操作

  • 初始化pthread_spin_init()

  • 加锁pthread_spin_lock()

  • 解锁pthread_spin_unlock()

  • 销毁pthread_spin_destroy()

  • 适用场景锁持有时间极短,且线程不希望因上下文切换而增加开销的场景。

  • 下面是一个使用 pthread 自旋锁 (Spinlock) 的完整 C 语言示例。程序创建了两个线程,它们通过自旋锁来保护对共享计数器的并发访问,避免数据竞争。

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

/* 共享资源 */
long counter = 0;

/* 自旋锁变量 */
pthread_spinlock_t spinlock;

/* 线程函数:循环递增计数器 */
void* thread_func(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 1000000; ++i) {
        /* 加自旋锁 */
        pthread_spin_lock(&spinlock);

        /* 临界区:修改共享变量 */
        counter++;

        /* 解锁 */
        pthread_spin_unlock(&spinlock);
    }
    printf("线程 %d 完成工作\n", id);
    return NULL;
}

int main() {
    pthread_t threads[2];
    int ids[2] = {0, 1};

    /* 初始化自旋锁 */
    if (pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE) != 0) {
        perror("pthread_spin_init");
        exit(EXIT_FAILURE);
    }

    /* 创建两个线程 */
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_func, &ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    /* 等待线程结束 */
    for (int i = 0; i < 2; ++i) {
        pthread_join(threads[i], NULL);
    }

    printf("counter = %ld (期望值: 2000000)\n", counter);

    /* 销毁自旋锁 */
    pthread_spin_destroy(&spinlock);

    return 0;
}
特性 说明
忙等待 当锁被占用时,线程不会休眠,而是在一个循环中不断检查锁是否可用(消耗 CPU 时间)。
无上下文切换 不会进入内核态,因此加锁/解锁的开销极低(仅需几条原子指令)。
适用场景 锁持有时间极短,且线程不希望因阻塞而导致上下文切换开销。例如,保护简单变量的递增/递减、标志位的修改。
危险场景 如果锁持有时间较长,自旋锁会浪费大量 CPU 时间,导致整体性能下降(甚至比互斥锁更差)。
不可递归 同一个线程不能重复加锁,否则会死锁(因为自旋锁不区分持有者)。
  • pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE) 的第二个参数:

  • PTHREAD_PROCESS_PRIVATE:自旋锁仅在同一进程内的线程间共享(默认)。

  • PTHREAD_PROCESS_SHARED :自旋锁可放在共享内存中,用于跨进程同步(需要系统支持)。

  • ⚙️注意事项

  • 仅在多核/多处理器系统上使用 :在单核 CPU 上,自旋锁会浪费大量 CPU 时间,因为等待线程无法被调度,而锁的持有者也得不到执行,可能导致死锁(如果锁被其他线程持有,而该线程因优先级反转等原因无法运行,自旋线程会无限循环)。

  • 临界区必须极短:确保加锁和解锁之间的代码只做简单的内存操作,不包含系统调用、I/O 或复杂的计算。

  • 避免递归加锁 :自旋锁不会记录持有者,同一个线程再次 pthread_spin_lock 会导致死锁。

  • 注意内存顺序:自旋锁的实现包含完整的内存屏障,能保证临界区内的操作对其他 CPU 可见。

  • 可移植性 :自旋锁是 POSIX 标准的一部分(在 <pthread.h> 中定义),但某些嵌入式平台可能不支持,使用时需检查 _POSIX_SPIN_LOCKS 宏。

  • 注意事项:如果锁持有时间较长,自旋锁会浪费大量CPU时间,应避免使用。

五、屏障 (Barrier)

屏障用于同步一组线程 ,让它们共同等待,直到所有线程都到达屏障点后,才一起继续执行。

  • 核心操作

  • 初始化pthread_barrier_init(),需指定参与同步的线程总数。

  • 等待pthread_barrier_wait()。当调用次数达到初始化的总数时,所有线程被释放。

  • 销毁pthread_barrier_destroy()

  • 适用场景:需要多个线程在某个阶段"对齐",例如并行计算中的归约操作。

  • 注意事项 :调用 pthread_barrier_wait() 的线程数必须与初始化时设置的总数一致。

六、信号量 (Semaphore)

严格来说,信号量不属于 pthread 库的一部分,而是POSIX 信号量。它是一个计数器,可用于控制对资源的访问。

  • 核心操作

  • 初始化sem_init()

  • 等待(P操作)sem_wait()

  • 释放(V操作)sem_post()

  • 适用场景:不仅可用于线程同步,也可用于进程间同步,以及控制对有限资源(如连接池)的并发访问。


七、跨进程同步 (Process-Shared Synchronization)

以上所有机制(信号量除外)默认都是进程私有(PTHREAD_PROCESS_PRIVATE 的,即只能在同一进程内的线程间使用。

如果需要在不同进程 间共享同步机制(例如,通过共享内存),可以使用进程共享(PTHREAD_PROCESS_SHARED 属性。具体方法是:

  1. 将同步对象(如 pthread_mutex_t)放置在共享内存区域中。
  2. 在初始化时,通过特定的属性对象(attr)设置 pshared 属性为 PTHREAD_PROCESS_SHARED
  3. 不同进程中的线程便可以通过这个共享的同步对象来协调对共享内存的访问。