【C语言系统编程】【第二部分:并发编程】2.3 实践与案例分析

7 篇文章 0 订阅
2.3 实践与案例分析
2.3.1 案例分析:多线程文件搜索程序

本文中,我们将通过一个多线程文件搜索程序的案例,展示如何在实际项目中应用多线程编程技术,并具体介绍任务分解、线程创建、结果汇总及锁机制的应用。

2.3.1.1 任务分解与线程创建

在一个多线程文件搜索程序中,我们首先需要将搜索任务分解为多个子任务,每个子任务由一个线程执行。

  • 任务分解:将整个文件夹树的搜索任务分解成若干个独立的搜索路径,每个线程负责一个或多个搜索路径。这种方式能够充分利用多核CPU,提高搜索效率。
  • 线程创建:利用POSIX线程库中的 pthread_create 函数创建多个线程,并让每个线程执行它们分配到的搜索任务。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <dirent.h>

#define MAX_THREADS 10

// 线程函数声明
void* search_file(void *arg);

// 任务结构体定义
typedef struct {
    char path[256];   // 搜索路径 [1]
    char target[256]; // 目标文件名 [2]
} SearchTask;

int main() {
    pthread_t threads[MAX_THREADS];      // 线程数组 [3]
    SearchTask task[MAX_THREADS];        // 任务数组 [4]

    // 初始化任务,例如将 /home/user 作为搜索路径
    strcpy(task[0].path, "/home/user");    // 设置搜索路径 [5]
    strcpy(task[0].target, "target_file.txt");  // 设置目标文件 [6]

    // 创建线程
    if (pthread_create(&threads[0], NULL, search_file, (void*)&task[0])) {  // 创建线程 [7]
        fprintf(stderr, "Error creating thread\n");
        return 1;
    }

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

    return 0;
}

void* search_file(void *arg) {
    SearchTask *task = (SearchTask*)arg;  // 获取任务参数 [9]
    DIR *dir;
    struct dirent *entry; // 目录项结构指针

    // 打开目录
    if ((dir = opendir(task->path)) == NULL) {  // 打开目录 [10]
        perror("opendir() error"); // 处理打开失败情况
        return NULL;
    }

    // 读取目录项
    while ((entry = readdir(dir)) != NULL) { // 遍历目录项 [11]
        if (strcmp(entry->d_name, task->target) == 0) {  // 文件名匹配检查 [12]
            printf("Found file: %s/%s\n", task->path, task->target); // 找到目标文件 [13]
            break; // 找到文件后退出
        }
    }

    // 关闭目录
    closedir(dir);
    return NULL;
}
  • [1] 搜索路径char path[256] 用于存储待搜索的目录路径。
  • [2] 目标文件名char target[256] 保存待查找的文件名。
  • [3] 线程数组pthread_t threads[MAX_THREADS] 用于存储线程ID。
  • [4] 任务数组SearchTask task[MAX_THREADS] 存储每个线程的搜索任务。
  • [5] 设置搜索路径:使用 strcpy() 函数初始化任务中路径。
  • [6] 设置目标文件:使用 strcpy() 函数初始化任务中目标文件名。
  • [7] 创建线程:使用 pthread_create() 函数创建线程并指定执行函数及其参数。
  • [8] 等待线程结束pthread_join() 用于等待线程执行完毕,以确保所有线程在程序结束前完成任务。
  • [9] 获取任务参数:将 void* arg 转换为 SearchTask*
  • [10] 打开目录opendir() 函数打开指定路径的目录,返回目录指针。
  • [11] 遍历目录项:使用 readdir() 读取目录内容,将结果存入 struct dirent* entry
  • [12] 文件名匹配检查strcmp() 判断目录项名称是否与目标文件名相同。
  • [13] 找到目标文件:匹配后打印出文件的完整路径。
2.3.1.2 线程间结果汇总

