1. 问题来源
使用多线程进行文件下载,下载功能的实现由线程睡眠模拟,下载进度在控制台进行查看。
此问题可使用一个主线程与多个子线程解决。其中主线程功能为:
1) 查看各子线程的下载进度
2) 查看剩余的待下载任务数量
子线程功能为:
1) 执行下载任务
2) 下载任务完成后,发出通知,执行回调函数
#include <stdio.h>
#include <tinycthread.h>
#include <sys/timeb.h>
/*
* 下载文件的案例
* 1.主线程功能:
* 1) 查看各子线程的下载进度
* 2) 查看剩余的待下载任务数量
*
* 2.子线程功能:
* 1) 执行下载任务
* 2) 下载任务完成后,发出通知,执行回调函数
*/
// 定义各线程之间的上下文
typedef struct Context {
mtx_t mutex;
int download_left; // 主线程中待下载任务数量,各子线程共享,故加锁才能修改
} Context;
// 子线程执行下载任务时接收的请求
typedef struct DownloadRequest {
Context *context; // 线程上下文
char const *url; // 下载地址
char const *filename; // 下载文件名
int progress; // 下载进度
int interval; // 下载时的模拟参数(下载速度)
void (*callback)(struct DownloadRequest *); // 函数回调,本质是定义一个函数指针,在下载完成后执行该函数,发出下载完成的通知
} DownloadRequest;
// 线程睡眠
void SleepMs(long milliseconds) {
// 获取系统当前时间
struct timeb time_buffer;
ftime(&time_buffer);
// 当前时间之后睡眠多长时间
long sec = milliseconds / 1000; // 获取睡眠秒数
long nsec = (milliseconds % 1000) * 1000000L;
thrd_sleep(&(struct timespec) {.tv_sec = time_buffer.time + sec,
.tv_nsec = time_buffer.millitm * 1000000L + nsec}, NULL);
}
// 子线程下载文件
int DownloadFile(DownloadRequest *request) {
// 打印下载提示
printf("\rDownloading file from: %s into %s ...\n", request->url, request->filename);
// 模拟下载过程
for (int i = 1; i < 101; ++i) {
SleepMs(request->interval);// 使用线程睡眠模拟下载
request->progress = i;
}
// 下载完成,执行回调,发出通知
request->callback(request); // 注意,不要忘记传参数,request->callback 就相当于函数名
return 0;
}
/*
* 子线程下载完成后执行的回调函数
* 1. 修改剩余下载任务数
* 2. 发出完成任务通知
*/
void DownloadCallBack(DownloadRequest *request) {
// 由于修改主线程变量,需要加锁
mtx_lock(&request->context->mutex);
// 1. 修改剩余下载任务数
request->context->download_left--;
// 2. 发出完成任务通知
printf("\rDownload file from %s into %s successfully, left: %d\n",
request->url, request->filename, request->context->download_left);
mtx_unlock(&request->context->mutex);
}
/*
* 主线程管理整体的下载情况
* 1. 初始化下载请求,线程上下文
* 2. 创建子线程进行下载
* 3. 主线程实时查看下载进度
*/
void MainDownload(char *urls[], char *filenames[]){
#define DOWNLOAD_TASKS 5 // 定义下载任务个数
// 1. 初始化线程上下文,及下载请求
Context context = {.download_left = DOWNLOAD_TASKS}; // 初始化线程上下文
mtx_init(&context.mutex, mtx_plain); // 初始化线程上下文中的锁
DownloadRequest request[DOWNLOAD_TASKS]; // 创建五个下载请求
for (int i = 0; i < DOWNLOAD_TASKS; ++i) { // 初始化下载请求
request[i] = (DownloadRequest){
.context = &context,
.url = urls[i],
.filename = filenames[i],
.progress = 0,
.interval = i * 100 + 50, // 不同线程下载速度
.callback = DownloadCallBack
}; // 此处利用匿名结构体进行初始化操作,也可使用 request[i].context, request[i].url... 这样一个个进行初始化
}
// 2. 创建子线程进行下载
for (int i = 0; i < DOWNLOAD_TASKS; ++i) {
thrd_t t;
thrd_create(&t, DownloadFile, &request[i]);
thrd_detach(t); // 由于主线程需要实时查看下载进度,所以不等待子线程运行完毕,使用 detach,让子线程自己运行
}
// 3. 主线程实时查看下载进度 (周期性刷新控制台,查看下载进度)
while (1){
// 加锁获取当前剩余下载任务数
mtx_lock(&context.mutex);
int download_left = context.download_left; // 使用局部变量获取全局变量的值,这样就不再访问共享资源
mtx_unlock(&context.mutex);
// 判断是否下载完成
if(download_left == 0){
break;
}
// 没下载完成的情况下,显示下载进度
printf("\r"); // 光标回到首部
for (int i = 0; i < DOWNLOAD_TASKS; ++i) {
printf("%s -- %3d%% \t", request[i].filename, request[i].progress); // 注意,打印出 % 不是 \%,而是 %%
}
fflush(stdout); // 刷新缓冲区,不然不能实时查看
SleepMs(100); // 每 100 ms 查看一次
}
mtx_destroy(&context.mutex); // 销毁线程上下文中的锁
}
int main() {
char *urls[] = {
"https://www.baidu.com/file1",
"https://www.baidu.com/file2",
"https://www.baidu.com/file3",
"https://www.baidu.com/file4",
"https://www.baidu.com/file5"
};
char *filenames[] = {
"file1",
"file2",
"file3",
"file4",
"file5"
};
MainDownload(urls, filenames);
return 0;
}
使用 msvc 或 mingw 编译器查看时,程序执行结果较乱,原因在于控制台光标输出后自动换行,\r 是在下一行打印,执行结果为:
可使用 windows power shell 执行程序,光标输出后不会自动换行,其执行结果为: