【C语言系统编程】【第二部分:并发编程】2.2 线程同步

news2024/10/5 5:22:43
2.2 线程同步

在多线程编程中,线程同步是确保多个线程能安全地访问共享资源的重要技术。线程同步的主要目的是防止数据不一致以及竞争条件的发生。以下将介绍互斥锁的相关概念和使用方法。

2.2.1 互斥锁
  • 2.2.1.1 互斥锁的基本概念
  • 2.2.1.2 互斥锁使用方法(pthread_mutex_init, pthread_mutex_lock, pthread_mutex_unlock, pthread_mutex_destroy
  • 2.2.1.3 避免死锁的策略
  • 2.2.1.4 递归互斥锁
2.2.1.1 互斥锁的基本概念

互斥锁(Mutex)是一种用于实现线程间同步的机制,它能够确保在任意时刻只有一个线程能够访问特定的资源。这可以防止出现竞争条件,保证数据的一致性。

  • 作用:互斥锁用于保护共享资源,使得同一时间只有一个线程能持有锁并访问该资源。
  • 特点
    • 互斥锁通常用于短期锁定资源。例如,保护对共享变量的访问。
    • 互斥锁具有占用时间短的特点,避免长时间占用资源。
2.2.1.2 互斥锁使用方法

互斥锁的操作通常包括初始化、加锁、解锁和销毁。以下是POSIX线程库中互斥锁的使用方法:

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

pthread_mutex_t lock; // 声明互斥锁 [1]

void* thread_function(void* arg) {
    pthread_mutex_lock(&lock); // 获取锁 [2]
    // 临界区代码
    printf("Thread %ld: Inside critical section\n", (long)arg);
    pthread_mutex_unlock(&lock); // 释放锁 [3]
    return NULL;
}

int main() {
    pthread_t threads[2];

    pthread_mutex_init(&lock, NULL); // 初始化互斥锁 [4]

    for (long i = 0; i < 2; ++i) {
        pthread_create(&threads[i], NULL, thread_function, (void*)i); // 创建线程 [5]
    }

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

    pthread_mutex_destroy(&lock); // 销毁互斥锁 [7]

    return 0;
}
  • [1] 声明互斥锁:在程序开始的全局区域声明一个互斥锁实例。
  • [2] 获取锁:线程在进入临界区前调用 pthread_mutex_lock 函数获取锁。如果锁已经被其他线程持有,调用线程将进入阻塞状态,直到锁被释放。
  • [3] 释放锁:在完成对共享资源的操作后,使用 pthread_mutex_unlock 释放锁,使得其他阻塞的线程可以继续运行。
  • [4] 初始化互斥锁:在主函数中使用 pthread_mutex_init 函数初始化互斥锁。该函数通常在程序开始时被调用一次。
  • [5] 创建线程:使用 pthread_create 函数创建新线程,并传递线程函数 thread_function 以及参数。
  • [6] 等待线程完成:使用 pthread_join 函数等待线程执行完毕。
  • [7] 销毁互斥锁:在程序结束前使用 pthread_mutex_destroy 销毁互斥锁,以释放资源。
2.2.1.3 避免死锁的策略

死锁是指两个或两个以上的线程互相等待对方释放资源而进入的无限等待状态。避免死锁的策略主要包括:

  • 策略1:避免嵌套锁:尽量减少加锁的层级复杂度,避免嵌套锁。
  • 策略2:规定加锁顺序:为所有锁规定一个固定的获取顺序,确保所有线程按同样的顺序请求锁,避免相互等待。
  • 策略3:锁超时机制:使用带超时机制的锁请求,如果超时则放弃请求并采取合适的处理措施。
  • 策略4:谨慎地持锁:尽量缩短持有锁的时间,减少锁占用的时间窗口。
2.2.1.4 递归互斥锁

递归互斥锁允许同一线程多次获取同一个互斥锁,而不会造成死锁。这在一些特定场景下非常有用,例如递归函数中需要保护共享资源。

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

pthread_mutex_t recursive_lock; // 递归锁 [1]

// 定义递归函数,其中使用互斥锁
void recursive_function(int count) {
    if (count > 0) {
        pthread_mutex_lock(&recursive_lock); // 获取锁 [4]
        printf("Entering level %d\n", count);
        recursive_function(count - 1); // 递归调用 [5]
        printf("Exiting level %d\n", count);
        pthread_mutex_unlock(&recursive_lock); // 释放锁 [6]
    }
}

int main() {
    pthread_mutexattr_t attr; // 定义锁属性变量 [2]
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 设置锁属性为递归锁 [3]

    pthread_mutex_init(&recursive_lock, &attr); // 使用递归属性初始化互斥锁

    recursive_function(3); // 调用递归函数 [7]

    pthread_mutex_destroy(&recursive_lock); // 销毁互斥锁 [8]
    pthread_mutexattr_destroy(&attr); // 销毁属性对象 [9]

    return 0;
}
  • [1] 递归锁定义pthread_mutex_t recursive_lock 声明了一个pthread互斥锁变量。
  • [2] 锁属性变量pthread_mutexattr_t attr 用于存储互斥锁的属性设置。
  • [3] 设置锁属性为递归锁:使用 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) 将互斥锁的类型设置为递归,使得同一线程可以多次对该锁加锁而不会死锁。
  • [4] 获取锁:通过 pthread_mutex_lock(&recursive_lock) 加锁,以确保同一时间仅有一个线程能够进入该段代码。
  • [5] 递归调用recursive_function(count - 1) 用于递归调用,模拟不同层次的嵌套锁定。
  • [6] 释放锁:对应每次锁定调用 pthread_mutex_unlock(&recursive_lock)进行解锁。
  • [7] 调用递归函数:通过 recursive_function(3) 启动递归调用,开始测试递归锁。
  • [8] 销毁互斥锁:程序结束前使用 pthread_mutex_destroy(&recursive_lock) 销毁互斥锁以释放资源。
  • [9] 销毁属性对象:使用 pthread_mutexattr_destroy(&attr) 释放互斥锁属性对象,以避免内存泄漏。