在一个多线程程序中,各个线程需要将它们的搜索结果汇总到一个共享的数据结构中。为了确保线程安全,汇总结果时需要使用同步机制。

  • 共享数据结构:可以使用一个全局链表或数组来存储搜索到的目标文件路径。
  • 线程安全:在访问共享数据结构时使用互斥锁 (pthread_mutex_t) 来防止数据竞态条件。
#include <pthread.h>

// 定义一个互斥锁
pthread_mutex_t lock;

// 定义一个二维字符数组用于存储搜索结果
char results[10][256]; // 假设最多可以存储10个结果 [1]

// 函数 search_file:用于在文件中搜索
void* search_file(void *arg) {
    // ... 进行搜索的代码,与前代码段一致 ...

    // 加锁以保证线程安全
    pthread_mutex_lock(&lock);

    // 将结果存储到共享的 `results` 数组中
    strcpy(results[0], task->path); // 假设将结果存储到第一个位置 [2]

    // 解锁以允许其他线程访问
    pthread_mutex_unlock(&lock);

    // ... 继续进行其余的搜索逻辑 ...

    return NULL; // 返回空指针表示线程无返回值 [3]
}

int main() {
    // 初始化互斥锁
    pthread_mutex_init(&lock, NULL); // 使用默认属性初始化锁 [4]

    // 创建线程并等待其完成,这部分代码在较早示例中提供
    // ... 线程创建及执行代码略 ...

    // 销毁互斥锁
    pthread_mutex_destroy(&lock); // 销毁锁以释放系统资源 [5]

    return 0;
}
  • [1] 结果存储数组char results[10][256] 定义了一个二维数组,能够存储最多10个字符串结果,每个结果最长256字节。
  • [2] 结果存储到共享数组strcpy(results[0], task->path); 此行示例中假设将结果存储在 results 数组的第一个位置。实际存储位置需要根据逻辑进行动态选择。
  • [3] 返回空指针:线程函数search_file不需要返回具体数据,直接返回 NULL
  • [4] 初始化互斥锁pthread_mutex_init(&lock, NULL); 用于初始化互斥锁,NULL 表示使用默认的锁属性。
  • [5] 释放锁资源pthread_mutex_destroy(&lock); 销毁互斥锁,以释放占用的系统资源,这通常在主程序最后进行。
2.3.1.3 锁机制的应用

为了避免多个线程同时操作共享数据而导致竞态条件,需要使用互斥锁机制来确保线程安全。

  • 互斥锁:在每次访问共享数据结构之前,先锁住互斥锁 (pthread_mutex_lock),操作完成后解锁 (pthread_mutex_unlock)。
  • 应用场景:在搜索结果的汇总过程中,各个线程需要安全地将找到的结果存储到共享的数据结构中,使用锁能确保各线程按照序列进行数据操作。
pthread_mutex_lock(&lock); // 锁住数据
// 更新共享数据
pthread_mutex_unlock(&lock); // 解锁数据

以上内容详细讲解了多线程编程中任务分解、线程创建、结果汇总及锁机制的应用。通过这个案例,您可以了解如何在实际项目中应用多线程技术,提高程序的并发处理能力和效率。

2.3.2 案例分析:生产者-消费者模型
2.3.2.1 问题描述与解决思路

生产者-消费者问题是一个经典的并发控制问题,描述了两个线程(或进程)之间的互斥使用共享资源的情况。通常,一个或多个生产者线程负责生产数据并放入一个共享的缓冲区,而一个或多个消费者线程负责从缓冲区中取出数据进行处理。

问题描述

  1. 生产者不断地向缓冲区放入产品。
  2. 消费者不断地从缓冲区取出产品。
  3. 缓冲区有固定容量,当缓冲区满时,生产者必须等待;当缓冲区空时,消费者必须等待。

