【C语言】Windows下的C语言线程编程详解

news2025/1/12 2:43:46

文章目录

  • 1. 头文件
    • 1.1 windows.h
    • 1.2 process.h
  • 2. 创建线程
  • 3. 线程同步
    • 3.1 线程同步方式
    • 3.1 互斥量(Mutex)
    • 3.2 事件(Event)
  • 4. 线程的结束与资源管理
  • 5.线程池(简要)

在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逻辑值类型,可以是 TRUEFALSE
BYTE8 位无符号整数
CHAR8 位字符类型
DWORD32 位无符号整数
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逻辑值 TRUEFALSE
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_RESERVATIONdwStackSize 参数指定的是堆栈的保留大小,而不是真实的堆栈大小
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,  // 线程函数指针
		&param,       // 线程函数参数
		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. 线程同步

线程同步是指多个线程之间协调工作,以确保它们正确地访问共享资源并按照预期顺序执行。在多线程编程中,当多个线程同时访问共享资源时,可能会出现以下问题:

  1. 竞态条件(Race Condition):多个线程竞争同时对共享资源进行读写操作,导致结果不确定或不正确。
  2. 死锁(Deadlock):多个线程相互等待对方释放资源而无法继续执行。
  3. 活锁(Livelock):线程之间互相响应对方的动作而无法继续执行,类似于死锁但线程还在运行。
  4. 饿死(Starvation):某个线程由于优先级低或者其他原因,一直无法获取到所需的资源,无法继续执行。

为了解决这些问题,需要使用线程同步机制来确保线程之间的正确协作。线程同步的目的是确保:

  • 各个线程按照规定的顺序访问共享资源,避免竞态条件。
  • 线程之间相互协作、通信,确保工作按照预期顺利进行。
  • 避免死锁和活锁等线程间互相等待的情况发生。

3.1 线程同步方式

常用线程同步方式:

同步方式描述常用场景
互斥量(Mutex)用于保护临界区资源,防止多个线程同时访问对共享资源的独占访问,如文件操作、数据库操作等
临界区(Critical Section)用于保护临界资源的轻量级同步对象,只适用于同一进程内的线程同步对临界资源的保护,需要高效的同步机制
信号量(Semaphore)控制对资源的访问,允许多个线程同时访问同一资源有限资源的控制,如线程池、连接池等
事件(Event)用于线程之间的通信和同步,允许一个或多个线程等待事件的状态改变线程间的信号通知和同步等
条件变量(Condition Variable)用于线程等待某个条件成立时再继续执行生产者-消费者模型中,消费者等待生产者产生数据
读写锁(Read-Write Lock)允许多个线程同时读取共享数据,但只允许一个线程写入数据对于读操作频繁、写操作较少的情况
自旋锁(Spin Lock)一种忙等待的同步机制,用于临界区很小,不希望线程切换的情况对于临界区非常短小,不希望线程切换带来的开销
事件计数器(Event Counters)表示一个或多个事件的发生次数,用于线程之间的通信控制多个事件的发生次数,线程等待特定数量的事件发生
  1. 互斥量(Mutex)

    • 互斥量是一种同步对象,用于保护临界区资源,防止多个线程同时访问。
    • 通过互斥量,只有拥有互斥量的线程可以进入临界区。
  2. 临界区(Critical Section)

    • 临界区是一种轻量级的同步对象,类似于互斥量,用于保护临界资源。
    • 临界区通常比互斥量更快速,但只能用于同一进程内的线程同步。
  3. 信号量(Semaphore)

    • 信号量是一种计数器,控制对资源的访问,允许多个线程同时访问同一资源。
    • 信号量的值表示可用资源的数量,当资源被占用时,信号量减少;当资源释放时,信号量增加。
  4. 事件(Event)

    • 事件用于线程之间的通信和同步,允许一个或多个线程等待某个事件的状态改变。
    • 事件有两种状态:有信号(signaled)和无信号(nonsignaled)。
  5. 条件变量(Condition Variable)

    • 条件变量用于线程等待某个条件成立时再继续执行。
    • 一般和互斥量结合使用,当条件不满足时,线程进入等待状态并释放互斥量;当条件满足时,线程被唤醒继续执行。
  6. 读写锁(Read-Write Lock)

    • 读写锁允许多个线程同时读取共享数据,但只允许一个线程写入数据。
    • 读锁可以多个线程同时持有,写锁只能被一个线程持有。
  7. 自旋锁(Spin Lock)

    • 自旋锁是一种忙等待的同步机制,当某个线程尝试获得锁时,如果锁被其他线程持有,则该线程会一直循环等待直到锁被释放。
    • 自旋锁适用于临界区很小,不希望线程切换的情况。
  8. 事件计数器(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. 线程的结束与资源管理

结束方式:

  1. return:最简单的方法是让线程函数执行完毕并返回。
  2. ExitThread():在任何时候,线程都可以调用ExitThread()来终止自己。
  3. 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)是一种线程管理的机制,它包含了多个预先创建好的线程,这些线程可以被重复使用来执行多个任务,而不需要每次都创建新的线程。线程池在多线程编程中被广泛应用,它的主要目的是提高线程的利用率和减少线程创建和销毁的开销。