这种实现允许同一个线程在一个函数的不同嵌套层次中多次获取同一把互斥锁,从而避免了死锁的发生。通过合理使用递归锁定,可以安全高效地同步多线程程序。

2.2.2 信号量
2.2.2.1 信号量的基本概念

信号量(Semaphore)是一种用于多线程同步的机制,用来控制对公共资源的访问。信号量本质上是一个计数器,可以用来解决变量共享造成的资源竞争问题。它们在并发编程中被广泛使用,尤其是在避免死锁和管理线程间的顺序执行上表现尤为突出。

  • 作用:控制对有限资源的访问,防止资源竞争和避免死锁。
  • 特点
    • 通过计数器机制管理资源访问。
    • 可以控制一个或多个资源的访问。
    • 常用于解决因多个线程共享资源而出现的同步问题。
2.2.2.2 POSIX信号量使用方法

POSIX信号量提供了一组标准的函数,这些函数操作起来相对简单,可以实现信号量的初始化、等待和释放操作。以下是相关函数及其具体用法:

  • sem_init:初始化一个未命名的信号量。
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    // sem: 信号量对象的指针
    // pshared: 0表示信号量用于线程间同步,非0表示用于进程间同步
    // value: 初始的计数值
    
  • sem_wait:等待信号量减为非零或被解锁。
    int sem_wait(sem_t *sem);
    // 等待sem的值为非零,之后将值减1。如果值为零,则阻塞直到值变为非零
    
  • sem_post:增加信号量计数值。
    int sem_post(sem_t *sem);
    // 将sem的值增加1,唤醒等待的线程
    
  • sem_destroy:销毁信号量。
    int sem_destroy(sem_t *sem);
    // 释放信号量相关的资源
    
2.2.2.3 有名信号量与无名信号量

信号量分为有名信号量和无名信号量两种:

  • 有名信号量:可以在进程间共享,使用文件系统路径进行唯一标识,通过sem_open创建。

    sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
    // name: 信号量的名字(路径)
    // oflag: 用于标识创建(O_CREAT)和初始化(O_EXCL)新信号量
    // mode: 信号量的权限
    // value: 初始的计数值
    
  • 无名信号量:只能在线程间共享,直接使用sem_init初始化。

    int sem_init(sem_t *sem, int pshared, unsigned int value);
    // 如上
    
2.2.2.4 应用场景和实际案例

信号量在许多实际应用中非常重要,以下举两个常见的应用案例:

  1. 限流器(Rate Limiter)
    在控制资源的访问速率时可以使用信号量来作为限流器。例如,一个网络服务器可能限制每秒处理的请求数量:

    #include <semaphore.h>   // 信号量库 [1]
    #include <pthread.h>     // 线程库 [2]
    #include <stdio.h>
    
    // 定义信号量用于请求限流
    sem_t rate_limiter;
    
    // 函数 handle_request:处理请求
    void* handle_request(void* arg) {
        sem_wait(&rate_limiter);   // 尝试进入临界区 [3]
        // 处理请求的逻辑部分 [4]
        sem_post(&rate_limiter);   // 离开临界区 [5]
        return NULL;
    }
    
    int main() {
        sem_init(&rate_limiter, 0, 10); // 初始化信号量,初始值为10 [6]
    
        pthread_t threads[20];
        for (int i = 0; i < 20; i++) {
            pthread_create(&threads[i], NULL, handle_request, NULL); // 创建线程 [7]
        }
        for (int i = 0; i < 20; i++) {
            pthread_join(threads[i], NULL); // 等待线程结束 [8]
        }
        sem_destroy(&rate_limiter); // 销毁信号量 [9]
        return 0;
    }
    
    • [1] 信号量库 (<semaphore.h>):提供信号量类 sem_t 及其操作函数。信号量通常用于解决同步问题。
    • [2] 线程库 (<pthread.h>):支持POSIX标准线程操作,包含线程创建、联接等操作。
    • [3] 信号量等待 (sem_wait):尝试将信号量值减一,当值为0时等待,有资源时进入临界区。
    • [4] 请求处理逻辑:用户逻辑代码实现部分,在线程获取资源后执行。
    • [5] 信号量释放 (sem_post):信号量值加一,标记临界区资源已被释放。
    • [6] 信号量初始化 (sem_init)
      • 初始化信号量rate_limiter
      • 0 为用于线程同步(非进程间)。
      • 初始值 10 限制同一时刻最大并发数为10。
    • [7] 创建线程 (pthread_create):创建20个线程尝试同时处理请求。
    • [8] 等待线程结束 (pthread_join):确保程序在主线程退出前所有子线程执行完毕。
    • [9] 信号量销毁 (sem_destroy):释放信号量资源,防止内存泄露。