解决思路

  1. 使用互斥锁(mutex)保护对共享缓冲区的访问,防止生产者和消费者同时操作缓冲区导致的数据不一致。
  2. 使用信号量(semaphore)或条件变量(condition variable)来管理缓冲区的状态,控制生产者和消费者的等待和唤醒。
2.3.2.2 信号量与互斥锁的结合使用

在生产者-消费者问题中,信号量常用于管理缓冲区中的空闲位置和已有数据的数量,而互斥锁则用于保护缓冲区的访问。

信号量

  • 空闲位置信号量:表示缓冲区中可放置产品的位置数量。
  • 已有数据信号量:表示缓冲区中已有的产品数量。

互斥锁

  • 这段代码展示了经典的生产者-消费者问题,用缓冲区实现生产者生成产品,消费者消费产品。生产者和消费者之间通过信号量和互斥锁进行同步和互斥控制。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];  // 缓冲区 [1]
int in = 0, out = 0;      // 生产和消费的指针 [2]

pthread_mutex_t mutex;     // 互斥锁 [3]
sem_t empty, full;         // 信号量:空槽和满槽 [4]

// 生产者线程的函数
void *producer(void *param) {
    int item;
    while (1) {
        item = rand() % 100; // 生产一个随机数 [5]
        sem_wait(&empty);    // 等待空闲位置 [6]
        pthread_mutex_lock(&mutex); // 获取互斥锁,保证对缓冲区操作的原子性 [7]

        buffer[in] = item;   // 将产品放入缓冲区 [8]
        in = (in + 1) % BUFFER_SIZE;   // 环形缓冲区,更新生产序号 [9]
        printf("Produced: %d\n", item);

        pthread_mutex_unlock(&mutex); // 释放互斥锁 [10]
        sem_post(&full);    // 增加已有产品数量 [11]
    }
}

// 消费者线程的函数
void *consumer(void *param) {
    int item;
    while (1) {
        sem_wait(&full);    // 等待已有产品 [12]
        pthread_mutex_lock(&mutex); // 获取互斥锁,保证对缓冲区操作的原子性 [13]

        item = buffer[out]; // 从缓冲区取出产品 [14]
        out = (out + 1) % BUFFER_SIZE;  // 环形缓冲区,更新消费序号 [15]
        printf("Consumed: %d\n", item);

        pthread_mutex_unlock(&mutex); // 释放互斥锁 [16]
        sem_post(&empty); // 增加空闲位置数量 [17]
    }
}

