C/C++ _beginthreadex 多线程操作
Posted cpp_learners
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++ _beginthreadex 多线程操作相关的知识,希望对你有一定的参考价值。
最近主管没有分配任务,于是将C/C++的线程学习一下。
经过了解才知道,C++03之前,用的创建线程都是CreateThread 与 _beginthreadex。使用这个两个函数进行创建线程。
然后C++11之后,就出现了新的线程函数thread,当然,这个创建线程比较方便!
经过两三天的纠结,最终决定深入研究_beginthreadex此方式创建线程,具体为什么我也说不清楚,看到网上很多人都推荐使用这个。。。
反正_beginthreadex内部都是调用CreateThread 进行创建线程的!
一、概念
简单说一下概念。
进程是包含线程的,一个进程可以有多个线程,多个线程(或一个)组合成一个进程。
线程是CPU调度和分派的基本单位,进程是分配资源的基本单位。
一个应用程序就是一个进程,例如运行一个QQ,运行一个微信等等,都是一个进程。
在QQ里面,我一边接收文件,一遍与别人聊天,这就是两个线程在同时运行!
为什么使用多线程
- 避免阻塞
大家知道,单个进程只有一个主线程,当主线程阻塞的时候,整个进程也就阻塞了,无法再去做其它的一些功能了。 - 避免CPU空转
应用程序经常会涉及到RPC,数据库访问,磁盘IO等操作,这些操作的速度比CPU慢很多,而在等待这些响应时,CPU却不能去处理新的请求,导致这种单线程的应用程序性能很差。 - 提升效率
一个进程要独立拥有4GB的虚拟地址空间,而多个线程可以共享同一地址空间,线程的切换比进程的切换要快得多。
头文件:
#include < process.h >
二、_beginthreadex创建线程
_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
);
返回值 : 成功返回新线程句柄, 失败返回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;
}
第一第二个参数,默认写NULL和0就行了,第三个参数是线程执行的函数,第四个参数是函数的参数,第五个参数是指定线程立刻执行,第六个参数是获得线程的ID。
可以判断一下线程句柄,是否创建线程成功:
if (hThread == NULL) {
printf("_beginthreadex() Error!\\n");
return -1;
}
如果函数需求是传入多个参数,那么只能使用结构体了!
到此,一个简单的线程就已经创建完毕了!
测试代码:
#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!\\n");
}
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;
}
printf("main end\\n");
//Sleep(5000);
system("pause");
return 0;
}
注意:
代码中必须加上 system(“pause”);让主线程暂停在哪里等待子线程执行完毕,否则主线程执行完毕就直接接收程序了,子线程完全没有机会执行!
这里的mian函数就是主线程也就是进程,当进程结束后,会结束掉所有线程!
如下截图:
三、单线程句柄阻塞
上面第二点,如果我想等子线程执行完了之后再接着继续往下走执行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!\\n");
}
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");
printf("main end\\n");
//Sleep(5000);
//system("pause"); // 不需要再暂停了
return 0;
}
四、多个线程句柄阻塞
提一个需求,创建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));
// 循环n次,创建n个线程
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);
printf("result:%lld\\n", num);
return 0;
}
// 对全局变量加一操作
unsigned WINAPI threadInc(void *arg) {
for (int i = 0; i < 500000; i++) {
num += 1;
}
return 0;
}
// 对全局变量减一操作
unsigned WINAPI threadDes(void *arg) {
for (int i = 0; i < 500000; i++) {
num -= 1;
}
return 0;
}
可以看到,最终结果不是零,而是乱值,这是为什么???
以上图为例,假设全局变量num是99,线程一开始执行,获取它做加一操作,然后还没等线程一将数值还回去,线程二又获取它做操作,然后还回去,最后线程一才将数值还回去,所以导致计算结果不正确。
这里这是举一个例子啊,大致上表达的意思就是线程一和线程二公用同一块内存变量,导致数值计算不正确。
如果想要解决这个问题,那么必须使用:互斥对象CreateMutex,这个将在下一篇博客中讲解用法!
五、CreateThread创建线程
CreateThread是一种微软在Windows API中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
• 第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。
• 第二个参数 dwStackSize 表示线程栈空间大小。传入0表示使用默认大小(1MB)。
• 第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
• 第四个参数 lpParameter 是传给线程函数的参数。
• 第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。
• 第六个参数 lpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号
测试代码:
#include <stdio.h>
#include <Windows.h>
#include <process.h>
DWORD WINAPI ThreadFun(LPVOID p) {
double d = *((double *)p);
printf("我是子线程, PID = %d\\nd = %f\\n", GetCurrentThreadId(), d);
return 0;
}
int main(void) {
HANDLE hThread;
DWORD dwThreadID;
double p = 3.14;
hThread = CreateThread(NULL, 0, ThreadFun, &p, 0, &dwThreadID);
printf("我是主线程, PID = %d\\n", GetCurrentThreadId());
// 关闭线程
CloseHandle(hThread);
Sleep(2000);
system("pause");
return 0;
}
六、thread创建线程
#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
void print1() {
Sleep(3000);
cout << "子线程1在运行。。。" << endl;
}
void print2() {
Sleep(3000);
cout << "子线程2在运行。。。" << endl;
}
int main(void) {
// 创建线程
thread test1(print1);
thread test2(print2);
// join函数,汇合线程,阻塞主线程,等待子线程执行结束,才会回到主线程中
test1.join();
// detch函数,分离,打破与主线程的依赖关系,即主线程运行主线程,子线程运行子线程,两个互不干扰
test2.detach();
cout << "主线程在运行..." << endl;
// joinable函数,判断当前线程是否可以做join或者detach过程,可以返回true,不可以返回false.
if (test2.joinable()) {
test2.join();
} else {
cout << "子线程不可以做join或detch操作" << endl;
}
return 0;
}
七、总结
线程的创建就是这么简单,当然,可以根据自己的项目需求,有针对性的去学习哪一种线程,效果都是一样的!
以上是关于C/C++ _beginthreadex 多线程操作的主要内容,如果未能解决你的问题,请参考以下文章
015 _beginthreadex CreateThread 函数区别
小解_beginthreadex与_beginthreadex和CreateThread的区别
多线程CreateThread与_beginthreadex本质区别
秒杀多线程第二篇 多线程第一次亲密接触 CreateThread与_beginthreadex本质区别