文章目录
在Windows平台下,C语言提供了一套丰富的线程(Thread
)编程接口,使得开发者可以轻松地实现多线程并发操作。本文将详细介绍如何在Windows环境下使用C语言创建、管理和同步线程,以及一些常见的线程操作技巧。
这里指的是使用MSVC编译,Windows下也可以使用gcc,这时可以使用pthread.h。这个放在下一篇文章中说明。
1. 头文件
头文件:
#include <windows.h>
#include <stdio.h>
#include <process.h>
windows.h
:包含了Windows API中线程相关的函数和数据结构。stdio.h
:用于标准输入输出。process.h
:包含了线程相关的一些宏和函数声明。如果只创建一些简单的线程,可以不用这个头文件。
1.1 windows.h
一些 <windows.h>
头文件中定义的常见数据类型、结构、函数和宏的详细说明:
数据类型:
数据类型 | 描述 |
---|---|
BOOL | 逻辑值类型,可以是 TRUE 或 FALSE |
BYTE | 8 位无符号整数 |
CHAR | 8 位字符类型 |
DWORD | 32 位无符号整数 |
HANDLE | 通用句柄类型,可以表示线程、进程、文件等 |
HINSTANCE | 模块实例句柄,表示一个加载到内存中的 DLL 或 EXE 文件的实例 |
HWND | 窗口句柄 |
LPVOID | 指向任意类型的指针 |
LPCSTR | 指向常量字符串的指针 |
LPWSTR | 指向宽字符字符串的指针 |
LPTHREAD_START_ROUTINE | 线程函数指针,指向一个线程函数 |
LPSECURITY_ATTRIBUTES | 安全属性指针 |
DWORD_PTR | 用于指针大小的无符号整数,通常与指针一起使用 |
LONG_PTR | 用于指针大小的有符号整数,通常与指针一起使用 |
SIZE_T | 用于表示大小或计数的无符号整数 |
结构:
结构体 | 描述 |
---|---|
CRITICAL_SECTION | 临界区对象,用于线程同步 |
FILETIME | 文件时间结构,表示一个文件的创建、最后访问和最后修改时间 |
SYSTEMTIME | 系统时间结构,表示一个日期和时间 |
PROCESS_INFORMATION | 进程信息结构,包含有关新进程及其主线程的信息 |
STARTUPINFO | 进程的起始信息结构,用于指定新进程的主窗口的外观、标准输入输出和错误流 |
SECURITY_ATTRIBUTES | 安全属性结构,用于指定对象的安全描述符 |
函数
函数 | 描述 |
---|---|
CreateThread | 创建一个新的线程 |
CreateMutex | 创建一个互斥量 |
CreateEvent | 创建一个事件 |
CreateSemaphore | 创建一个信号量 |
WaitForSingleObject | 等待一个对象的信号,如线程、事件、互斥量等 |
WaitForMultipleObjects | 等待多个对象的信号 |
ReleaseMutex | 释放互斥量 |
SetEvent | 设置事件,使得等待该事件的线程可以继续执行 |
ResetEvent | 重置事件的信号状态 |
EnterCriticalSection | 进入临界区,获取临界区的控制权 |
LeaveCriticalSection | 离开临界区,释放对临界区的控制权 |
CloseHandle | 关闭一个内核对象的句柄 |
GetCurrentThreadId | 获取当前线程的线程ID |
GetCurrentProcessId | 获取当前进程的进程ID |
GetLastError | 获取最后一个发生错误的错误代码 |
Sleep | 让当前线程休眠指定的时间 |
TerminateThread | 终止一个线程 |
ExitThread | 退出当前线程 |
SetThreadPriority | 设置线程的优先级 |
GetThreadPriority | 获取线程的优先级 |
CreateProcess | 创建一个新的进程 |
GetSystemTime | 获取当前的系统时间 |
GetLocalTime | 获取当前的本地时间 |
GetProcessTimes | 获取进程的创建时间、用户模式和内核模式执行时间等信息 |
GetThreadTimes | 获取线程的创建时间、用户模式和内核模式执行时间等信息 |
GetModuleFileName | 获取模块的文件名 |
GetModuleHandle | 获取模块的句柄 |
GetProcAddress | 获取动态链接库中的函数地址 |
LoadLibrary | 加载一个动态链接库 |
FreeLibrary | 释放一个动态链接库 |
MessageBox | 显示一个消息框 |
MoveFile | 移动文件或重命名文件 |
DeleteFile | 删除文件 |
FindFirstFile | 查找一个文件 |
FindNextFile | 继续查找下一个文件 |
FindClose | 关闭一个查找句柄 |
宏:
宏 | 描述 |
---|---|
MAX_PATH | 文件路径最大长度 |
INFINITE | 用于指示无限等待的超时值 |
TRUE , FALSE | 逻辑值 TRUE 和 FALSE |
WAIT_OBJECT_0 | 等待对象的信号状态值,表示对象已经收到信号 |
WAIT_TIMEOUT | 等待超时的信号状态值 |
WAIT_FAILED | 等待失败的信号状态值 |
IN_CLASSA , IN_CLASSB , IN_CLASSC , IN_CLASSD , IN_CLASSE | 用于定义 IP 地址类别的常量 |
WINAPI
是一个宏,用于标记 Windows API 函数的调用约定。在Windows平台上,使用WINAPI宏声明的函数使用的是stdcall调用约定,这是一种在函数调用时处理函数参数和堆栈的标准方式。
- 参数传递顺序:参数是从右向左依次入栈的,即右边的参数先入栈,左边的参数后入栈。被调用函数会按照相反的顺序弹出这些参数。
- 堆栈清理:被调用的函数会负责清理调用堆栈。这意味着在调用函数后,调用者不需要负责清理堆栈。
为什么使用__stdcall?
- 约定性:使用标准调用约定能够确保在不同的函数之间有一个一致的接口和调用规则。
- 兼容性:许多 Windows API 函数都使用__stdcall调用约定。如果您编写的函数也使用相同的约定,可以更方便地与这些函数进行交互。
- 性能:由于清理堆栈是由被调用函数负责的,因此在某些情况下,__stdcall可以比其他调用约定更高效。
函数前面加上WINAPI通常是为了方便移植。
1.2 process.h
下面是 <process.h>
头文件中定义的一些常见数据类型、结构、函数和宏的详细说明:
数据类型:
数据类型 | 描述 |
---|---|
_pid_t | 进程 ID 的数据类型,通常是整数类型 |
_pipe_t | 管道的句柄类型 |
_fmode_t | 文件模式的数据类型,用于设置文件打开模式 |
_wfinddata_t | 用于 _wfindfirst() 和 _wfindnext() 函数的数据结构 |
_wfinddatai64_t | _wfindfirsti64() 和 _wfindnexti64() 函数的数据结构的 64 位版本 |
_wchdir_t | 用于 _wchdir() 函数的数据类型 |
_wexecle_t | 用于 _wexecle() 函数的数据类型 |
_wexecve_t | 用于 _wexecve() 函数的数据类型 |
_wexecvpe_t | 用于 _wexecvpe() 函数的数据类型 |
_wsearchenv_t | 用于 _wsearchenv() 函数的数据类型 |
_wsplitpath_t | 用于 _wsplitpath() 函数的数据类型 |
结构:
结构体 | 描述 |
---|---|
_finddata_t | 用于 _findfirst() 和 _findnext() 函数的数据结构 |
_finddatai64_t | _findfirsti64() 和 _findnexti64() 函数的数据结构的 64 位版本 |
_startupinfo | 进程的起始信息结构,用于指定新进程的主窗口的外观、标准输入输出和错误流 |
_PROCESS_INFORMATION | 进程信息结构,包含有关新进程及其主线程的信息 |
函数:
函数 | 描述 |
---|---|
_execl() | 用指定的参数列表执行一个新的程序 |
_execle() | 用指定的参数列表执行一个新的程序,并指定环境变量 |
_execlp() | 用指定的参数列表执行一个新的程序(带路径) |
_execlpe() | 用指定的参数列表执行一个新的程序(带路径和环境变量) |
_execv() | 用指定的参数列表执行一个新的程序 |
_execve() | 用指定的参数列表执行一个新的程序,并指定环境变量 |
_execvp() | 用指定的参数列表执行一个新的程序(带路径) |
_execvpe() | 用指定的参数列表执行一个新的程序(带路径和环境变量) |
_getpid() | 获取当前进程的进程 ID |
_getwch() | 从控制台获取一个宽字符 |
_getws() | 从控制台读取一个宽字符字符串 |
_pipe() | 创建一个管道,用于父子进程之间的通信 |
_spawnl() | 用指定的参数列表创建一个新的进程 |
_spawnle() | 用指定的参数列表创建一个新的进程,并指定环境变量 |
_spawnlp() | 用指定的参数列表创建一个新的进程(带路径) |
_spawnlpe() | 用指定的参数列表创建一个新的进程(带路径和环境变量) |
_spawnv() | 用指定的参数列表创建一个新的进程 |
_spawnve() | 用指定的参数列表创建一个新的进程,并指定环境变量 |
_spawnvp() | 用指定的参数列表创建一个新的进程(带路径) |
_spawnvpe() | 用指定的参数列表创建一个新的进程(带路径和环境变量) |
_wexecl() | 用指定的参数列表执行一个新的程序(宽字符) |
2. 创建线程
在Windows下,可以使用CreateThread
函数来创建线程。其原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
函数参数说明:
lpThreadAttributes
:指向SECURITY_ATTRIBUTES
结构的指针,用于指定新线程的安全属性。通常为 NULL,表示使用默认的安全属性。
dwStackSize
:指定新线程的堆栈大小,以字节为单位。如果为 0,则新线程使用与创建线程的进程相同的堆栈大小。
lpStartAddress
:指向线程函数的指针,表示新线程从哪个函数开始执行。线程函数的原型应为DWORD WINAPI ThreadFunc(LPVOID lpParam)
,其中lpParam
参数可以接收lpParameter
参数的值。
lpParameter
:传递给线程函数的参数,可以是任意类型的指针。该参数将被传递给lpStartAddress
指向的线程函数。
dwCreationFlags
:指定线程的创建标志。
lpThreadId
:用于接收新线程的线程 ID。如果为 NULL,则不返回线程 ID。
dwCreationFlags:
这些标志可以单独使用,也可以使用按位 OR 操作符 | 组合使用,以实现更复杂的创建标志设置。若为0,则表示不设置任何标志,即使用默认的创建标志。在这种情况下,新线程会立即开始执行,不会挂起,也不会设置其他特殊的创建选项。
标志 | 描述 |
---|---|
CREATE_SUSPENDED | 创建后线程处于挂起状态,需要调用 ResumeThread 才能开始执行 |
STACK_SIZE_PARAM_IS_A_RESERVATION | dwStackSize 参数指定的是堆栈的保留大小,而不是真实的堆栈大小 |
CREATE_NEW_CONSOLE | 为新进程创建一个新的控制台窗口 |
CREATE_UNICODE_ENVIRONMENT | 使用 Unicode 环境变量 |
DETACHED_PROCESS | 新进程将不与其父进程有关联,父进程退出时不会影响子进程 |
CREATE_NO_WINDOW | 新进程不会创建窗口 |
CREATE_DEFAULT_ERROR_MODE | 新进程将使用默认的错误模式 |
CREATE_NEW_PROCESS_GROUP | 将新进程创建为一个新的进程组,使其成为新会话的首要进程 |
返回值说明:
- 如果函数调用成功,将返回新线程的句柄,可以使用这个句柄操作新线程。
- 如果函数调用失败,返回值为 NULL。可以使用
GetLastError()
获取具体的错误信息。
下面是一个简单的例子,演示如何创建一个线程并执行线程函数:
#include<windows.h>
#include<stdio.h>
// 定义一个线程函数,打印参数值
DWORD WINAPI ThreadFunc(LPVOID lpParam) {
int* p = (int*)lpParam;
printf("Thread is running with parameter: %d\n",*p);
return 0;
}
int main() {
// 创建一个句柄
HANDLE hTheard;
// 线程id变量
DWORD dwThreadId;
int param = 123;
hTheard = CreateThread(
NULL, // 安全性
0, // 线程堆栈大小
ThreadFunc, // 线程函数指针
¶m, // 线程函数参数
0, // 线程创建标志,默认,立即执行
&dwThreadId); // 接收线程id
// 检查是否创建成功
if (hTheard == NULL) {
DWORD err = GetLastError();
printf("Failed to create Thread with code %d.\n",err);
return 1;
}
// 等待线程结束
WaitForSingleObject(hTheard,INFINITE);
// 关闭线程
CloseHandle(hTheard);
return 0;
}
3. 线程同步
线程同步是指多个线程之间协调工作,以确保它们正确地访问共享资源并按照预期顺序执行。在多线程编程中,当多个线程同时访问共享资源时,可能会出现以下问题:
- 竞态条件(Race Condition):多个线程竞争同时对共享资源进行读写操作,导致结果不确定或不正确。
- 死锁(Deadlock):多个线程相互等待对方释放资源而无法继续执行。
- 活锁(Livelock):线程之间互相响应对方的动作而无法继续执行,类似于死锁但线程还在运行。
- 饿死(Starvation):某个线程由于优先级低或者其他原因,一直无法获取到所需的资源,无法继续执行。
为了解决这些问题,需要使用线程同步机制来确保线程之间的正确协作。线程同步的目的是确保:
- 各个线程按照规定的顺序访问共享资源,避免竞态条件。
- 线程之间相互协作、通信,确保工作按照预期顺利进行。
- 避免死锁和活锁等线程间互相等待的情况发生。
3.1 线程同步方式
常用线程同步方式:
同步方式 | 描述 | 常用场景 |
---|---|---|
互斥量(Mutex) | 用于保护临界区资源,防止多个线程同时访问 | 对共享资源的独占访问,如文件操作、数据库操作等 |
临界区(Critical Section) | 用于保护临界资源的轻量级同步对象,只适用于同一进程内的线程同步 | 对临界资源的保护,需要高效的同步机制 |
信号量(Semaphore) | 控制对资源的访问,允许多个线程同时访问同一资源 | 有限资源的控制,如线程池、连接池等 |
事件(Event) | 用于线程之间的通信和同步,允许一个或多个线程等待事件的状态改变 | 线程间的信号通知和同步等 |
条件变量(Condition Variable) | 用于线程等待某个条件成立时再继续执行 | 生产者-消费者模型中,消费者等待生产者产生数据 |
读写锁(Read-Write Lock) | 允许多个线程同时读取共享数据,但只允许一个线程写入数据 | 对于读操作频繁、写操作较少的情况 |
自旋锁(Spin Lock) | 一种忙等待的同步机制,用于临界区很小,不希望线程切换的情况 | 对于临界区非常短小,不希望线程切换带来的开销 |
事件计数器(Event Counters) | 表示一个或多个事件的发生次数,用于线程之间的通信 | 控制多个事件的发生次数,线程等待特定数量的事件发生 |
互斥量(Mutex)
- 互斥量是一种同步对象,用于保护临界区资源,防止多个线程同时访问。
- 通过互斥量,只有拥有互斥量的线程可以进入临界区。
临界区(Critical Section)
- 临界区是一种轻量级的同步对象,类似于互斥量,用于保护临界资源。
- 临界区通常比互斥量更快速,但只能用于同一进程内的线程同步。
信号量(Semaphore)
- 信号量是一种计数器,控制对资源的访问,允许多个线程同时访问同一资源。
- 信号量的值表示可用资源的数量,当资源被占用时,信号量减少;当资源释放时,信号量增加。
事件(Event)
- 事件用于线程之间的通信和同步,允许一个或多个线程等待某个事件的状态改变。
- 事件有两种状态:有信号(signaled)和无信号(nonsignaled)。
条件变量(Condition Variable)
- 条件变量用于线程等待某个条件成立时再继续执行。
- 一般和互斥量结合使用,当条件不满足时,线程进入等待状态并释放互斥量;当条件满足时,线程被唤醒继续执行。
读写锁(Read-Write Lock)
- 读写锁允许多个线程同时读取共享数据,但只允许一个线程写入数据。
- 读锁可以多个线程同时持有,写锁只能被一个线程持有。
自旋锁(Spin Lock)
- 自旋锁是一种忙等待的同步机制,当某个线程尝试获得锁时,如果锁被其他线程持有,则该线程会一直循环等待直到锁被释放。
- 自旋锁适用于临界区很小,不希望线程切换的情况。
事件计数器(Event Counters)
- 事件计数器用于线程之间的通信,表示一个或多个事件的发生次数。
- 可以等待事件计数器的值达到某个特定值,然后继续执行。
本文暂且只介绍互斥量和事件。
3.1 互斥量(Mutex)
在 Windows 中,可以使用 CreateMutex()
函数来创建互斥量(Mutex)。互斥量是一种同步对象,用于控制多个线程对共享资源的访问。只有一个线程可以拥有一个互斥量,当一个线程拥有互斥量时,其他线程需要等待这个互斥量被释放才能访问被保护的资源。
原型:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
lpMutexAttributes
: 一个指向SECURITY_ATTRIBUTES
结构的指针,决定了新创建的互斥量的安全描述符。通常情况下可以设为NULL
。bInitialOwner
: 如果为TRUE
,表示调用线程拥有互斥量;如果为FALSE
,表示互斥量是未拥有的。通常情况下可以设为FALSE
,除非你确实需要在创建时让某个线程拥有这个互斥量。lpName
: 互斥量的名称,可以为NULL
。如果互斥量是局部的,可以设为NULL
;如果要在多个进程中共享互斥量,可以给互斥量取一个名字。
释放信号量使用:ReleaseMutex()
。获取:WaitForSingleObject()
。
互斥量用于保护临界区,确保同时只有一个线程可以访问共享资源。示例代码如下:
#include<Windows.h>
#include<stdio.h>
HANDLE hMutex;
int sharedata = 0;
DWORD WINAPI threadFunc(LPVOID lpParam) {
for (int i = 0; i < 5; i++) {
// 获取互斥量
WaitForSingleObject(hMutex,INFINITE);
sharedata++;
// 释放互斥量
ReleaseMutex(hMutex);
}
return 0;
}
int main() {
hMutex = CreateMutex(NULL,FALSE,NULL);
if (hMutex == NULL) {
printf("Create Mutex error.\n");
return 1;
}
HANDLE hThraed1, hThread2;
hThraed1 = CreateThread(NULL, 0, threadFunc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, threadFunc, NULL, 0, NULL);
if (hThraed1 == NULL || hThread2 == NULL) {
printf("Create thread failed.\n");
return 1;
}
WaitForSingleObject(hThraed1,1);
WaitForSingleObject(hThread2,INFINITE);
CloseHandle(hThraed1);
CloseHandle(hThread2);
CloseHandle(hMutex);
printf("The final value of sharedata is :%d\n",sharedata);
return 0;
}
注:
WaitForSingleObject()
是 Windows API 中用于等待一个对象的函数。在多线程编程中,它通常用于等待线程结束、等待信号量、事件等。这个函数的原型如下:
DWORD WaitForSingleObject(
HANDLE hHandle, // 要等待的对象的句柄
DWORD dwMilliseconds // 等待的时间(毫秒),单位为毫秒,INFINITE 表示无限等待
);
3.2 事件(Event)
CreateEvent 函数是 Windows API 中用于创建事件对象的函数。事件对象可以用于线程间的同步和通信,允许一个线程等待其他线程的某个事件发生后再继续执行。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
参数:
lpEventAttributes
:指向SECURITY_ATTRIBUTES
结构的指针,用于指定事件对象的安全属性。一般情况下设为NULL
即可。bManualReset
:指定事件是手动重置还是自动重置。如果为TRUE
,表示事件为手动重置,需要显式调用ResetEvent
函数来重置事件;如果为FALSE
,表示事件为自动重置,当一个等待的线程被释放后,事件自动重置为未触发状态。一般情况下,我们常用自动重置。bInitialState
:指定事件的初始状态。如果为TRUE
,表示初始状态为有信号(事件已触发);如果为FALSE
,表示初始状态为无信号(事件未触发)。lpName
:指定事件对象的名字。如果为NULL
,则创建一个匿名事件。
返回值:
- 如果函数成功,返回一个事件对象的句柄
HANDLE
; - 如果函数失败,返回
NULL
。可以调用GetLastError()
函数获取错误信息。
示例代码如下:
#include<windows.h>
#include<stdio.h>
HANDLE hEvent;
int sharedData = 0;
DWORD WINAPI Thread1Func(LPVOID lpParam) {
WaitForSingleObject(hEvent, INFINITE);
sharedData = 1;
printf("Thread 1 sets sharedData to 1\n");
return 0;
}
DWORD WINAPI Thread2Func(LPVOID lpParam) {
Sleep(100); // 等待100毫秒
SetEvent(hEvent);
printf("Thread 2 signals the event\n");
return 0;
}
int main() {
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (hEvent == NULL)return 1;
HANDLE hThread1, hThread2;
hThread1 = CreateThread(NULL, 0, Thread1Func, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Thread2Func, NULL, 0, NULL);
if (hThread1==NULL||hThread2==NULL)return 1;
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hEvent);
printf("Final sharedData value: %d\n", sharedData);
return 0;
}
4. 线程的结束与资源管理
结束方式:
return
:最简单的方法是让线程函数执行完毕并返回。ExitThread()
:在任何时候,线程都可以调用ExitThread()来终止自己。TerminateThread()
:可以用来强制终止一个线程,但应该慎用,因为它可能导致资源泄漏或者使程序处于不一致的状态。
DWORD WINAPI ThreadFunc(LPVOID lpParam) {
// 线程执行的代码
return 0;
// ExitThread(0);
}
int main() {
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &dwThreadId);
// 主线程等待一段时间后强制终止线程
// Sleep(2000);
// TerminateThread(myThread, 0);
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
还可以使用PostThreadMessage()
函数向指定线程发送一个消息,让线程在处理这个消息时结束自己。但要确保线程有消息循环,通常在GUI线程中使用较多。
#include <stdio.h>
#include <windows.h>
#define WM_QUIT_THREAD (WM_USER + 1)
DWORD WINAPI myThreadFunction(LPVOID lpParam) {
printf("Thread is running...\n");
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
if (msg.message == WM_QUIT_THREAD) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
printf("Thread is ending...\n");
return 0;
}
int main() {
HANDLE myThread = CreateThread(NULL, 0, myThreadFunction, NULL, 0, NULL);
// 主线程等待一段时间后向线程发送消息结束它
Sleep(5000);
PostThreadMessage(GetThreadId(myThread), WM_QUIT_THREAD, 0, 0);
// 等待线程结束
WaitForSingleObject(myThread, INFINITE);
printf("Thread has ended.\n");
CloseHandle(myThread);
return 0;
}
5.线程池(简要)
线程池(Thread Pool
)是一种线程管理的机制,它包含了多个预先创建好的线程,这些线程可以被重复使用来执行多个任务,而不需要每次都创建新的线程。线程池在多线程编程中被广泛应用,它的主要目的是提高线程的利用率和减少线程创建和销毁的开销。
原理和优势:
- 重用线程:线程池中的线程被预先创建并保持在池中,可以被反复使用来处理不同的任务,而不需要每次都创建新的线程。这样可以避免线程的频繁创建和销毁,提高了系统的性能和效率。
- 减少资源开销:线程的创建和销毁会消耗系统的资源,包括内存和CPU时间。线程池可以减少这些开销,因为线程一旦创建就可以被重复利用,不需要频繁地分配和回收资源。
- 控制并发数量:通过设置线程池的大小,可以限制系统中并发执行的线程数量,避免因为过多的线程而导致资源竞争和性能下降的问题。
- 提高响应速度:线程池中的线程可以立即执行任务,不需要等待新线程的创建,从而减少了任务开始执行的延迟,提高了系统的响应速度。
工作流程:
- 初始化线程池:预先创建一定数量的线程,并将它们放入线程池中。
- 任务提交:当有任务需要执行时,将任务提交给线程池。
- 任务执行:线程池中的空闲线程会从任务队列中取出任务并执行。
- 任务完成:任务执行完毕后,线程会返回线程池,并等待下一个任务。
- 线程池销毁:当线程池不再需要时,可以销毁线程池中的线程。
使用场景:
- 服务器编程:在服务器程序中,需要处理大量的客户端请求。使用线程池可以有效地管理这些请求,提高服务器的并发能力。
- 多任务处理:在计算密集型或IO密集型的任务中,可以使用线程池来管理和执行这些任务,提高系统的效率。
- 图像处理:对大量图片进行处理时,可以使用线程池来并行处理这些图片,加快处理速度。
示例:
#include <Windows.h>
#include <stdio.h>
#define MAX_THREADS 4
#define TASKS 8
// 任务结构
typedef struct {
int task_id;
} Task;
// 线程池结构
typedef struct {
HANDLE threads[MAX_THREADS]; // 线程句柄数组
CRITICAL_SECTION lock; // 临界区对象,用于线程安全操作
HANDLE events[MAX_THREADS]; // 事件对象数组,用于线程间的同步
Task* task_queue[TASKS]; // 任务队列
int task_count; // 任务队列中的任务数量
int active_threads; // 活动线程的数量
} ThreadPool;
// 工作线程函数
DWORD WINAPI Worker(LPVOID arg) {
ThreadPool* pool = (ThreadPool*)arg;
while (1) {
WaitForSingleObject(pool->events[pool->active_threads], INFINITE); // 等待事件触发
EnterCriticalSection(&pool->lock); // 进入临界区
if (pool->task_count == 0) { // 检查任务队列是否为空
LeaveCriticalSection(&pool->lock);
break;
}
Task* task = pool->task_queue[--pool->task_count]; // 从任务队列取出一个任务
LeaveCriticalSection(&pool->lock); // 离开临界区
printf("Thread %ld processing task %d\n", GetCurrentThreadId(), task->task_id);
// 模拟任务执行时间
Sleep(1000);
free(task); // 释放任务内存
SetEvent(pool->events[pool->active_threads]); // 触发下一个线程的事件
}
return 0;
}
// 初始化线程池
void ThreadPoolInit(ThreadPool* pool) {
pool->task_count = 0;
pool->active_threads = MAX_THREADS;
InitializeCriticalSection(&pool->lock); // 初始化临界区对象
for (int i = 0; i < MAX_THREADS; ++i) {
pool->events[i] = CreateEvent(NULL, FALSE, TRUE, NULL); // 创建自动重置的有信号事件
pool->threads[i] = CreateThread(NULL, 0, Worker, (LPVOID)pool, 0, NULL); // 创建工作线程
}
}
// 提交任务到线程池
void SubmitTask(ThreadPool* pool, Task* task) {
EnterCriticalSection(&pool->lock); // 进入临界区
pool->task_queue[pool->task_count++] = task; // 将任务加入到任务队列
SetEvent(pool->events[--pool->active_threads]); // 触发一个空闲线程的事件
LeaveCriticalSection(&pool->lock); // 离开临界区
}
// 销毁线程池
void DestroyThreadPool(ThreadPool* pool) {
for (int i = 0; i < MAX_THREADS; ++i) {
WaitForSingleObject(pool->threads[i], INFINITE); // 等待工作线程结束
CloseHandle(pool->threads[i]); // 关闭线程句柄
CloseHandle(pool->events[i]); // 关闭事件对象句柄
}
DeleteCriticalSection(&pool->lock); // 删除临界区对象
}
// 主函数
int main() {
ThreadPool pool;
ThreadPoolInit(&pool); // 初始化线程池
// 创建一些任务并提交到线程池
for (int i = 0; i < TASKS; ++i) {
Task* task = (Task*)malloc(sizeof(Task));
task->task_id = i + 1;
SubmitTask(&pool, task);
}
// 等待一段时间,让任务执行
Sleep(5000);
// 销毁线程池
DestroyThreadPool(&pool);
return 0;
}