此代码使用信号量作为限流机制,每次最多允许10个请求同时处理。通过信号量控制请求的同时进行数量,保证资源的有限分配。

  1. 生产者-消费者模型
    信号量在生产者-消费者模型中用于管理生产者和消费者之间的同步。如下示例:

    #include <pthread.h>
    #include <semaphore.h>
    #include <stdio.h>
    
    #define BUFFER_SIZE 10
    
    int buffer[BUFFER_SIZE]; // 缓冲区 [1]
    int count = 0; // 当前缓冲区中元素数量
    sem_t empty_slots; // 信号量:空闲槽位 [2]
    sem_t filled_slots; // 信号量:填充槽位 [3]
    pthread_mutex_t buffer_mutex; // 互斥锁,保护缓冲区 [4]
    
    // 生产者线程函数
    void* producer(void* arg) {
        for (int i = 0; i < 20; i++) {
            sem_wait(&empty_slots); // 等待空位 [5]
            pthread_mutex_lock(&buffer_mutex); // 加锁
            buffer[count++] = i; // 生产 [6]
            pthread_mutex_unlock(&buffer_mutex); // 解锁
            sem_post(&filled_slots); // 通知有新数据 [7]
        }
        return NULL;
    }
    
    // 消费者线程函数
    void* consumer(void* arg) {
        for (int i = 0; i < 20; i++) {
            sem_wait(&filled_slots); // 等待有数据 [8]
            pthread_mutex_lock(&buffer_mutex); // 加锁
            int item = buffer[--count]; // 消费 [9]
            pthread_mutex_unlock(&buffer_mutex); // 解锁
            sem_post(&empty_slots); // 通知有新空位 [10]
            printf("Consumed: %d\n", item);
        }
        return NULL;
    }
    
    int main() {
        sem_init(&empty_slots, 0, BUFFER_SIZE); // 初始化空闲槽位计数信号量 [11]
        sem_init(&filled_slots, 0, 0); // 初始化填充槽位计数信号量 [12]
        pthread_mutex_init(&buffer_mutex, NULL); // 初始化互斥锁 [13]
    
        pthread_t prod, cons;
        pthread_create(&prod, NULL, producer, NULL); // 创建生产者线程 [14]
        pthread_create(&cons, NULL, consumer, NULL); // 创建消费者线程 [15]
    
        pthread_join(prod, NULL); // 等待生产者线程结束 [16]
        pthread_join(cons, NULL); // 等待消费者线程结束 [17]
    
        sem_destroy(&empty_slots); // 销毁信号量 [18]
        sem_destroy(&filled_slots); // 销毁信号量
        pthread_mutex_destroy(&buffer_mutex); // 销毁互斥锁
    
        return 0;
    }
    
    • [1] 缓冲区:这里定义了一个固定大小的整数数组 buffer,用于存储生产者生产的数据。
    • [2] 信号量:空闲槽位sem_t empty_slots 用来跟踪缓冲区中的空闲槽位数量。
    • [3] 信号量:填充槽位sem_t filled_slots 用来跟踪缓冲区中的已填充槽位数量。
    • [4] 互斥锁pthread_mutex_t buffer_mutex 用于保护对共享资源 buffercount 的访问,避免数据竞争。
    • [5] 等待空位:生产者线程尝试获取一个空闲槽位,若无空闲槽位则等待。
    • [6] 生产:在持有锁的情况下,将数据写入缓冲区并更新计数。
    • [7] 通知有新数据:生产完后,通知消费者有新的数据可消费。
    • [8] 等待有数据:消费者线程尝试获取一个填充槽位,若无填充槽位则等待。
    • [9] 消费:消费数据时,同样需要锁定缓冲区避免其他线程干扰。
    • [10] 通知有新空位:消费后释放一个空闲槽位。
    • [11] 初始化空闲槽位信号量:信号量初始值为 BUFFER_SIZE 表示缓冲区最初全空。
    • [12] 初始化填充槽位信号量:信号量初始值为 0 表示缓冲区最初无数据。
    • [13] 初始化互斥锁:为缓冲区访问准备互斥锁。
    • [14] 创建生产者线程:启动生产者线程以进行数据生产。
    • [15] 创建消费者线程:启动消费者线程以进行数据消费。
    • [16], [17]线程同步:使用 pthread_join() 以确保生产者与消费者线程都已完成执行。
    • [18] 资源销毁:信号量和互斥锁在程序结束时需要销毁以释放资源。

以上内容详细介绍了信号量的基本概念、使用方法、分类以及实际应用场景,有助于理解和掌握信号量在多线程编程中的重要性和使用技巧。

2.2.3 条件变量
2.2.3.1 条件变量的基本概念

条件变量是用于进程或线程间同步机制的一种方式,允许一个或多个线程等待某个条件成立而进行同步。条件变量通常与互斥锁结合使用,以避免竞态条件(数据竞争)。

  • 作用:使线程可以高效地等待某个条件的发生。
  • 特点
    • 允许线程在等待条件成立时挂起,避免无效的轮询。
    • 条件变量的等待操作通常配合互斥锁以避免竞态条件。
  • 常用情况:生产者-消费者问题、事件等待机制等。
2.2.3.2 条件变量使用方法

