228.线程同步(互斥、读写、自旋锁)

线程同步是指在多线程编程中控制多个线程对共享资源的访问,以避免数据竞争和不一致性。

一、竞态条件和锁

(1)竞态条件

竞态条件 是指多个线程在不进行适当同步的情况下同时访问和操作共享资源,导致程序行为不可预测或不一致的问题。

竞态条件示例

假设有两个线程同时对全局变量 counter 进行操作:

#include <pthread.h>
#include <stdio.h>

int counter = 0;

void* increment_counter(void* arg) {
    for (int i = 0; i < 1000000; i++) {
        counter++;
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment_counter, NULL);
    pthread_create(&t2, NULL, increment_counter, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Counter: %d\n", counter);
    return 0;
}

期望的输出是 2000000,但由于竞态条件,实际输出可能小于 2000000

(2)如何避免竞态条件

上述程序如果想避免竞态条件,有下面两种解决方案:

  1. 避免多线程写入一个地址。
  2. 给资源加锁,使同一时间操作特定资源的线程只有一个。

方法1可以通过逻辑上组织业务逻辑实现,这里我们讲方法2。

想解决竞争问题,我们需要互斥锁——mutex。

(3)常见的锁机制

锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。包括上面的互斥锁在内,常见的锁机制共有三种:

  1. 互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
  2. 读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问是互斥的。
  3. 自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景,一般是Linux内核使用。

二、互斥锁

互斥锁保证一次只有一个线程能够持有锁,从而防止多个线程同时访问共享资源。

用途

  • 保护共享数据,避免同时被多个线程访问导致的数据不一致问题。
  • 实现线程间的同步,确保线程之间对共享资源的访问按照预定的顺序进行。
  • 操作
  • 初始化(pthread_mutex_init):创建互斥锁并初始化。
  • 锁定(pthread_mutex_lock):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。
  • 尝试锁定(pthread_mutex_trylock):尝试获取互斥锁。如果锁已被持有,立即返回而不是阻塞。
  • 解锁(pthread_mutex_unlock):释放互斥锁,使其可被其他线程获取。
  • 销毁(pthread_mutex_destroy):清理互斥锁资源。

使用互斥锁来解决上面的竞态条件问题:

#include <pthread.h>
#include <stdio.h>

int counter = 0;
pthread_mutex_t lock;

void* increment_counter(void* arg) {
    for (int i = 0; i < 1000000; i++) {
        pthread_mutex_lock(&lock); // 加锁
        counter++;
        pthread_mutex_unlock(&lock); // 解锁
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&lock, NULL); // 初始化互斥锁
    pthread_create(&t1, NULL, increment_counter, NULL);
    pthread_create(&t2, NULL, increment_counter, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_mutex_destroy(&lock); // 销毁互斥锁
    printf("Counter: %d\n", counter);
    return 0;
}

三、读写锁 

读写锁允许多个线程同时读取共享资源,但在写操作时,只有一个线程可以写且其他线程不能读或写。这在读操作多于写操作的情况下可以提高性能。

读操作:在读写锁的控制下,多个线程可以同时获得读锁。这些线程可以并发地读取共享资源,但它们的存在阻止了写锁的授予。

写操作:如果至少有一个读操作持有读锁,写操作就无法获得写锁。写操作将会阻塞,直到所有的读锁都被释放。

#include <pthread.h>
#include <stdio.h>

int shared_resource = 0;
pthread_rwlock_t rwlock;

void* reader(void* arg) {
    pthread_rwlock_rdlock(&rwlock); // 加读锁
    printf("Reader: %d\n", shared_resource);
    pthread_rwlock_unlock(&rwlock); // 解锁
    return NULL;
}

void* writer(void* arg) {
    pthread_rwlock_wrlock(&rwlock); // 加写锁
    shared_resource++;
    printf("Writer: %d\n", shared_resource);
    pthread_rwlock_unlock(&rwlock); // 解锁
    return NULL;
}

int main() {
    pthread_t readers[5], writers[5];
    pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁

    for (int i = 0; i < 5; i++) {
        pthread_create(&readers[i], NULL, reader, NULL);
        pthread_create(&writers[i], NULL, writer, NULL);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(readers[i], NULL);
        pthread_join(writers[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock); // 销毁读写锁
    return 0;
}

运行这段代码的输出结果是多个读线程和写线程同时打印出 shared_resource 的值。由于使用了读写锁,确保了:

  • 多个读线程可以同时读取 shared_resource,不会相互阻塞。
  • 写线程修改 shared_resource 时,其他线程不能读取或写入,确保了数据的一致性。

四、自旋锁

        在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。自旋锁相对于其他锁的优点是它们在锁被占用时会持续检查锁的状态(即“自旋”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免了线程上下文切换的开销。

自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用。

五、条件变量 

        条件变量是用于线程间通信的一种机制,允许一个或多个线程等待某个条件的发生,并由另一个线程发出信号来通知等待的线程条件已满足。条件变量通常与互斥锁一起使用,以确保对共享资源的访问是线程安全的。

说明:

  1. 使用条件变量时,通常涉及到一个或多个线程等待“条件变量”代表的条件成立,而另外一些线程在条件成立时触发条件变量。
  2. 条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的。
  3. 条件变量提供了一种线程间的通信机制,允许线程以无竞争的方式等待特定条件的发生。

用途

  • 允许线程等待特定条件的发生。当条件尚未满足时,线程通过条件变量等待,直到其他线程修改条件并通知条件变量。
  • 通知等待中的线程条件已改变,允许它们重新评估条件。

条件变量的基本操作

  • 初始化(pthread_cond_init):创建并初始化条件变量。
  • 等待(pthread_cond_wait):在给定的互斥锁上等待条件变量。调用时,线程将释放互斥锁并进入等待状态,直到被唤醒。
  • 定时等待(pthread_cond_timedwait):等待条件变量或直到超过指定的时间。
  • 信号(pthread_cond_signal):唤醒至少一个等待该条件变量的线程。
  • 广播(pthread_cond_broadcast):唤醒所有等待该条件变量的线程。
  • 销毁(pthread_cond_destroy):清理条件变量资源。

示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define BUFFER_SIZE 5 // 缓冲区大小
int buffer[BUFFER_SIZE]; // 缓冲区数组
int count = 0; // 当前缓冲区中的数据量

// 初始化互斥锁和条件变量
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
static pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;

// 生产者线程函数
void *producer(void *arg) {
    int item = 1; // 要生产的初始数据
    while (1) {
        pthread_mutex_lock(&mutex); // 获取互斥锁

        // 如果缓冲区已满,等待消费者消费数据
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_producer, &mutex); // 暂停生产者线程,等待被唤醒
        }

        // 将数据放入缓冲区
        buffer[count++] = item++;
        printf("Producer produced %d\n", buffer[count - 1]);

        // 通知消费者可以消费数据了
        pthread_cond_signal(&cond_consumer);
        pthread_mutex_unlock(&mutex); // 释放互斥锁

        sleep(1); // 模拟生产时间
    }
}

// 消费者线程函数
void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex); // 获取互斥锁

        // 如果缓冲区为空,等待生产者生产数据
        while (count == 0) {
            pthread_cond_wait(&cond_consumer, &mutex); // 暂停消费者线程,等待被唤醒
        }

        // 从缓冲区消费数据
        printf("Consumer consumed %d\n", buffer[--count]);

        // 通知生产者可以生产数据了
        pthread_cond_signal(&cond_producer);
        pthread_mutex_unlock(&mutex); // 释放互斥锁

        sleep(1); // 模拟消费时间
    }
}