// 主函数
int main() {
    pthread_t prod, cons;
    pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁 [18]
    sem_init(&empty, 0, BUFFER_SIZE);  // 初始化信号量 `empty`,初始值为 BUFFER_SIZE,表示起始时所有位置都是空的 [19]
    sem_init(&full, 0, 0);             // 初始化信号量 `full`,初始值为 0,表示起始时一个产品都没有 [20]

    pthread_create(&prod, NULL, producer, NULL); // 创建生产者线程 [21]
    pthread_create(&cons, NULL, consumer, NULL); // 创建消费者线程 [22]

    pthread_join(prod, NULL); // 阻塞主线程,等待生产者线程结束 [23]
    pthread_join(cons, NULL); // 阻塞主线程,等待消费者线程结束 [24]

    pthread_mutex_destroy(&mutex); // 销毁互斥锁 [25]
    sem_destroy(&empty);           // 销毁信号量 `empty` [26]
    sem_destroy(&full);            // 销毁信号量 `full` [27]

    return 0;
}
详细注释
  • [1] 缓冲区:用于存储生产者产生而等待被消费者消费的产品。
  • [2] 生产和消费的指针in指向下一个将存入产品的位置,out指向将被消费的产品位置。两者实现环形缓冲区的指针管理。
  • [3] 互斥锁:保护对共享缓冲区的访问,确保同一时刻只有一个线程进行写入或读取操作。
  • [4] 信号量emptyfull
    • empty表示缓冲区中空余的槽位。
    • full表示缓冲区中已满的槽位,以便消费者知道是否有产品可以消费。
  • [5] 生产随机产品:使用rand()函数生成随机数,模拟产品生产。
  • [6] 等待空闲位置:通过sem_wait(&empty)empty信号量为0,则线程会被阻塞,直到有槽位可用。
  • [7] 获取互斥锁:确保对共享资源操作的独占性。
  • [8] 存放产品:将生产的产品放入缓冲区指定位置。
  • [9] 环形缓冲区更新:生产器位置in的循环递增。
  • [10] 释放互斥锁:操作完成后释放锁。
  • [11] 增加产品数量:信号量full增加,表示多一个产品可消费。
  • [12] 等待已有产品:通过sem_wait(&full)full信号量为0,则线程会被阻塞,直到有产品可用。
  • [13] 获取互斥锁:确保对共享资源操作的独占性。
  • [14] 消费产品:从缓冲区的指定位置取出产品。
  • [15] 环行缓冲区更新:消费者位置out的循环递增。
  • [16] 释放互斥锁:操作完成后释放锁。
  • [17] 增加空闲位置:信号量empty增加,表示多一个槽位可用。
  • [18] 初始化互斥锁:对mutex进行初始化,准备使用。
  • [19] 初始化信号量empty:信号量empty表示初始时所有位置都是空的。
  • [20] 初始化信号量full:初始值为0,表示起始时没有产品可以消费。
  • [21-22] 创建线程:分配并启动生产者和消费者线程。
  • [23-24] 等待线程结束pthread_join用于等待线程结束,确保主线程不会提前退出。
  • [25-27] 资源销毁:销毁锁和信号量,释放资源。
2.3.2.3 条件变量的应用

条件变量提供了一种线程间通知的机制。一个线程可以等待特定的条件变量,另一个线程可以在条件变量上发出信号,通知等待的线程条件已经满足。

在生产者-消费者问题中,可以用条件变量实现对缓冲区状态的检查和通知。

代码示例

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h> // 添加stdlib.h用于rand()

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE]; // 循环缓冲区 [1]
int in = 0, out = 0;     // 生产者和消费者指针 [2]

pthread_mutex_t mutex;            // 互斥锁 [3]
pthread_cond_t not_full, not_empty; // 条件变量 [4]

// 生产者线程函数
void *producer(void *param) {
    int item;
    while (1) {
        item = rand() % 100; // 生成随机数 [5]

        pthread_mutex_lock(&mutex); // 获取互斥锁 [6]

        while ((in + 1) % BUFFER_SIZE == out) {
            pthread_cond_wait(&not_full, &mutex); // 缓冲区满时等待 [7]
        }

        buffer[in] = item; // 放入缓冲区 [8]
        in = (in + 1) % BUFFER_SIZE;
        printf("Produced: %d\n", item);

        pthread_cond_signal(&not_empty); // 通知消费者 [9]
        pthread_mutex_unlock(&mutex);    // 释放互斥锁 [10]
    }
}

// 消费者线程函数
void *consumer(void *param) {
    int item;
    while (1) {
        pthread_mutex_lock(&mutex); // 获取互斥锁 [11]

        while (in == out) {
            pthread_cond_wait(&not_empty, &mutex); // 缓冲区空时等待 [12]
        }

        item = buffer[out]; // 提取缓冲区产品 [13]
        out = (out + 1) % BUFFER_SIZE;
        printf("Consumed: %d\n", item);

        pthread_cond_signal(&not_full); // 通知生产者 [14]
        pthread_mutex_unlock(&mutex);   // 释放互斥锁 [15]
    }
}

