本文中,我们将通过一个多线程文件搜索程序的案例,展示如何在实际项目中应用多线程编程技术,并具体介绍任务分解、线程创建、结果汇总及锁机制的应用。
在一个多线程文件搜索程序中,我们首先需要将搜索任务分解为多个子任务,每个子任务由一个线程执行。
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;
}
char path[256] 用于存储待搜索的目录路径。char target[256] 保存待查找的文件名。pthread_t threads[MAX_THREADS] 用于存储线程ID。SearchTask task[MAX_THREADS] 存储每个线程的搜索任务。strcpy() 函数初始化任务中路径。strcpy() 函数初始化任务中目标文件名。pthread_create() 函数创建线程并指定执行函数及其参数。pthread_join() 用于等待线程执行完毕,以确保所有线程在程序结束前完成任务。void* arg 转换为 SearchTask*。opendir() 函数打开指定路径的目录,返回目录指针。readdir() 读取目录内容,将结果存入 struct dirent* entry。strcmp() 判断目录项名称是否与目标文件名相同。在一个多线程程序中,各个线程需要将它们的搜索结果汇总到一个共享的数据结构中。为了确保线程安全,汇总结果时需要使用同步机制。
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;
}
char results[10][256] 定义了一个二维数组,能够存储最多10个字符串结果,每个结果最长256字节。strcpy(results[0], task->path); 此行示例中假设将结果存储在 results 数组的第一个位置。实际存储位置需要根据逻辑进行动态选择。search_file不需要返回具体数据,直接返回 NULL。pthread_mutex_init(&lock, NULL); 用于初始化互斥锁,NULL 表示使用默认的锁属性。pthread_mutex_destroy(&lock); 销毁互斥锁,以释放占用的系统资源,这通常在主程序最后进行。为了避免多个线程同时操作共享数据而导致竞态条件,需要使用互斥锁机制来确保线程安全。
pthread_mutex_lock),操作完成后解锁 (pthread_mutex_unlock)。pthread_mutex_lock(&lock); // 锁住数据
// 更新共享数据
pthread_mutex_unlock(&lock); // 解锁数据
以上内容详细讲解了多线程编程中任务分解、线程创建、结果汇总及锁机制的应用。通过这个案例,您可以了解如何在实际项目中应用多线程技术,提高程序的并发处理能力和效率。
生产者-消费者问题是一个经典的并发控制问题,描述了两个线程(或进程)之间的互斥使用共享资源的情况。通常,一个或多个生产者线程负责生产数据并放入一个共享的缓冲区,而一个或多个消费者线程负责从缓冲区中取出数据进行处理。
问题描述:
解决思路:
在生产者-消费者问题中,信号量常用于管理缓冲区中的空闲位置和已有数据的数量,而互斥锁则用于保护缓冲区的访问。
信号量:
互斥锁:
#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;
}
in指向下一个将存入产品的位置,out指向将被消费的产品位置。两者实现环形缓冲区的指针管理。empty和full:
empty表示缓冲区中空余的槽位。full表示缓冲区中已满的槽位,以便消费者知道是否有产品可以消费。rand()函数生成随机数,模拟产品生产。sem_wait(&empty)若empty信号量为0,则线程会被阻塞,直到有槽位可用。in的循环递增。full增加,表示多一个产品可消费。sem_wait(&full)若full信号量为0,则线程会被阻塞,直到有产品可用。out的循环递增。empty增加,表示多一个槽位可用。mutex进行初始化,准备使用。empty:信号量empty表示初始时所有位置都是空的。full:初始值为0,表示起始时没有产品可以消费。pthread_join用于等待线程结束,确保主线程不会提前退出。条件变量提供了一种线程间通知的机制。一个线程可以等待特定的条件变量,另一个线程可以在条件变量上发出信号,通知等待的线程条件已经满足。
在生产者-消费者问题中,可以用条件变量实现对缓冲区状态的检查和通知。
代码示例:
#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(¬_full, &mutex); // 缓冲区满时等待 [7]
}
buffer[in] = item; // 放入缓冲区 [8]
in = (in + 1) % BUFFER_SIZE;
printf("Produced: %d\n", item);
pthread_cond_signal(¬_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(¬_empty, &mutex); // 缓冲区空时等待 [12]
}
item = buffer[out]; // 提取缓冲区产品 [13]
out = (out + 1) % BUFFER_SIZE;
printf("Consumed: %d\n", item);
pthread_cond_signal(¬_full); // 通知生产者 [14]
pthread_mutex_unlock(&mutex); // 释放互斥锁 [15]
}
}
int main() {
pthread_t prod, cons;
// 初始化互斥锁和条件变量 [16]
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_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(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
in和out用于指示生产和消费的位置,基于环形缓冲区机制,确保指针在0到BUFFER_SIZE-1之间循环利用。pthread_mutex_t mutex用于确保对共享缓冲区的互斥访问,以避免竞争条件。not_full 和 not_empty 用于生产者和消费者之间的同步,确保缓冲区状态改变时进行相应的阻塞或唤醒。not_full条件变量上等待,直至消费者消耗产品。pthread_cond_signal()通知可能在等待的消费者。not_empty条件变量上等待,直至生产者提供产品。pthread_cond_signal()通知可能在等待的生产者。pthread_create()启动生产者和消费者线程。在生产者-消费者模型中,通过信号量和互斥锁可以有效地管理生产和消费的过程,避免竞争条件。而条件变量提供了更灵活的线程间通知机制,可以更精确地控制生产者和消费者的互相等待和唤醒。
读写者(Reader-Writer)问题是经典的并发控制问题之一。在这个问题中,存在多个读者(Reader)和多个写者(Writer),读者可以同时读取数据,而写者在执行写操作时,需要保证没有其他读者或写者。
问题描述:
在共享数据的并发访问中,需要满足以下条件:
问题解决方案:
为了解决读写者问题,可以使用读写锁(Reader-Writer Lock)。读写锁允许多个读者同时读取数据,但在有写者进行写操作时,会锁定读访问和写访问。
使用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;
}
#define NUM_READERS 5 表示创建5个读者线程。#define NUM_WRITERS 2 表示创建2个写者线程。pthread_rwlock_t rwlock 用于同步对共享数据的访问。shared_data 是所有线程共享访问的整数。pthread_rwlock_rdlock 保证能安全地读共享数据。pthread_rwlock_unlock 在读数据结束后释放锁,以允许其他线程访问。pthread_rwlock_wrlock 为写者线程获取独占访问权。pthread_rwlock_unlock 在写操作结束后释放锁。pthread_rwlock_init 函数用于读写锁的初始化处理。pthread_create 启动指定数量的读者线程。pthread_create 启动指定数量的写者线程。pthread_join 用于等待对应的读者线程执行结束。pthread_join 用于等待对应的写者线程执行结束。pthread_rwlock_destroy 清理和释放读写锁的资源。这段代码示例了一个经典的读者-写者问题的同步解决方案,确保多个读者可以并发地读取资源,但只允许一个写者可以更新资源。
优化建议:
性能分析:
使用读写锁显著提高了并发性,因为读操作不相互干扰。但需要注意的是,如果写操作频繁,可能导致读操作的延迟。因此,对于高并发的读操作,读写锁是有效的,但如果有大量的写操作需求,可能需要重新审视锁策略或者使用其他并发控制机制。
通过本文中读写锁的基本介绍及实际代码示例,可以帮助解决经典的读写者问题,提升对并发编程的理解与应用能力。
上述解释包括了对读写者问题的描述,引入了读写锁的相关概念,并通过实际的C语言代码展示了如何使用读写锁,最后对优化措施和性能进行了简单分析。
因篇幅问题不能全部显示,请点此查看更多更全内容