以下是使用条件变量的基本方法:

  1. 初始化条件变量:在使用条件变量之前,需要首先初始化它。

    pthread_cond_t cond;
    pthread_cond_init(&cond, NULL);
    
  2. 等待条件变量:当某个线程需要等待某个条件时,它可以调用 pthread_cond_wait。在此函数调用时,必须已经持有相关的互斥锁。

    pthread_mutex_t mutex;                      // 定义互斥量 [1]
    pthread_mutex_lock(&mutex);                 // 加锁互斥量 [2]
    
    while (condition_not_met) {
        pthread_cond_wait(&cond, &mutex);       // 等待条件变量 [3]
    }
    
    // 条件满足,进入临界区代码 [4]
    pthread_mutex_unlock(&mutex);               // 解锁互斥量 [5]
    
    • [1] 定义互斥量pthread_mutex_t mutex 定义了一个互斥量,用于实现线程间的互斥访问。
    • [2] 加锁互斥量pthread_mutex_lock() 用于锁定互斥量 mutex,使当前线程进入该锁定区域与其他线程互斥。
      • 作用:确保只有一个线程能进入临界区代码,从而避免数据竞争。
    • [3] 等待条件变量pthread_cond_wait(&cond, &mutex) 用于阻塞当前线程,直至接收到信号并条件满足。
      • 详细解释
        • 调用该函数时,线程会自动释放持有的 mutex 锁并进入等待状态。
        • 当条件变量 cond 发出信号,例如通过调用 pthread_cond_signal(),线程会被唤醒。此时它尝试重新获得 mutex 锁以继续执行。
    • [4] 条件满足,进入临界区代码:当 condition_not_met 不再成立,说明条件满足,线程继续执行临界区代码。
    • [5] 解锁互斥量pthread_mutex_unlock() 用于解锁互斥量 mutex,使其他被阻塞的线程可以获得此锁继续执行。
      • 注意:始终在离开临界区后解锁,以保持互斥保护。

    上下文说明
    在多线程编程中,互斥锁 pthread_mutex_t 和条件变量 pthread_cond_t 是常用的同步机制。互斥锁用于保护共享数据,而条件变量用于在线程之间协调状态。上述代码片段示例实现了线程对条件状态的等待及锁的使用。

  3. 发出信号:当某个线程满足条件时,它可以通过 pthread_cond_signalpthread_cond_broadcast 通知等待该条件的一个或多个线程:

    pthread_mutex_lock(&mutex);
    
    // Modify the condition
    condition_met = 1;
    
    // Notify one waiting thread
    pthread_cond_signal(&cond);
    
    pthread_mutex_unlock(&mutex);
    
    • 知识点详解
    1. 互斥锁(Mutex)

      • 用于保护共享数据,防止多个线程同时修改同一数据,从而造成数据不一致的情况。
      • pthread_mutex_lock(&mutex);:加锁,进入临界区操作。若mutex已经被其它线程锁定,调用线程会阻塞直到该mutex可用。
    2. 修改条件

      • condition_met = 1;:在互斥锁保护下修改共享条件。确保只有一个线程能够访问和修改 condition_met 以至于避免竞争条件。
    3. 条件变量(Condition Variable)

      • 用于线程间的通知机制,使一个线程可以通知其它正在等待某个条件的线程其状态已经改变。
      • pthread_cond_signal(&cond);:唤醒一个正等待 cond 条件变量的线程。该函数表示条件发生变化需要通知等待线程继续执行。如果没有线程在等待,该信号会被忽略。
    4. 解锁互斥锁

      • pthread_mutex_unlock(&mutex);:释放互斥锁,其他被阻塞的线程可以继续操作该临界区。当一个线程完成了到共享数据的控制操作后,解锁以允许其他被阻塞的线程获得该锁展开后续操作。
    • 应用案例:多个线程协作完成某项任务,以及确保线程间数据同步的一般模式。在多线程编程中,通过保护共享资源,保证数据和状态的一致性与正确性。

    注意:使用 pthread_cond_signal 只能唤醒一个等待线程,而 pthread_cond_broadcast 可以唤醒所有等待该条件变量的线程。根据实际需求选择相应的策略,以避免不必要的上下文切换和资源争用。

  4. 销毁条件变量:在不再需要条件变量时,应该将其销毁以释放资源。

    pthread_cond_destroy(&cond);
    
2.2.3.3 条件变量与互斥锁的配合

条件变量通常与互斥锁一起使用,以确保在等待和信号操作之间没有竞态条件。以下是一个结合使用的简单示例:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h> // 包含 sleep 函数

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁 [1]
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;   // 初始化条件变量 [2]
int condition_met = 0;                            // 条件标志 [3]