原理和优势:

  1. 重用线程:线程池中的线程被预先创建并保持在池中,可以被反复使用来处理不同的任务,而不需要每次都创建新的线程。这样可以避免线程的频繁创建和销毁,提高了系统的性能和效率。
  2. 减少资源开销:线程的创建和销毁会消耗系统的资源,包括内存和CPU时间。线程池可以减少这些开销,因为线程一旦创建就可以被重复利用,不需要频繁地分配和回收资源。
  3. 控制并发数量:通过设置线程池的大小,可以限制系统中并发执行的线程数量,避免因为过多的线程而导致资源竞争和性能下降的问题。
  4. 提高响应速度:线程池中的线程可以立即执行任务,不需要等待新线程的创建,从而减少了任务开始执行的延迟,提高了系统的响应速度。

工作流程:

  • 初始化线程池:预先创建一定数量的线程,并将它们放入线程池中。
  • 任务提交:当有任务需要执行时,将任务提交给线程池。
  • 任务执行:线程池中的空闲线程会从任务队列中取出任务并执行。
  • 任务完成:任务执行完毕后,线程会返回线程池,并等待下一个任务。
  • 线程池销毁:当线程池不再需要时,可以销毁线程池中的线程。
    在这里插入图片描述

使用场景:

  • 服务器编程:在服务器程序中,需要处理大量的客户端请求。使用线程池可以有效地管理这些请求,提高服务器的并发能力。
  • 多任务处理:在计算密集型或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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1514385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

隧道技术和代理技术(三)

隧道技术 知识点 -隧道技术&#xff1a;解决不出网协议上线的问题&#xff08;利用出网协议进行封装出网&#xff09; -代理技术&#xff1a;解决网络通讯不通的问题&#xff08;利用跳板机建立节点后续操作&#xff09; 内环境示意图&#xff0c;方便理解 思路&#xff1a;…

【网络原理】UDP协议的详细解析

文章目录 &#x1f384;UDP协议概念&#x1f332;UDP协议端格式&#x1f384;UDP的特点&#x1f338;无连接&#x1f338;不可靠传输&#x1f338;面向数据报&#x1f338;缓冲区&#x1f338;全双工&#x1f338;大小受限 &#x1f340;基于UDP的应用层协议&#x1f38d;扩展问…

(BAT向)Java岗常问高频面试汇总:MyBatis 微服务 Spring 分布式 MySQL等

看面试题可以是为了面试&#xff0c;也可以是对自己学到的东西的一种查漏补缺&#xff0c;更加深刻的去了解一些核心知识点 Spring面试高频问题 问题一&#xff1a;谈 需要zi料 绿色徽【vip1024b】 谈你对spring IOC 和 DI 的理解&#xff0c;它们有什么区别&#xff1f; **问题…

全球首个AI程序员Devin诞生,真的不再需要程序员了吗?

前言 今年到底是怎么回事&#xff1f;前有Sora(首个文生视频大模型)独领风骚&#xff0c;后有Mistral&#xff08;号称世界第二语言大模型&#xff09;横刀立马&#xff0c;甚至Claude3的热度都还没有过&#xff0c;今天一则更炸裂的消息就爆出&#xff0c;世界上第一个AI程序…

代码贴--链表--数据机构

本博客将记录链表代码(单链表)&#xff0c;后续其他链表和其他数据结构内容请看我的其他博客 头文件(SList.h) #pragma once #include<iostream> #include<bits/stdc.h> using namespace std;typedef int SLTDataType;struct SListNode {int data;struct SListNo…

windows系统玩游戏找不到d3dx9_43.dll缺失,无法启动此程序的解决方法

今日&#xff0c;我们要深入讨论d3dx9_43.dll文件的重要性及其缺失问题。最近&#xff0c;我也遇到了这个文件丢失的困扰&#xff0c;因此想借此机会与大家分享如何解决d3dx9_43.dll缺失的问题。 一.电脑d3dx9_43.dll丢失会提示什么&#xff1f; 关于电脑提示d3dx9_43.dll丢失…

【人力资源开发】某主题公园人力资源开发管理咨询项目纪实

虽然很多企业将“人事部”改为“人力资源部”&#xff0c;但是企业的人力资源管理水平却仍停留在“人事管理”的阶段。该主题公园也是如此。随着公园的不断发展&#xff0c;其人力资源管理问题逐渐显露&#xff0c;而管理者也不清楚问题的根源在哪里&#xff0c;只能采取“头疼…

AWS入门实践-AWS CLI工具的使用介绍

AWS CLI&#xff08;Amazon Web Services Command Line Interface&#xff09;是一个强大的工具&#xff0c;它允许您直接从命令行与AWS服务进行交互。这不仅可以加快许多任务的处理速度&#xff0c;而且还可以通过脚本自动化。 一、AWS CLI工具的安装 1、Windows 安装下载…