int main() {
    pthread_t prod, cons;

    // 初始化互斥锁和条件变量 [16]
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    // 创建生产者和消费者线程 [17]
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);

    // 等待线程结束(不会发生)[18]
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    // 销毁互斥锁和条件变量 [19]
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);

    return 0;
}
  • [1] 循环缓冲区:定义一个大小为5的整数数组,用于存储生产者生成的数据。
  • [2] 生产者和消费者指针inout用于指示生产和消费的位置,基于环形缓冲区机制,确保指针在0到BUFFER_SIZE-1之间循环利用。
  • [3] 互斥锁pthread_mutex_t mutex用于确保对共享缓冲区的互斥访问,以避免竞争条件。
  • [4] 条件变量not_fullnot_empty 用于生产者和消费者之间的同步,确保缓冲区状态改变时进行相应的阻塞或唤醒。
  • [5] 生成随机数:生产者生成0-99之间的随机整数作为产品。
  • [6] 获取互斥锁:生产者在向缓冲区中放入产品之前锁定互斥锁,确保独占访问。
  • [7] 缓冲区满时等待:如果缓冲区满,则生产者在not_full条件变量上等待,直至消费者消耗产品。
  • [8] 放入缓冲区:将生成的产品放入缓冲区,并更新生产者索引。
  • [9] 通知消费者:生产者生产完产品后,通过pthread_cond_signal()通知可能在等待的消费者。
  • [10] 释放互斥锁:对于共享数据操作完成后释放锁,允许其他线程访问。
  • [11] 获取互斥锁:消费者在从缓冲区中消费产品之前锁定互斥锁。
  • [12] 缓冲区空时等待:如果缓冲区为空,消费者在not_empty条件变量上等待,直至生产者提供产品。
  • [13] 提取缓冲区产品:从缓冲区中取出产品,并更新消费者索引。
  • [14] 通知生产者:消费者取完产品后,通过pthread_cond_signal()通知可能在等待的生产者。
  • [15] 释放互斥锁:消费者完成操作后释放互斥锁。
  • [16] 初始化互斥锁和条件变量:在程序开始时初始化全局同步资源。
  • [17] 创建生产者和消费者线程:使用pthread_create()启动生产者和消费者线程。
  • [18] 等待线程结束(不会发生):主线程等待生产者和消费者结束,但由于它们是无限循环,因此不会发生。
  • [19] 销毁互斥锁和条件变量:程序结束时清理资源。不实际发生在这段代码中,因为程序永远运行。
总结

在生产者-消费者模型中,通过信号量和互斥锁可以有效地管理生产和消费的过程,避免竞争条件。而条件变量提供了更灵活的线程间通知机制,可以更精确地控制生产者和消费者的互相等待和唤醒。

2.3.3 案例分析:读写者问题

读写者(Reader-Writer)问题是经典的并发控制问题之一。在这个问题中,存在多个读者(Reader)和多个写者(Writer),读者可以同时读取数据,而写者在执行写操作时,需要保证没有其他读者或写者。

2.3.3.1 问题描述与解决方案

问题描述

在共享数据的并发访问中,需要满足以下条件:

  1. 当一个写者正在写数据时,不允许任何读者或者写者访问数据。
  2. 当一个读者正在读数据时,允许其他读者继续读,但不允许写者写数据。

问题解决方案

为了解决读写者问题,可以使用读写锁(Reader-Writer Lock)。读写锁允许多个读者同时读取数据,但在有写者进行写操作时,会锁定读访问和写访问。

2.3.3.2 读写锁的使用

使用POSIX线程库中的读写锁可以有效解决读写者问题,以下是一个基本使用示例:

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

#define NUM_READERS 5   // 定义读者线程数量 [1]
#define NUM_WRITERS 2   // 定义写者线程数量 [2]

pthread_rwlock_t rwlock;  // 声明读写锁 [3]
int shared_data = 0;      // 共享数据资源 [4]

// 读者线程函数
void* reader(void* arg) {
    int id = *((int*)arg);
    while (1) {
        pthread_rwlock_rdlock(&rwlock);  // 获取读锁 [5]
        printf("Reader %d: read shared_data = %d\n", id, shared_data);
        pthread_rwlock_unlock(&rwlock);  // 释放读锁 [6]
        sleep(1);
    }
    return NULL;
}