// 线程函数
void *thread_func(void *arg) {
    pthread_mutex_lock(&mutex);                    // 获取互斥锁 [4]

    while (!condition_met) {
        pthread_cond_wait(&cond, &mutex);          // 等待条件满足 [5]
    }

    // 条件满足后进行操作
    printf("Condition met, thread proceeding.\n");

    pthread_mutex_unlock(&mutex);                  // 释放互斥锁 [6]
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL); // 创建线程 [7]

    // 模拟某些操作
    sleep(1);

    pthread_mutex_lock(&mutex);                    // 获取互斥锁 [8]
    condition_met = 1;                             // 修改条件标志 [9]
    pthread_cond_signal(&cond);                    // 发出条件信号 [10]
    pthread_mutex_unlock(&mutex);                  // 释放互斥锁 [11]

    pthread_join(thread, NULL);                    // 等待线程结束 [12]
    pthread_mutex_destroy(&mutex);                 // 销毁互斥锁 [13]
    pthread_cond_destroy(&cond);                   // 销毁条件变量 [14]
    return 0;
}
  • [1] 初始化互斥锁pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 初始化一个互斥锁用于保护共享数据。
  • [2] 初始化条件变量pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 初始化条件变量,用于线程同步。
  • [3] 条件标志int condition_met = 0; 用于标记条件是否满足,初始值为0表示条件未满足。
  • [4] 获取互斥锁:通过 pthread_mutex_lock(&mutex); 锁定互斥锁,防止其他线程访问共享数据。
  • [5] 等待条件满足pthread_cond_wait(&cond, &mutex); 阻塞线程,直到接收到条件信号。此函数自动解锁互斥锁并等待,当条件满足后重新锁定。
  • [6] 释放互斥锁:当条件满足后,通过 pthread_mutex_unlock(&mutex); 释放互斥锁。
  • [7] 创建线程pthread_create(&thread, NULL, thread_func, NULL); 创建一个新线程,执行 thread_func 函数。
  • [8] 获取互斥锁:在主线程中锁定互斥锁,准备修改共享变量。
  • [9] 修改条件标志condition_met = 1; 改变条件标志,表示条件已满足。
  • [10] 发出条件信号pthread_cond_signal(&cond); 向等待在条件变量上的线程发送信号,告诉它条件已满足。
  • [11] 释放互斥锁:当条件标志修改完成后释放互斥锁。
  • [12] 等待线程结束:使用 pthread_join(thread, NULL); 等待创建的线程完成执行。
  • [13] 销毁互斥锁pthread_mutex_destroy(&mutex); 销毁互斥锁以释放资源。
  • [14] 销毁条件变量pthread_cond_destroy(&cond); 销毁条件变量以释放资源。

此示例程序展示了基本的线程同步机制,使用互斥锁和条件变量来协调线程的执行顺序。线程在等待某个条件满足前会先等待并阻塞在条件变量上,而主线程通过条件变量信号来通知条件已经满足,以便唤醒线程继续执行指定的任务。

2.2.3.4 应用场景和实际案例

应用场景

  1. 生产者-消费者模型:生产者用条件变量通知消费者有新数据可用,消费者用条件变量通知生产者缓冲区可以被写入。
  2. 事件等待机制:线程等待某个特定事件的发生,例如文件准备好进行读写等。
  3. 线程池:线程池中的线程等待任务队列中有新的任务提交。

实际案例

这个程序实现了一个基本的生产者-消费者模型。在该模型中,生产者线程负责产生数据并将其放到缓冲区中,而消费者线程负责消费缓冲区中的数据。通过使用互斥锁(mutex)和条件变量(condition variable)来同步生产者和消费者,以确保数据的安全访问和条件协调。

  • 关键组件

    • 缓冲区int buffer[BUFFER_SIZE] 定义了一个大小为 BUFFER_SIZE 的整数数组,作为共享数据缓冲区。
    • 计数器int count 用来跟踪当前缓冲区中的有效数据个数。
  • 线程同步

    • 互斥锁 (pthread_mutex_t):用于保护共享资源(缓冲区和计数器)的同时访问,防止数据竞争。
    • 条件变量 (pthread_cond_t):用于线程间的条件控制,同步生产者和消费者之间的数据读取和写入操作。
void *producer(void *arg) {
    int data = 1;

    while (1) {
        pthread_mutex_lock(&mutex);  // 锁定互斥锁 [1]

        while (count == BUFFER_SIZE) {  // 缓冲区已满 [2]
            pthread_cond_wait(&cond_producer, &mutex);  // 等待,释放锁
        }

        buffer[count++] = data++;  // 放入数据 [3]
        printf("Produced: %d\n", data);

        pthread_cond_signal(&cond_consumer);  // 通知消费者 [4]
        pthread_mutex_unlock(&mutex);  // 解锁互斥锁 [5]

        sleep(1);  // 模拟生产时间
    }
    return NULL;
}
  • [1] 锁定互斥锁:在访问共享资源(缓冲区与计数器)前,生产者线程会锁定 mutex
  • [2] 缓冲区已满:如果缓冲区已满,生产者线程会等待 cond_producer 条件变量,同时释放 mutex 以让消费者线程运行。
  • [3] 放入数据:生产者将数据放入缓冲区,并增加 count
  • [4] 通知消费者:在缓冲区放入数据后,生产者通过 cond_consumer 条件变量通知消费者可能有数据可供消费。
  • [5] 解锁互斥锁:操作完成后,生产者解锁 mutex 以释放共享资源给其他线程。
void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);  // 锁定互斥锁 [6]

        while (count == 0) {  // 缓冲区为空 [7]
            pthread_cond_wait(&cond_consumer, &mutex);  // 等待,释放锁
        }

        int data = buffer[--count];  // 取出数据 [8]
        printf("Consumed: %d\n", data);

        pthread_cond_signal(&cond_producer);  // 通知生产者 [9]
        pthread_mutex_unlock(&mutex);  // 解锁互斥锁 [10]

        sleep(2);  // 模拟消费时间
    }
    return NULL;
}
  • [6] 锁定互斥锁:在访问共享资源前,消费者线程会锁定 mutex

  • [7] 缓冲区为空:如果缓冲区为空,消费者线程会等待 cond_consumer 条件变量,同时释放 mutex 以让生产者线程运行。

  • [8] 取出数据:消费者从缓冲区取得可用数据,减少 count

  • [9] 通知生产者:在取出数据后,消费者通过 cond_producer 条件变量通知生产者可能有空间可供生产。

  • [10] 解锁互斥锁:操作完成后,消费者解锁 mutex

  • 主程序

    • main 函数中创建并启动生产者和消费者线程。
    • 使用 pthread_join 等待线程结束(尽管在本程序中线程不会结束)。
    • 销毁互斥锁和条件变量,以释放内核资源。

