如何等待GTK窗口的pthread关闭?批量下载进度窗异常排查
问题分析与修复
核心问题
- 全局变量滥用:
progress_bar、window、new_handle均为全局变量,循环调用downloadFile时,后续调用会覆盖这些变量的内容,导致前一次下载的进度窗口控件被覆盖,无法正常更新。 - 主线程被强制阻塞:
downloadFile中创建progressDialog线程后立刻调用pthread_join,迫使主线程等待当前下载完成才进入下一次循环,这是所有下载完成后才批量弹出窗口的直接原因。 - GTK事件循环缺失:
progressDialog中注释了gtk_main(),GTK未启动主事件循环,无法处理窗口绘制和事件更新,导致窗口无法及时显示。 - GTK重复初始化:每次调用
progressDialog都执行gtk_init,GTK仅允许初始化一次,多次调用会引发不可预期的错误。 - 跨线程操作GTK控件:
progress_func在libcurl的回调线程中直接操作GTK进度条,违反了GTK控件必须在主循环线程操作的规则,可能导致界面卡顿或崩溃。
修复方案
1. 封装下载与窗口数据为结构体
将每个下载任务的curl句柄、进度条、窗口等数据封装到专属结构体中,彻底消除全局变量冲突:
typedef struct { CURL* curl_handle; GtkWidget* window; GtkWidget* progress_bar; FILE* output_file; char* download_url; char* output_path; double total_size; double downloaded_size; } DownloadTask;
2. 重构线程逻辑,避免主线程阻塞
- 不在
downloadFile中调用pthread_join,让每个下载任务的线程独立运行。 - 程序入口仅初始化一次GTK并启动全局主循环,所有窗口创建和控件操作都在主循环线程中处理。
3. 线程安全更新进度条
通过g_idle_add将进度更新操作投递到GTK主循环线程执行,避免跨线程操作控件:
static gboolean update_progress_bar(gpointer data) { DownloadTask* task = (DownloadTask*)data; if (task->total_size > 0) { double progress = task->downloaded_size / task->total_size; gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(task->progress_bar), progress); char text_buf[64]; snprintf(text_buf, sizeof(text_buf), "%.1f%%", progress * 100); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(task->progress_bar), text_buf); } return FALSE; } int progress_func(void *ptr, double total, double downloaded, double ultotal, double ulnow) { DownloadTask* task = (DownloadTask*)ptr; task->total_size = total; task->downloaded_size = downloaded; g_idle_add(update_progress_bar, task); return 0; }
修复后的完整简化代码示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <gtk/gtk.h> #include <curl/curl.h> #include <pthread.h> typedef struct { CURL* curl_handle; GtkWidget* window; GtkWidget* progress_bar; FILE* output_file; char* download_url; char* output_path; double total_size; double downloaded_size; } DownloadTask; static size_t write_data(void* data, size_t size, size_t nmemb, void* userdata) { DownloadTask* task = (DownloadTask*)userdata; return fwrite(data, size, nmemb, task->output_file); } static gboolean update_progress_bar(gpointer data) { DownloadTask* task = (DownloadTask*)data; if (task->total_size > 0) { double progress = task->downloaded_size / task->total_size; gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(task->progress_bar), progress); char text_buf[64]; snprintf(text_buf, sizeof(text_buf), "%.1f%%", progress * 100); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(task->progress_bar), text_buf); } return FALSE; } int progress_func(void *ptr, double total, double downloaded, double ultotal, double ulnow) { DownloadTask* task = (DownloadTask*)ptr; task->total_size = total; task->downloaded_size = downloaded; g_idle_add(update_progress_bar, task); return 0; } void* download_task_thread(void* arg) { DownloadTask* task = (DownloadTask*)arg; // 初始化CURL句柄 task->curl_handle = curl_easy_init(); curl_easy_setopt(task->curl_handle, CURLOPT_URL, task->download_url); curl_easy_setopt(task->curl_handle, CURLOPT_WRITEFUNCTION, write_data); curl_easy_setopt(task->curl_handle, CURLOPT_WRITEDATA, task); curl_easy_setopt(task->curl_handle, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(task->curl_handle, CURLOPT_PROGRESSFUNCTION, progress_func); curl_easy_setopt(task->curl_handle, CURLOPT_PROGRESSDATA, task); curl_easy_setopt(task->curl_handle, CURLOPT_FAILONERROR, 1L); // 执行下载 CURLcode res = curl_easy_perform(task->curl_handle); if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res)); } // 清理资源 curl_easy_cleanup(task->curl_handle); fclose(task->output_file); g_idle_add((GSourceFunc)gtk_widget_destroy, task->window); free(task->download_url); free(task->output_path); free(task); return NULL; } gboolean create_progress_window(gpointer data) { DownloadTask* task = (DownloadTask*)data; // 创建进度窗口 task->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(task->window), "下载进度"); gtk_window_set_default_size(GTK_WINDOW(task->window), 300, 50); gtk_container_set_border_width(GTK_CONTAINER(task->window), 10); // 创建进度条 task->progress_bar = gtk_progress_bar_new(); gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(task->progress_bar), TRUE); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(task->progress_bar), "准备下载"); // 窗口布局 GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); gtk_container_add(GTK_CONTAINER(task->window), box); gtk_box_pack_start(GTK_BOX(box), task->progress_bar, TRUE, TRUE, 0); gtk_widget_show_all(task->window); return FALSE; } int downloadFile(char *download_url, char *output_path) { DownloadTask* task = malloc(sizeof(DownloadTask)); task->download_url = strdup(download_url); task->output_path = strdup(output_path); task->total_size = 0; task->downloaded_size = 0; // 打开输出文件 task->output_file = fopen(output_path, "wb"); if (!task->output_file) { fprintf(stderr, "无法打开文件: %s\n", output_path); free(task->download_url); free(task->output_path); free(task); return -1; } // 在GTK主线程创建进度窗口 g_idle_add(create_progress_window, task); // 创建下载线程并分离 pthread_t thread; if (pthread_create(&thread, NULL, download_task_thread, task) != 0) { fprintf(stderr, "创建线程失败\n"); fclose(task->output_file); free(task->download_url); free(task->output_path); free(task); return -1; } pthread_detach(thread); return 0; } int main(int argc, char** argv) { gtk_init(&argc, &argv); curl_global_init(CURL_GLOBAL_ALL); // 示例:循环调用下载 downloadFile("http://example.com/file1.zip", "file1.zip"); downloadFile("http://example.com/file2.zip", "file2.zip"); gtk_main(); curl_global_cleanup(); return 0; }
关键修复总结
- 用结构体封装每个下载任务的专属数据,彻底消除全局变量冲突。
- 主线程不阻塞,每个下载任务启动独立线程,窗口随任务实时创建。
- 所有GTK控件操作通过
g_idle_add投递到主循环线程,遵守GTK线程安全规则。 - 仅初始化一次GTK和libcurl,避免重复初始化引发的错误。
内容的提问来源于stack exchange,提问作者Xpl0itU
相关产品推荐
相关产品推荐

