C语言Windows环境下生产者多线程消费者队列实现文件备份的相关问题
Windows纯C多线程备份任务队列实现方案
采用生产者-消费者模型实现,扫描线程作为生产者负责将待备份文件名塞到队列,4个工作线程作为消费者从队列取任务执行备份,全程不会阻塞扫描逻辑,也不会出现重复任务。
依赖组件说明
全部使用Windows原生API实现,无需第三方依赖:
- 用
CRITICAL_SECTION临界区做队列锁,用户态同步速度极快,不会拖慢扫描线程 - 用
Semaphore信号量做任务计数,工作线程无任务时阻塞在信号量上,不占用CPU资源 - 文件名单独堆内存分配,避免扫描线程栈变量释放后野指针问题
核心结构定义
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <string.h> // 队列节点:存储待备份文件名 typedef struct TaskNode { char* filename; struct TaskNode* next; } TaskNode; // 线程安全的任务队列 typedef struct TaskQueue { TaskNode* head; TaskNode* tail; CRITICAL_SECTION lock; // 队列操作锁 HANDLE semaphore; // 任务计数信号量 int shutdown; // 退出标志位 } TaskQueue; // 全局队列实例,也可以封装后作为参数传递 TaskQueue g_backup_queue;
队列基础接口实现
队列初始化
void queue_init(TaskQueue* q) { q->head = q->tail = NULL; InitializeCriticalSection(&q->lock); // 信号量初始计数0,最大计数设为10万满足普通文件扫描量级,可按需调整 q->semaphore = CreateSemaphore(NULL, 0, 100000, NULL); q->shutdown = 0; }
任务入队(扫描线程调用)
操作仅持有极短时间锁,完全不会阻塞扫描流程:
void queue_push(TaskQueue* q, const char* filename) { // 先复制文件名,避免扫描线程栈内存释放后出现野指针 char* fname_buf = malloc(strlen(filename) + 1); strcpy(fname_buf, filename); TaskNode* node = malloc(sizeof(TaskNode)); node->filename = fname_buf; node->next = NULL; // 临界区仅操作链表指针,耗时<1us,几乎无阻塞 EnterCriticalSection(&q->lock); if (q->tail == NULL) { q->head = q->tail = node; } else { q->tail->next = node; q->tail = node; } LeaveCriticalSection(&q->lock); // 信号量计数+1,唤醒一个等待的工作线程 ReleaseSemaphore(q->semaphore, 1, NULL); }
任务出队(工作线程调用)
无任务时自动阻塞,不消耗CPU:
char* queue_pop(TaskQueue* q) { // 等待信号量,有任务或者队列关闭时才会返回 WaitForSingleObject(q->semaphore, INFINITE); EnterCriticalSection(&q->lock); if (q->shutdown) { LeaveCriticalSection(&q->lock); return NULL; } TaskNode* node = q->head; q->head = node->next; if (q->head == NULL) { q->tail = NULL; } LeaveCriticalSection(&q->lock); char* fname = node->filename; free(node); return fname; }
工作线程实现
// 你原有backup函数逻辑保留即可 int backup(char* filename) { // 压缩、移动到备份盘逻辑 printf("备份文件: %s\n", filename); return 1; } DWORD WINAPI backup_worker(LPVOID param) { while (1) { char* fname = queue_pop(&g_backup_queue); if (fname == NULL) { // 收到退出信号,线程结束 break; } backup(fname); free(fname); // 用完释放文件名内存 } return 0; }
扫描逻辑修改
原recursive_file_scan无需等待,扫到文件直接塞队列即可:
void recursive_file_scan(char* base_path) { // 你原有遍历逻辑保持不变 /* ...遍历逻辑... if(is_folder(file_path)) recursive_file_scan(file_path); */ // 扫到文件直接入队,不需要等待备份完成,马上继续遍历 char* filename = file_path; queue_push(&g_backup_queue, filename); }
主流程调用示例
int main() { // 1. 初始化队列 queue_init(&g_backup_queue); // 2. 启动4个工作线程 HANDLE workers[4]; for (int i = 0; i < 4; i++) { workers[i] = CreateThread(NULL, 0, backup_worker, NULL, 0, NULL); } // 3. 启动递归扫描,不会被备份逻辑阻塞 recursive_file_scan("C:\\需要备份的目录"); // 4. 扫描完成,通知所有工作线程退出 EnterCriticalSection(&g_backup_queue.lock); g_backup_queue.shutdown = 1; LeaveCriticalSection(&g_backup_queue.lock); // 发4次信号量,唤醒所有阻塞的工作线程 ReleaseSemaphore(g_backup_queue.semaphore, 4, NULL); // 5. 等待所有备份完成,释放资源 WaitForMultipleObjects(4, workers, TRUE, INFINITE); for (int i = 0; i < 4; i++) { CloseHandle(workers[i]); } CloseHandle(g_backup_queue.semaphore); DeleteCriticalSection(&g_backup_queue.lock); return 0; }
关键特性说明
- 无扫描卡顿:入队操作临界区持有时间极短,扫描线程几乎不会被阻塞,遍历速度和单线程扫盘一致
- 无重复备份:每个文件只会被扫描线程入队一次,队列出队操作加锁保证同一个任务只会被一个工作线程取到,不会重复执行
- 资源安全:文件名单独堆分配,不会出现栈变量野指针问题,所有内存都有对应释放逻辑
- 无空转消耗:工作线程无任务时阻塞在信号量上,不会占用CPU资源
内容的提问来源于stack exchange,提问作者mov eax
相关产品推荐
相关产品推荐