通过这样的同步模型,生产者和消费者能够安全并有效地共享数据缓冲区,实现并发环境下的数据生产与消费。

在上述示例中,生产者线程和消费者线程使用条件变量进行同步,以确保当缓冲区满或空时能够正确地等待和通知对方。这样就避免了对共享资源(缓冲区)的竞态条件,从而实现了高效的多线程协作。

2.2.4 读写锁

在多线程编程中,有时需要一个机制来允许多个线程同时读取数据而不互相阻塞,但在写数据时需要独占锁。这时候,读写锁(reader-writer lock)就非常有用。读写锁允许多个线程同时读,但如果有线程在写,则其他线程必须等待读操作和写操作完成。

2.2.4.1 读写锁的基本概念

读写锁是一种同步机制,分为读模式和写模式。

  • 读模式:多个读线程可以同时获取读锁,允许并发读取数据,读操作之间彼此不阻塞。
  • 写模式:只有一个写线程能够获取写锁,当写线程获取写锁后,所有其他读线程和写线程都将被阻塞,直至写锁被释放。
2.2.4.2 读写锁使用方法

读写锁的API函数在POSIX线程库中定义,以下是常用的基本操作:

  • 初始化读写锁pthread_rwlock_init
  • 获取读锁pthread_rwlock_rdlock
  • 获取写锁pthread_rwlock_wrlock
  • 释放读写锁pthread_rwlock_unlock
  • 销毁读写锁pthread_rwlock_destroy

以下是一个简单的读写锁使用示例:

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

// 声明全局变量和读写锁
int shared_data = 0;                 // 共享数据 [1]
pthread_rwlock_t rwlock;             // 读写锁 [2]

// 读线程函数
void *reader(void *arg) {
    pthread_rwlock_rdlock(&rwlock);   // 加读锁 [3]
    printf("Reader: shared_data = %d\n", shared_data);  // 读取共享数据
    pthread_rwlock_unlock(&rwlock);   // 解锁 [4]
    return NULL;
}

// 写线程函数
void *writer(void *arg) {
    pthread_rwlock_wrlock(&rwlock);   // 加写锁 [5]
    shared_data++;                    // 修改共享数据
    printf("Writer: modified shared_data to %d\n", shared_data);
    pthread_rwlock_unlock(&rwlock);   // 解锁 [6]
    return NULL;
}

int main() {
    pthread_t r1, r2, w1;
    
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);  // 初始化读写锁 [7]
    
    // 创建读线程和写线程
    pthread_create(&r1, NULL, reader, NULL);  // 创建第一个读线程 [8]
    pthread_create(&r2, NULL, reader, NULL);  // 创建第二个读线程 [9]
    pthread_create(&w1, NULL, writer, NULL);  // 创建写线程 [10]
    
    // 等待所有线程完成
    pthread_join(r1, NULL);  // 等待读线程 r1 完成 [11]
    pthread_join(r2, NULL);  // 等待读线程 r2 完成 [12]
    pthread_join(w1, NULL);  // 等待写线程 w1 完成 [13]
    
    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);  // 销毁读写锁 [14]
    
    return 0;
}
  • [1] 共享数据int shared_data是用于线程间共享和保护的全局变量。
  • [2] 读写锁pthread_rwlock_t rwlock是一个读写锁,保证对共享数据的多线程访问安全。
  • [3] 加读锁pthread_rwlock_rdlock(&rwlock)用于在读取数据前加读锁,允许多个读者,但阻止写者。
  • [4] 释放读锁pthread_rwlock_unlock(&rwlock)在读取数据完成后释放锁。
  • [5] 加写锁pthread_rwlock_wrlock(&rwlock)用于在修改数据前加写锁,保证互斥访问。
  • [6] 释放写锁:在完成对共享数据的修改后,释放写锁。
  • [7] 初始化读写锁pthread_rwlock_init(&rwlock, NULL)初始化读写锁,准备进入多线程环境。
  • [8-10] 创建线程pthread_create函数创建读线程和写线程,分别执行读和写操作。
  • [11-13] 等待线程完成:使用pthread_join确保主程序在继续执行前等待所有线程完成。
  • [14] 销毁读写锁:程序结束前销毁读写锁,释放资源。
2.2.4.3 应用场景和实际案例

读写锁非常适合以下场景:

  • 多读少写:当读操作远多于写操作时,读写锁能显著提高程序的并发性和性能。
  • 数据库系统:在数据库系统中,查询操作明显多于更新操作,使用读写锁可以提升并发查询性能。
  • 缓存系统:当读取缓存数据的频率高于写入频率时,使用读写锁能够提高读效率。

一个实际案例是文件读写系统,例如一个多线程日志系统:

  • 日志记录:在这个日志系统示例中,使用读写锁(pthread_rwlock_t)来管理多个线程对共享资源(log_data)的访问,确保数据的一致性和正确性。下面是程序的详细解析:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define LOG_SIZE 1024