int main() {
    pthread_t producer_thread, consumer_thread; // 线程标识符

    // 创建生产者和消费者线程
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);

    // 等待生产者和消费者线程结束
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    return 0;
}

可以看到producer线程生产数据,consumer线程消费数据,二者交替工作

  • 定义缓冲区大小和全局变量

    • BUFFER_SIZE 定义了缓冲区的大小。
    • buffer 是一个数组,用于存放生产者生成的数据。
    • count 表示当前缓冲区中的数据量。
  • 初始化互斥锁和条件变量

    • mutex 是一个互斥锁,用于保护对缓冲区的访问。
    • cond_producercond_consumer 是条件变量,用于在缓冲区满和空的情况下阻塞生产者和消费者线程。
  • 生产者线程函数

    • pthread_mutex_lock(&mutex):获取互斥锁,进入临界区。
    • pthread_cond_wait(&cond_producer, &mutex):如果缓冲区已满,生产者线程等待被消费者线程唤醒。
    • 将数据放入缓冲区,并增加 count
    • pthread_cond_signal(&cond_consumer):通知消费者线程可以消费数据了。
    • pthread_mutex_unlock(&mutex):释放互斥锁,离开临界区。
    • sleep(1):模拟生产时间。
  • 消费者线程函数

    • pthread_mutex_lock(&mutex):获取互斥锁,进入临界区。
    • pthread_cond_wait(&cond_consumer, &mutex):如果缓冲区为空,消费者线程等待被生产者线程唤醒。
    • 从缓冲区消费数据,并减少 count
    • pthread_cond_signal(&cond_producer):通知生产者线程可以生产数据了。
    • pthread_mutex_unlock(&mutex):释放互斥锁,离开临界区。
    • sleep(1):模拟消费时间。
  • 主函数

    • 创建生产者和消费者线程 (pthread_create)。
    • 等待生产者和消费者线程结束 (pthread_join)。

 

 

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清酒。233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值