// 写者线程函数
void* writer(void* arg) {
    int id = *((int*)arg);
    while (1) {
        pthread_rwlock_wrlock(&rwlock);  // 获取写锁 [7]
        shared_data++;
        printf("Writer %d: updated shared_data to %d\n", id, shared_data);
        pthread_rwlock_unlock(&rwlock);  // 释放写锁 [8]
        sleep(2);
    }
    return NULL;
}

int main() {
    pthread_t readers[NUM_READERS], writers[NUM_WRITERS];
    int ids[NUM_READERS > NUM_WRITERS ? NUM_READERS : NUM_WRITERS];
    
    pthread_rwlock_init(&rwlock, NULL);  // 初始化读写锁 [9]

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

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

    // 等待所有线程完成
    for (int i = 0; i < NUM_READERS; i++) {
        pthread_join(readers[i], NULL);  // 等待读者线程终止 [12]
    }
    for (int i = 0; i < NUM_WRITERS; i++) {
        pthread_join(writers[i], NULL);  // 等待写者线程终止 [13]
    }

    pthread_rwlock_destroy(&rwlock);  // 销毁读写锁 [14]

    return 0;
}
  • [1] 读者线程数量#define NUM_READERS 5 表示创建5个读者线程。
  • [2] 写者线程数量#define NUM_WRITERS 2 表示创建2个写者线程。
  • [3] 读写锁声明pthread_rwlock_t rwlock 用于同步对共享数据的访问。
  • [4] 共享数据资源:变量 shared_data 是所有线程共享访问的整数。
  • [5] 获取读锁pthread_rwlock_rdlock 保证能安全地读共享数据。
  • [6] 释放读锁pthread_rwlock_unlock 在读数据结束后释放锁,以允许其他线程访问。
  • [7] 获取写锁pthread_rwlock_wrlock 为写者线程获取独占访问权。
  • [8] 释放写锁pthread_rwlock_unlock 在写操作结束后释放锁。
  • [9] 初始化读写锁pthread_rwlock_init 函数用于读写锁的初始化处理。
  • [10] 创建读者线程pthread_create 启动指定数量的读者线程。
  • [11] 创建写者线程pthread_create 启动指定数量的写者线程。
  • [12] 等待读者线程终止pthread_join 用于等待对应的读者线程执行结束。
  • [13] 等待写者线程终止pthread_join 用于等待对应的写者线程执行结束。
  • [14] 销毁读写锁pthread_rwlock_destroy 清理和释放读写锁的资源。

这段代码示例了一个经典的读者-写者问题的同步解决方案,确保多个读者可以并发地读取资源,但只允许一个写者可以更新资源。

2.3.3.3 优化与性能分析

优化建议

  1. 读优先 vs 写优先:根据应用场景,可以设置锁的策略,例如优先读还是优先写。可以通过调整锁的使用顺序,防止读者或写者饥饿。
  2. 线程设计:在实际程序中,可以根据读写操作频率调整读者和写者的数量。
  3. 调整锁策略:如果共享数据的访问频率较高,可以考虑减少锁的争用。例如通过分段锁定减少锁的冲突。

性能分析

使用读写锁显著提高了并发性,因为读操作不相互干扰。但需要注意的是,如果写操作频繁,可能导致读操作的延迟。因此,对于高并发的读操作,读写锁是有效的,但如果有大量的写操作需求,可能需要重新审视锁策略或者使用其他并发控制机制。

通过本文中读写锁的基本介绍及实际代码示例,可以帮助解决经典的读写者问题,提升对并发编程的理解与应用能力。

上述解释包括了对读写者问题的描述,引入了读写锁的相关概念,并通过实际的C语言代码展示了如何使用读写锁,最后对优化措施和性能进行了简单分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值