char log_data[LOG_SIZE];                 // 日志数据缓冲区 [1]
pthread_rwlock_t log_rwlock;             // 读写锁 [2]

// 读线程函数
void *log_reader(void *arg) {
    pthread_rwlock_rdlock(&log_rwlock);  // 获取读锁 [3]
    printf("Read log: %s\n", log_data);
    pthread_rwlock_unlock(&log_rwlock);  // 释放锁 [4]
    return NULL;
}

// 写线程函数
void *log_writer(void *arg) {
    pthread_rwlock_wrlock(&log_rwlock);   // 获取写锁 [5]
    snprintf(log_data, LOG_SIZE, "Thread %ld wrote to log.", (long)arg);
    printf("Write log: %s\n", log_data);
    pthread_rwlock_unlock(&log_rwlock);   // 释放锁 [6]
    return NULL;
}

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

    // 创建读线程
    for (long i = 0; i < 5; i++) {
        pthread_create(&readers[i], NULL, log_reader, (void *)i); // 创建读线程 [8]
    }

    // 创建写线程
    for (long i = 0; i < 2; i++) {
        pthread_create(&writers[i], NULL, log_writer, (void *)i); // 创建写线程 [9]
    }

    // 等待所有线程完成
    for (int i = 0; i < 5; i++) {
        pthread_join(readers[i], NULL); // 等待读线程完成 [10]
    }
    for (int i = 0; i < 2; i++) {
        pthread_join(writers[i], NULL); // 等待写线程完成 [11]
    }

    pthread_rwlock_destroy(&log_rwlock); // 销毁读写锁 [12]
    return 0;
}
  • [1] 日志数据缓冲区char log_data[LOG_SIZE] 定义了一个用于存储日志信息的缓冲区,大小为1024字节。
  • [2] 读写锁pthread_rwlock_t log_rwlock 是一个读写锁,用于同步对日志的访问。
  • [3] 获取读锁pthread_rwlock_rdlock(&log_rwlock) 在读取日志时获取读锁,允许多个读线程同时读取。
  • [4] 释放锁pthread_rwlock_unlock(&log_rwlock) 释放获得的读锁或写锁。
  • [5] 获取写锁pthread_rwlock_wrlock(&log_rwlock) 在修改日志时获取写锁,确保只有一个线程可以写入。
  • [6] 释放锁:与[4]相同,用于释放写锁。
  • [7] 初始化读写锁pthread_rwlock_init(&log_rwlock, NULL) 初始化读写锁 log_rwlock
  • [8] 创建读线程pthread_create(&readers[i], NULL, log_reader, (void *)i) 启动多个读线程读取日志。
  • [9] 创建写线程pthread_create(&writers[i], NULL, log_writer, (void *)i) 启动写线程来写入日志。
  • [10] 等待读线程完成pthread_join(readers[i], NULL) 等待每个读线程完成执行。
  • [11] 等待写线程完成pthread_join(writers[i], NULL) 等待每个写线程完成执行。
  • [12] 销毁读写锁pthread_rwlock_destroy(&log_rwlock) 销毁读写锁 log_rwlock,释放系统资源。

通过读写锁,可以实现读写并发控制,允许多个线程同时读取数据,而写操作则是排他的,保证只有一个线程能进行写入操作。这种机制确保了数据的一致性,同时最大化了并发性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189328.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pikachu-Unsafe FileUpload-客户端check

上传图片&#xff0c;点击查看页面的源码&#xff0c; 可以看到页面的文件名校验是放在前端的&#xff1b;而且也没有发起网络请求&#xff1b; 所以&#xff0c;可以通过直接修改前端代码&#xff0c;删除 checkFileExt(this.value) 这部分&#xff1b; 又或者先把文件名改成…

九、2 USART串口外设

1、STM32内部的USART外设的介绍 &#xff08;1&#xff09; STM32的USART的同步模式只是多了个时钟输出&#xff0c;只支持时钟输出&#xff0c;不支持时钟输入。该同步模式更多是为了兼容别的协议或者特殊用途而设计的&#xff0c;并不支持两个USART之间进行同步通信&#xf…

剖解最小栈

最小栈 思路&#xff1a; 1. 首先实例化两个栈&#xff0c;分别是stack用于存放数据&#xff0c;minstack用于存放最小值 2. 将第一个元素压入两个栈中&#xff0c;判断此时若minStack栈中为空&#xff0c;则表示压入的为第一个数据 if ( minStack.empty () ) { minStack.pus…

MySQL 查询优化器

文章目录 控制查询计划optimizer_prune_leveloptimizer_search_depth 优化器参数优化器提示索引提示成本模型server_costcost_name engine_cost 控制查询计划 https://dev.mysql.com/doc/refman/8.4/en/controlling-query-plan-evaluation.html 在执行SQL前会根据优化器选择执…

Leetcode 第 140 场双周赛题解

Leetcode 第 140 场双周赛题解 Leetcode 第 140 场双周赛题解题目1&#xff1a;3300. 替换为数位和以后的最小元素思路代码复杂度分析 题目2&#xff1a;3301. 高度互不相同的最大塔高和思路代码复杂度分析 题目3&#xff1a;3302. 字典序最小的合法序列思路代码复杂度分析 题目…

入手一个小扒菜fqrr#com