java-双列集合

什么是双列集合&#xff1f; 集合中每次存的数据是成对存入的 以及它的特点是什么&#xff1f; 特别注意&#xff1a;键不可重复&#xff0c;值可以 Map是双列集合的顶层接口 Map 它有哪些方法呢&#xff1f; Map的常用API 添加 添加操作的代码如下 我们要明白一些细节&…

【项目笔记】java微服务:黑马头条(day02)

文章目录 app端文章查看&#xff0c;静态化freemarker,分布式文件系统minIO1)文章列表加载1.1)需求分析1.2)表结构分析1.3)导入文章数据库1.3.1)导入数据库1.3.2)导入对应的实体类 1.4)实现思路1.5)接口定义1.6)功能实现1.6.1)&#xff1a;导入heima-leadnews-article微服务&am…

C++初阶:内存管理

目录 1. C/C中各种资源的内存分布1.1 C/C程序内存区域划分1.2 各资源的内存分布情况&#xff08;练习&#xff09; 2. C中的动态内存管理方式2.1 new/delete开辟内置类型空间2.2 new/delete开辟销毁自定义类型空间 3. operator new 与 operator delete函数4. new与delete的实现…

逆变器功率软起斜率要求

安规说明 在NB32004中&#xff0c;有明确要求&#xff0c;有功功率调整速率不得超过正负10%Pn/min&#xff0c;包括起停机。 控制对象 控制功率最终是通过调整D轴电流给定来达到限制功率的目的&#xff0c;所以我们只要让D轴的电流给定限幅值按照10%/min增加就好了。 具体实…

openAI key 与ChatGPTPlus的关系,如何升级ChatGPTPLus

一、前言 先详细介绍一下Plus会员和Open API之间的区别&#xff1a; 实际上&#xff0c;这两者是相互独立的。举例来说&#xff0c;虽然您开通了Plus会员&#xff0c;并不意味着您就可以使用4.0版本的API。尽管这两个账户可以是同一个&#xff0c;但它们是完全独立的平台。 …

ChatGPT提问技巧:可解释的软提示

ChatGPT提问技巧&#xff1a;可解释的软提示 可解释的软提示是一种既能控制模型生成的文本&#xff0c;又能为模型提供一定灵活性的技术。 具体做法是为模型提供一组受控输入和一些有关所需输出的附加信息。这种技术可以使生成的文本更具可解释性和可控性。 提示示例及其公式…

DAY by DAY 史上最全的Linux常用命令汇总----man

man是按照手册的章节号的顺序进行搜索的。 man设置了如下的功能键&#xff1a; 功能键 功能 空格键 显示手册页的下一屏 Enter键 一次滚动手册页的一行 b 回滚一屏 f 前滚一屏 q 退出man命令 h 列出所有功能键 /word 搜索word字符串 注意&#xff1a…

每日一题——LeetCode1678.设计Goal解析器

方法一 splice 将字符串转为数组&#xff0c;对数组进行遍历&#xff0c;碰到G保持不变&#xff0c;继续循环&#xff0c;碰到 ( 看他后一位&#xff0c;是 ) 则删除两个元素&#xff0c;添加一个 o &#xff0c;不是则删除四个元素&#xff0c;添加元素 al &#xff0c;最后将…

JS ATM练习案例(复习循环知识)

需求&#xff1a;用户可以选择存钱、取钱、查看余额和退出功能。 分析&#xff1a;1循环时反复出现提示框&#xff0c;所以提示框写到循环里面。 2.退出的条件是4&#xff0c;所以是4就会结束循环 3.提前准备一个金额预存储 4取钱为减法操作&#xff0c;存钱为加法操作&#xf…

【论文阅读】Vision Mamba:双向状态空间模型的的高效视觉表示学习

文章目录 Vision Mamba:双向状态空间模型的的高效视觉表示学习摘要介绍相关工作用于视觉应用的状态空间模型 方法准备视觉MambaVim块结构细节高效分析计算效率 实验图片分类语义分割目标检测和实例分割消融实验双向SSM分类设计 总结和未来工作 论文地址&#xff1a; Vision Mam…

【C语言】qsort函数的使用

&#x1f451;个人主页&#xff1a;啊Q闻 &#x1f387;收录专栏&#xff1a;《C语言》 &#x1f389;道阻且长&#xff0c;行则将至 前言 这篇博客主要是介绍qsort函数的使用&#xff0c;以及利用冒泡排序的方式模拟实现qsort 一.初识qsort函数 我们可以在…

LJXpaper

表1-3引用出现较滞后 1.3文献[42]有问题 如图 如图 如图 如图 &#x1f447; &#x1f447; &#x1f447; &#x1f447; &#x1f447; &#x1f447; &#x1f447; 要不要加连接词&#xff1a;4-11 4-12之间 &#…