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

这篇具有很好参考价值的文章主要介绍了【C语言】Windows下的C语言线程编程详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在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
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 逻辑值 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_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,  // 线程函数指针
		&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()
windows中c语言线程如何创建,C Language,c语言,windows,开发语言

互斥量用于保护临界区,确保同时只有一个线程可以访问共享资源。示例代码如下:

#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. 提高响应速度:线程池中的线程可以立即执行任务,不需要等待新线程的创建,从而减少了任务开始执行的延迟,提高了系统的响应速度。

工作流程:

  • 初始化线程池:预先创建一定数量的线程,并将它们放入线程池中。
  • 任务提交:当有任务需要执行时,将任务提交给线程池。
  • 任务执行:线程池中的空闲线程会从任务队列中取出任务并执行。
  • 任务完成:任务执行完毕后,线程会返回线程池,并等待下一个任务。
  • 线程池销毁:当线程池不再需要时,可以销毁线程池中的线程。
    windows中c语言线程如何创建,C Language,c语言,windows,开发语言

使用场景:

  • 服务器编程:在服务器程序中,需要处理大量的客户端请求。使用线程池可以有效地管理这些请求,提高服务器的并发能力。
  • 多任务处理:在计算密集型或IO密集型的任务中,可以使用线程池来管理和执行这些任务,提高系统的效率。
  • 图像处理:对大量图片进行处理时,可以使用线程池来并行处理这些图片,加快处理速度。

示例:文章来源地址https://www.toymoban.com/news/detail-861731.html

#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;
}

到了这里,关于【C语言】Windows下的C语言线程编程详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Windows网络自学的第一天:创建线程

    目录 一、创建线程 CreateThread函数:  下面是示例: ​编辑 ThreadProc函数解释:     DWORD的本质是 unsigned long    PVOID的本质是 void* 二、线程的终止 1.WaitForSingleObject()函数: 示例如下: 2.ExitThread()函数: 示例如下: 3.TerminateThread()函数: 4.CloseHandle()函数: 5.正常return 0; 三、线程的

    2024年02月14日
    浏览(39)
  • 【Windows系统编程】03.远线程注入ShellCode

    shellcode:本质上也是一段普通的代码,只不过特殊的编程手法,可以在任意环境下,不依赖于原有的依赖库执行。 kali中借助生成shellcode: 桌面打开终端: 搜索payload: 另外打开一个终端: 回车,shellcode就出来了,复制出来就行了 然后我们来继续写代码: 这里给出我写出的

    2024年02月12日
    浏览(46)
  • QT安装OpenCv步骤流程及相关问题(基于Windows下的QT Creator编程)

       QT里安装OpenCV环境过程比较复杂,如果网友们想要在QT里做OpenCV的开发,就需要耐心的跟着我列出来的操作步骤一步一步去安装环境,其中包含我在安装环境时遇到的坑,都已经在步骤中帮大家罗列出来,如有不懂的网友,可以评论或者私信我即可! 1、Qt软件 2、CMake 3、

    2024年02月16日
    浏览(45)
  • Ubuntu (Linux) 下创建软链接(即符号链接,相当于windows下的快捷方式)方法

    使用创建软链接的命令 Reference(注) :

    2024年01月24日
    浏览(64)
  • CENTOS上的网络安全工具(二十四)Windows下的Hadoop+Spark编程环境构建

            前面我们搭建了hadoop集群,spark集群,也利用容器构建了spark的编程环境。但是一般来说,就并行计算程序的开发,一刚开始一般是在单机上的,比如hadoop的single node。但是老师弄个容器或虚拟机用vscode远程访问式开发,终究还是有些不爽。还好,hadoop和spark都是支持

    2024年02月09日
    浏览(66)
  • 【Java 基础篇】Java多线程编程详解:线程创建、同步、线程池与性能优化

    Java是一门强大的编程语言,其中最引人注目的特性之一是多线程支持。多线程允许我们在同一程序中同时执行多个任务,这大大提高了应用程序的性能和响应能力。本文将深入介绍Java线程的基础知识,无论您是初学者还是有一些经验的开发人员,都将从中获益。 在计算机科

    2024年02月07日
    浏览(55)
  • 【C语言基础】01环境安装 Windows下的CLion开发环境的安装

    资源:放在评论区中 把压缩包拖拽到C盘根目录,一键解压压缩包,得到文件夹mingw64 点击CLion.exe,运行安装程序 路径为默认安装,如需更改,注意路径中不要带有中文. Installation Options 全部勾选 选择我想要之后重启电脑 点击CLion,进入内部设置 点击下方continue 继续 存放在D盘,不要有中

    2024年02月01日
    浏览(50)
  • Windows下的Spark环境配置(含IDEA创建工程--《Spark大数据技术与应用》第九章-菜品推荐项目)

    本文适用于《Spark大数据技术与应用》第九章-菜品推荐项目环境配置:` 跟着做就行… 资源都在网盘里面,纯粹的无脑配置… 提示:以下是本篇文章正文内容,所用资源版本过低,用于课本实验 ,且已有Java环境 scala:2.12.8 spark:1.6.2 hadoop:2.6.4 hadoop启动文件exe JAVA 如果按照

    2024年02月09日
    浏览(53)
  • 如何设计WIndows系统下的单例进程程序?

    为了设计一个Windows系统下的单例进程程序,你可以遵循以下步骤: 首先,确定你的应用程序只能运行一个实例。这可以通过使用互斥量(Mutex)来实现。互斥量是一种同步对象,用于控制对共享资源的独占访问。 在应用程序的启动代码中,创建一个命名的互斥量对象。命名

    2024年01月22日
    浏览(46)
  • TCP/IP网络编程 第十九章:Windows平台下线程的使用

    要想掌握Windows平台下的线程,应首先理解“内核对象”(Kernel Objects)的概念。如果仅介绍Windows平台下的线程使用技巧,则可以省略相对陌生的内核对象相关内容。但这并不能使各位深入理解Windows平台下的线程。 内核对象的定义 操作系统创建的资源有很多种,如进程、线程

    2024年02月16日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包