fqrr#com 既带q又带r是很多人不喜的类型&#xff0c; 父亲 夫妻 番茄 分期 人人 日日 好无聊的米呀&#xff0c;竟然组合不出来意思 这个不是购买的&#xff0c;别人说他1150元购买的&#xff0c;算是半抵给我的吧 其实我也不喜欢&#xff0c;我4声母.com 已经够多了&am…

【教程】文字转语音的3个方法,文字转语音使用攻略

文字转语音的需求还是蛮多的&#xff0c;很多用户在视频剪辑中会遇到。不想用本人的声音&#xff0c;那么视频中的旁白就只能通过文字转语音软件实现了。 想要将文字转为语音那还是蛮好解决的&#xff0c;如果你还在找方法&#xff0c;那么以下内容可以了解下。本文整理了三种简…

2c 操作符详解

1. 操作符分类&#xff1a; 算术操作符 移位操作符 位操作符 赋值操作符 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员 2. 算术操作符 - * / % 1除了 % 操作符之外&#xff0c;其他的几个操作符可以作用于整数和浮点数。对于 / 操作…

NVIDIA NVLink-C2C

NVIDIA NVLink-C2C 文章目录 前言一、介绍1. 用于定制芯片集成的超快芯片互连技术2. 构建半定制芯片设计3. 使用 NVLink-C2C 技术的产品 二、NVLink-C2C 技术优势1. 高带宽2. 低延迟3. 低功率和高密度4. 行业标准协议 前言 将 NVLink 扩展至芯片级集成 一、介绍 1. 用于定制芯…

Candance仿真二阶米勒补偿OTA

1.OTA电路搭建目标——25Mhz GBW&#xff0c;65dB的增益 2.电路参照 3.candance电路搭建 实现步骤&#xff1a;应该是从下面这个公式开始推导 然后那个CL就是两边的那个CCa或CCb的大小 算出来就是gm75us

MongoDB-aggregate流式计算:带条件的关联查询使用案例分析

在数据库的查询中&#xff0c;是一定会遇到表关联查询的。当两张大表关联时&#xff0c;时常会遇到性能和资源问题。这篇文章就是用一个例子来分享MongoDB带条件的关联查询发挥的作用。 假设工作环境中有两张MongoDB集合&#xff1a;SC_DATA&#xff08;学生基本信息集合&…

基于微信小程序的旅游拼团系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

Colorize: 0 variables Colorize is not activated for this file. VsCode

问题情况 解决步骤 1.找到setting.json文件 2.输入以下代码&#xff0c;保存setting.json文件 "colorize.languages": ["css", "javascript", "sass", "less", "postcss", "stylus", "xml"…

基于SpringBoot+Vue+MySQL的中医院问诊系统

系统展示 用户前台界面 管理员后台界面 医生后台界面 系统背景 随着信息技术的迅猛发展和医疗服务需求的不断增加&#xff0c;传统的中医院问诊流程已经无法满足患者和医院的需求。纸质病历不仅占用大量存储空间&#xff0c;而且容易丢失和损坏&#xff0c;同时难以实现信息的快…

螺蛳壳里做道场:老破机搭建的私人数据中心---Centos下Docker学习04(环境准备)

4 创建docker容器 4.1创建网络 [rootlocalhost wutool]# docker network create -d macvlan --subnet192.168.137.0/24 --gateway192.168.137.2 --ip-range192.168.137.0/24 -o parentens33 nat 52af11381bfd655d175e4168265b2a507793e8fe48f119db846949ffd4dd27de [rootlocal…

【每天学个新注解】Day 15 Lombok注解简解(十四)—@UtilityClass、@Helper

UtilityClass 生成工具类的注解 将一个类通过注解变成一个工具类&#xff0c;并没有什么用&#xff0c;本来代码中的工具类数量就极为有限&#xff0c;并不能达到减少重复代码的目的 1、如何使用 加在需要委托将其变为工具类的普通类上。 2、代码示例 例&#xff1a; Uti…

设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)

文章目录 设计模式概述1、原型模式2、原型模式的使用场景3、优点4、缺点5、主要角色6、代码示例7、总结题外话关于使用序列化实现深拷贝 设计模式概述 创建型模式&#xff1a;工厂方法、抽象方法、建造者、原型、单例。 结构型模式有&#xff1a;适配器、桥接、组合、装饰器、…

构建高效新闻推荐系统:Spring Boot的力量

1系统概述 1.1 研究背景 如今互联网高速发展&#xff0c;网络遍布全球&#xff0c;通过互联网发布的消息能快而方便的传播到世界每个角落&#xff0c;并且互联网上能传播的信息也很广&#xff0c;比如文字、图片、声音、视频等。从而&#xff0c;这种种好处使得互联网成了信息传…

MacBook远程连接服务器,显示tensorboard的loss值

尼卡形态 GEAR-5 参考链接 当使用服务器进行模型训练时&#xff0c;想要使用MacBook查看一些可视化结果&#xff0c;如果远程服务器和本机在一个局域网内&#xff0c;可以通过以下命令解决&#xff1a; 登录服务器&#xff1a; 先用ssh工具重定向&#xff1a;ssh -L 16006:127…

java:pdfbox 删除扫描版PDF中文本水印

官网下载 https://pdfbox.apache.org/download.html下载 pdfbox-app-3.0.3.jar cd D:\pdfbox 运行 java -jar pdfbox-app-3.0.3.jar java -jar pdfbox-app-3.0.3.jar Usage: pdfbox [COMMAND] [OPTIONS] Commands:debug Analyzes and inspects the internal structu…