Win32 - 多线程
在window环境下,Win32 提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作:
1、线程操作函数
|
序号 |
函数名 |
功能 |
|
1 |
CreateThread() |
创建一个新线程 |
|
2 |
ExitThread() |
正常结束一个线程的执行 |
|
3 |
TerminateThead() |
强制终止一个线程的执行 |
|
4 |
ResumeThread() |
重启一个线程 |
|
5 |
SuspendThread() |
挂起一个线程 |
|
6 |
GetExiCodeThread() |
得到一个线程的退出码 |
|
7 |
GetThreadPriority() |
得到一个线程的优先级 |
|
8 |
SetThreadPriority() |
设置一个线程的优先级 |
|
9 |
CloseHandle() |
关闭一个线程的句柄 |
|
10 |
CreateRemoteThread() |
再另一个进程中创建一个新线程 |
|
11 |
PostThreadMessage() |
发送一条消息给指定的线程 |
|
12 |
GetCurrentThread() |
得到当前的线程句柄 |
|
13 |
GetCurrentThreadId() |
得到当前线程的ID |
|
14 |
GetThreadId() |
得到指定线程的ID |
|
15 |
WaitForSingleObject() |
等待单个对象 |
|
16 |
WaitForMultipleObjects() |
等待多个对象 |
关于多线程的API函数还有很多,以上只是列出了一些比较常用的函数,欲知更多函数和函数的使用方法,请参考MSDN或网络资源,在此就不再介绍了。
2、CreateThread创建线程
CreateThread是一种微软在Windows API中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。
CreateThread函数原型:
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全相关的属性,常置为NULL
SIZE_T dwStackSize, //新线程的初始化栈在大小,可设置为0
LPTHREAD_START_ROUTINE lpStartAddress, //被线程执行的回调函数,也称为线程函数
LPVOID lpParameter, //传入线程函数的参数,不需传递参数时为NULL
DWORD dwCreationFlags, //控制线程创建的标志
LPDWORD lpThreadId //传出参数,用于获得线程ID,如果为NULL则不返回线程ID
);
参数说明
-
lpThreadAttributes:线程安全属性(通常设为NULL)。 -
dwStackSize:线程栈大小(0表示默认大小)。 -
lpStartAddress:线程函数的入口地址。 -
lpParameter:传递给线程函数的参数。 -
dwCreationFlags:创建标志(0表示立即执行,CREATE_SUSPENDED表示挂起线程)。 -
lpThreadId:接收线程ID的变量指针(可为NULL)。
返回值
- 如果函数成功,则返回值是新线程的句柄。
- 如果函数失败,则返回值为NULL。
在 Windows API 中,一般使用 CreateThread() 函数创建线程。
创建一个不带参数的线程
#include<iostream>
#include<windows.h>
using namespace std;
// 编写了一个我的线程函数
DWORD WINAPI MyThread(LPVOID lpParamter)
{
cout << "fuck the multithread !\n";
return 0;
}
int main ()
{
// 创造线程
CreateThread(NULL, 0, MyThread, NULL, 0, NULL);
// 记得等待线程结束
system("PAUSE");
return 0;
}
创建一个带参线程:lpParam参数
下面是使用Windows API创建线程并打印数字并获取重要的线程信息的代码示例:
#include <iostream>
#include <Windows.h>
DWORD WINAPI ThreadFunction(LPVOID lpParam)
{
// 获取当前线程ID
DWORD threadId = GetCurrentThreadId();
std::cout << "Thread ID: " << threadId << std::endl;
// 获取线程传入的参数
int threadParam = *(int*)lpParam;
for (int i = 0; i < threadParam; i++)
{
std::cout << i << std::endl;
}
return 0;
}
int main()
{
int num = 10;
// 创建线程
HANDLE hThread = CreateThread(NULL, 0, ThreadFunction, &num, 0, NULL);
if (hThread == NULL)
{
cout << "Failed to create thread" << endl;
return 1;
}
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
// 关闭线程句柄
CloseHandle(hThread);
return 0;
}
3、_beginthreadex创建线程
经过了解才知道,C++03之前,用的创建线程都是CreateThread 与 _beginthreadex。使用这个两个函数进行创建线程。然后C++11之后,就出现了新的线程函数thread,当然,这个创建线程比较方便!经过两三天的纠结,最终决定深入研究_beginthreadex此方式创建线程,具体为什么我也说不清楚,看到网上很多人都推荐使用这个。反正_beginthreadex内部都是调用CreateThread 进行创建线程的!
_beginthreadex函数原型:
unsigned long _beginthreadex(
void *security, // 安全属性, 为NULL时表示默认安全性
unsigned stack_size, // 线程的堆栈大小, 一般默认为0
unsigned(_stdcall *start_address)(void *), // 线程函数
void *argilist, // 线程函数的参数
unsigned initflag, // 新线程的初始状态,0表示立即执行,//CREATE_SUSPENDED表示创建之后挂起
unsigned *threaddr // 用来接收线程ID
);
函数参数
- 第一个参数:默认写NULL,
- 第二个参数:默认写0,
- 第三个参数:是线程执行的函数,
- 第四个参数:是函数的参数,
- 第五个参数:是指定线程立刻执行,
- 第六个参数:是获得线程的ID。
返回值
- 成功:返回新线程句柄,
- 失败:返回0
创建线程:
int iParam = 10;
unsigned int dwThreadID; // 接收线程的ID
HANDLE hThread; // 线程句柄
// 创建线程
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (void *)&iParam, 0, &dwThreadID);
// 线程执行的函数
unsigned int WINAPI ThreadFun(LPVOID p)
{
int cnt = *((int *)p);
// ......
return 0;
}
可以判断一下线程句柄,是否创建线程成功
if (hThread == NULL)
{
printf("_beginthreadex() Error!\n");
return -1;
}
如果函数需求是传入多个参数,那么只能使用结构体了!
然后线程执行完毕后要记得关闭线程句柄:
CloseHandle(hThread);
示例代码:
#include <stdio.h>
#include <Windows.h>
#include <process.h>
unsigned int WINAPI ThreadFun(LPVOID p)
{
int cnt = *((int *)p);
for (int i = 0; i < cnt; i++)
{
Sleep(1000);
printf("Running thread! %d\n", i);
}
return 0;
}
int main(void)
{
int iParam = 10;
unsigned int dwThreadID; // 线程id
printf("main begin\n");
// 线程句柄 // 创建线程
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (void *)&iParam, 0, &dwThreadID);
if (hThread == NULL)
{
printf("_beginthreadex() Error!\n");
return -1;
}
// 关闭
CloseHandle(hThread);
printf("main end\n");
//Sleep(5000);
system("pause");
return 0;
}
运行结果:
注意:代码中必须加上 system(“pause”);让主线程暂停在哪里等待子线程执行完毕,否则主线程执行完毕就直接结束程序了,子线程完全没有机会执行!
这里的mian函数就是主线程也就是进程,当进程结束后,会结束掉所有线程!
两种方式的区别:
CreateThread |
_beginthreadex |
|---|---|
| 原生 Windows API | CRT 库封装,兼容 C 运行时库 |
可能导致 CRT 内存泄漏(如使用 printf) |
正确初始化线程本地存储(TLS) |
| 直接返回 Windows 线程句柄 | 返回伪句柄,需转换为 HANDLE |
4、单线程句柄阻塞
上面第二点,如果我想等子线程执行完了之后再接着继续往下走执行printf(“main end\n”);
这需要怎么操作呢?
这就要用到阻塞了,就是阻塞主线程,子线程执行完毕后,再接着执行主线程。
使用这个函数:
WaitForSingleObject:等待一个内核对象变为已通知状态
WaitForSingleObject(
_In_ HANDLE hHandle, //指明一个内核对象的句柄
_In_ DWORD dwMilliseconds //等待时间
);
使用方式:
DWORD wr;
// 等待通知,等待子线程执行完毕
// 阻塞主线程,INFINITE:等待特定秒数;WAIT_FAILED:WaitForSingleObject等待结束后会返回它
printf("WaitForSingleObject() begin\n");
if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
{
printf("Thread wait error!\n");
return -1;
}
printf("WaitForSingleObject() end\n");
WaitForSingleObject传入两个参数,参数一是线程句柄,参数二是等待时间,INFINITE表示很多秒,具体多少秒不知道,但是足以应付日常线程使用,也可以指定具体秒数。
如果发生错误会返回WAIT_FAILED,这样进行判断就可以知道等待是有没有发生错误了!
测试代码:
#include <stdio.h>
#include <Windows.h>
#include <process.h>
// 线程执行的函数
unsigned int WINAPI ThreadFun(LPVOID p)
{
int cnt = *((int *)p);
for (int i = 0; i < cnt; i++)
{
Sleep(1000);
printf("Running thread! %d\n", i);
}
return 0;
}
int main(void)
{
int iParam = 10;
unsigned int dwThreadID; // 线程id
DWORD wr; // 阻塞线程返回值
printf("main begin\n");
// 线程句柄 // 创建线程
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (void *)&iParam, 0, &dwThreadID);
if (hThread == NULL)
{
printf("_beginthreadex() Error!\n");
return -1;
}
// 等待通知,等待子线程执行完毕
// 阻塞主线程,INFINITE:等待特定秒数;WAIT_FAILED:WaitForSingleObject等待结束后会返回它
printf("WaitForSingleObject() begin\n");
if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
{
printf("Thread wait error!\n");
return -1;
}
printf("WaitForSingleObject() end\n");
CloseHandle(hThread);
printf("main end\n");
//Sleep(5000);
system("pause"); // 不需要再暂停了
return 0;
}
运行结果:
5、多个线程句柄阻塞
提一个需求,创建n个线程,然后循环n次,奇数次对一个全局变量做加一操作,偶数次对一个全局变量做减一操作。
按照正常来讲,设置这个全局变量为零,当程序结束,这个全局变量也还是零才对,事实上是这样嘛?
使用WaitForMultipleObjects阻塞多个线程
WaitForMultipleObjects(
_In_ DWORD nCount, // 要监测的句柄的组的句柄的个数
_In_reads_(nCount) CONST HANDLE* lpHandles, //要监测的句柄的组
_In_ BOOL bWaitAll, // TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号
_In_ DWORD dwMilliseconds //等待时间
);
使用如下:
// 阻塞多个线程句柄,直到子线程运行完毕,主线程才会往下走
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
函数参数
- 参数一:检测句柄的个数;
- 参数二:检测句柄的数组;
- 参数三:TRUE等待所有线程执行完毕,FALSE,任意一个完成就停止阻塞;
- 参数四:等待时间
测试代码:
#include <stdio.h>
#include <Windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg); // 减一操作
unsigned WINAPI threadDes(void *arg); // 加一操作
// 操作的全局变量
long long num = 0;
int main(void)
{
// 创建n个线程句柄
HANDLE tHandles[NUM_THREAD];
printf("sizeof long long :%d\n", sizeof(long long));
// 循环50次,创建50个线程
for (int i = 0; i < NUM_THREAD; i++)
{
if (i % 2)//奇数次
{
//创建线程
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
}
else//偶数次
{
//创建线程
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
}
// 阻塞多个线程句柄,直到子线程运行完毕,主线程才会往下走
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
for (int i = 0; i < NUM_THREAD; i++)
{
CloseHandle(tHandles[i]);
}
printf("result:%lld\n", num);
system("pause");
return 0;
}
// 对全局变量加一操作
unsigned WINAPI threadInc(void *arg)
{
for (int i = 0; i < 500000; i++)
{
num += 1;
//printf("%d\n", num);
}
return 0;
}
// 对全局变量减一操作
unsigned WINAPI threadDes(void *arg)
{
for (int i = 0; i < 500000; i++)
{
num -= 1;
//printf("%d\n", num);
}
return 0;
}
有一个全局变量`long long num = 0;`,然后创建了50个线程,其中一半执行`threadInc`函数,对num进行加1操作,另一半执行`threadDes`函数,进行减1操作。每个线程循环50万次,所以理论上如果没有任何竞争的话,最后的结果应该是0,因为加减次数相同。但实际运行的时候,结果可能不是0,而是其他数值,这说明存在数据竞争的问题。
运行结果:
可以看到,最终结果不是零,而是乱值,这是为什么?
以上图为例,假设全局变量num是99,线程一开始执行,获取它做加一操作,然后还没等线程一将数值还回去,线程二又获取它做操作,然后还回去,最后线程一才将数值还回去,所以导致计算结果不正确。
这里这是举一个例子啊,大致上表达的意思就是线程一和线程二公用同一块内存变量,导致数值计算不正确。
如果想要解决这个问题,那么必须使用:互斥对象CreateMutex。





浙公网安备 33010602011771号