异步执行函数线程Dome

调用MSVC CRT的函数_beginthread()或_beginthreadex()来创建线程。

_beginthread 参数和返回值

unsigned long _beginthread(
void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式 ,一般指函数名称
unsigned stack_size, //是线程堆栈大小,一般默认为0
void *arglist //向线程传递的参数指针,一般为结构体
);

#include<stdio.h>
#include<stdlib.h>
#include<process.h>//线程头文件
#include<Windows.h>


void msg(void *p)
{
	char* str = (char*)p;
	MessageBoxA(0, "窗口", str, 0);
}

void main()
{

	char ch1[20] = "第一条信息";
	char ch2[20] = "第二条信息";
	char ch3[20] = "第三条信息";

	//开辟三个线程异步执行msg函数
	_beginthread(msg, 0, &ch1);
	_beginthread(msg, 0, &ch2);
	_beginthread(msg, 0, &ch3);


	system("pause");
}

执行效果:

三个对话框同时弹出

我们知道在Windows下创建一个线程的方法有两种,一种就是调用Windows API CreateThread()来创建线程;另外一种就是调用MSVC CRT的函数_beginthread()或_beginthreadex()来创建线程。相应的退出线程也有两个函数Windows API的ExitThread()和CRT的_endthread()。这两套函数都是用来创建和退出线程的,它们有什么区别呢?

根据Windows API和MSVC CRT的关系,可以看出来_beginthread()是对CreateThread()的包装,它最终还是调用CreateThread()来创建线程。那么在_beginthread()调用CreateThread()之前做了什么呢?我们可以看一下_beginthread()的源代码,它位于CRT源代码中的thread.c。我们可以发现它在调用CreateThread()之前申请了一个叫_tiddata的结构,然后将这个结构用_initptd()函数初始化之后传递给_beginthread()自己的线程入口函数_threadstart。_threadstart首先把由_beginthread()传过来的_tiddata结构指针保存到线程的显式TLS数组,然后它调用用户的线程入口真正开始线程。在用户线程结束之后,_threadstart()函数调用_endthread()结束线程。并且_threadstart还用__try/__except将用户线程入口函数包起来,用于捕获所有未处理的信号,并且将这些信号交给CRT处理。

所以除了信号之外,很明显CRT包装Windows API线程接口的最主要目的就是那个_tiddata。这个线程私有的结构里面保存的是什么呢?我们可以从mtdll.h中找到它的定义,它里面保存的是诸如线程ID、线程句柄、erron、strtok()的前一次调用位置、rand()函数的种子、异常处理等与CRT有关的而且是线程私有的信息。可见MSVC CRT并没有使用我们前面所说的__declspec(thread)这种方式来定义线程私有变量,从而防止库函数在多线程下失效,而是采用在堆上申请一个_tiddata结构,把线程私有变量放在结构内部,由显式TLS保存_tiddata的指针。

那么_tiddata在什么时候会被释放呢?ExitThread()肯定不会,因为它根本不知道有_tiddata这样一个结构存在,那么很明显是_endthread()释放的,这也正是CRT的做法。不过我们很多时候会发现,即使使用CreateThread()和ExitThread() (不调用ExitThread()直接退出线程函数的效果相同),也不会发现任何内存泄露,这又是为什么呢?经过仔细检查之后,我们发现原来密码在CRT DLL的入口函数DllMain中。我们知道,当一个进程/线程开始或退出的时候,每个DLL的DllMain都会被调用一次,于是动态链接版的CRT就有机会在DllMain中释放线程的_tiddata。可是DllMain只有当CRT是动态链接版的时候才起作用,静态链接CRT是没有DllMain的!这就是造成使用CreateThread()会导致内存泄露的一种情况,在这种情况下,_tiddata在线程结束时无法释放,造成了泄露。

 
#include <Windows.h>
#include <process.h>
void thread(void *a)
{
    char* r = strtok( "aaa", "b" );
    ExitThread(0); // 这个函数是否调用都无所谓
}
int main(int argc, char* argv[])
{
    while(1) {
        CreateThread(  0, 0, (LPTHREAD_START_ROUTINE)thread, 0, 0, 0 );
        Sleep( 5 );
    }
return 0;
}

如果用动态链接的CRT (/MD,/MDd)就不会有问题,但是,如果使用静态链接CRT (/MT,/MTd),运行程序后在进程管理器中观察它就会发现内存用量不停地上升,但是如果我们把thread()函数中的ExitThread()改成_endthread()就不会有问题,因为_endthread()会将_tiddata()释放。

这个问题可以总结为:当使用CRT时(基本上所有的程序都使用CRT),请尽量使用_beginthread()/_beginthreadex()/_endthread()/_endthreadex()这组函数来创建线程。在MFC中,还有一组类似的函数是AfxBeginThread()和AfxEndThread(),根据上面的原理类推,它是MFC层面的线程包装函数,它们会维护线程与MFC相关的结构,当我们使用MFC类库时,尽量使用它提供的线程包装函数以保证程序运行正确。

原文地址:https://www.jb51.net/article/41459.htm

 

线程的冻结和解冻调试:

#include<stdio.h>
#include<stdlib.h>
#include<process.h>//线程头文件
#include<Windows.h>





void printNumber(void* vPtr)
{
	int *num = (int*)vPtr;
	while (1)
	{
		printf("%d\n", (*num)++);
		Sleep(1000);//休眠1秒
	}
}

void main()
{
	int n1 = 10;
	
	_beginthread(printNumber, 0, &n1);
	
	//插入断点
	system("pause");//阻塞主线程
	//插入断点
	system("pause");
	//插入断点
	system("pause");
	//插入断点
	system("pause");
	//插入断点
	system("pause");
}

打开线程调试窗口:

system("pause") 函数阻塞线程执行

多线程访问全局变量冲突问题:

WaitForMultipleObjects是Windows中的一个功能非常强大的函数,几乎可以等待Windows中的所有的内核对象。等待多个线程或事件执行结束。

函数原型为:

DWORD WaitForMultipleObjects(

DWORD nCount, // 线程个数

CONST HANDLE *lpHandles, // 指向对象句柄数组的指针

BOOL fWaitAll, // 释放等待所有

DWORD dwMilliseconds // 超时间隔(毫秒)

);

参数解析:

DWORD 就是 Double Word, 每个word为2个字节的长度,DWORD双字即为4个字节,每个字节是8位。

nCount 指定列表中的句柄数量 最大值为MAXIMUM_WAIT_OBJECTS(64)

*lpHandles 句柄数组的指针。lpHandles为指定对象句柄组合中的第一个元素 HANDLE类型可以为(Event,Mutex,Process,Thread,Semaphore)数组

bWaitAll 等待的类型,如果为TRUE,表示除非对象都发出信号,否则就一直等待下去;如果FALSE,表示任何对象发出信号即可

dwMilliseconds指定要等候的毫秒数。如设为零,表示立即返回。如指定常数INFINITE,则可根据实际情况无限等待下去

函数的返回值有:

WAIT_ABANDONED_0:所有对象都发出消息,而且其中有一个或多个属于互斥体(一旦拥有它们的进程中止,就会发出信号)

WAIT_TIMEOUT:对象保持未发信号的状态,但规定的等待超时时间已经超过

WAIT_OBJECT_0:所有对象都发出信号

WAIT_IO_COMPLETION:(仅适用于WaitForMultipleObjectsEx)由于一个I/O完成操作已作好准备执行,所以造成了函数的返回

返回WAIT_FAILED则表示函数执行失败,会设置GetLastError

如bWaitAll为FALSE,那么返回结果相似,只是可能还会返回相对于WAIT_ABANDONED_0或WAIT_OBJECT_0的一个正偏移量,指出哪个对象是被抛弃还是发出信号。

WAIT_OBJECT_0是微软定义的一个宏,你就把它看成一个数字就可以了。

例如,WAIT_OBJECT_0 + 5的返回结果意味着列表中的第5个对象发出了信号

如果程序中的nObjectWait是WAIT_OBJECT_0 + 5

int nIndex = nObjectWait - WAIT_OBJECT_0;就是说nIndex =5也就表示第5个对象发出了信号

 

DWORD WINAPI WaitForMultipleObjectsEx

  _In_ DWORD nCount,

  _In_ const HANDLE* lpHandles,

  _In_ BOOL bWaitAll,

  _In_ DWORD dwMilliseconds,

  _In_ BOOL bAlertable

);  和上面差不多,详情参见:https://blog.csdn.net/liu4030127/article/details/9457275

当 bWaitAll参数为FALSE可以等待其中之一的事件:示例如下

HANDLE m_hEvent[2];    
  //两事件  
  
m_hEvent[0]=CreateEvent(NULL, FALSE, FALSE, NULL);  
m_hEvent[1]=CreateEvent(NULL, FALSE, FALSE, NULL);  
CreateThread(NULL, 0, MyThreadProc, this, 0, NULL);  
DWORD WINAPI MyThreadProc(LPVOID lpParam)  
{   
while(TRUE)  
 {  //每次等500毫秒   
 int nIndex = WaitForMultipleObjects(2, pThis->m_hEvent, FALSE,500);     
 if (nIndex == WAIT_OBJECT_0 + 1)   
 {  
 //第二个事件发生   //ExitThread(0);   //break;    
}   
 else if (nIndex == WAIT_OBJECT_0) //第一个事件发生    
{   
  //第一个事件  
   }    
else if (nIndex == WAIT_TIMEOUT) //超时500毫秒    
{   //超时可作定时用    
}   
}  
 OutputDebugString("线程结束. /n");  
 return 0L;}  
#include<stdio.h>
#include<stdlib.h>
#include<process.h>//线程头文件
#include<Windows.h>

int num = 0;

void run(void* vPtr)
{
	for (int i = 0; i < 100; i++)
	{
		num++;
	}
}

void main()
{
	HANDLE hd[100]; //函数句柄   typedef void * ; 创建一个线程数组
	for (int i = 0; i < 100; i++)
	{
		//_beginthread 返回值为 unsigned int 或 unsigned __int64
		hd[i]=(HANDLE)_beginthread(run, 0,NULL);
	}
	
	//等待线程全部结束
	WaitForMultipleObjects(100, hd, TRUE, INFINITE);

	printf("num=%d\n", num);

	system("pause");
}

实际输出与理论值不一致,这就是多线程同时访问全局变量冲突造成的。

Windows提供了许多内核对象来实现线程的同步。对于线程同步而言,这些内核对象有两个非常重要的状态:“已通知”状态,“未通知”状态(也有翻译为:受信状态,未受信状态)。Windows提供了几种内核对象可以处于已通知状态和未通知状态:进程、线程、作业、文件、控制台输入/输出/错误流、事件、等待定时器、信号量、互斥对象。

  你可以通知一个内核对象,使之处于“已通知状态”,然后让其他等待在该内核对象上的线程继续执行。你可以使用Windows提供的API函数,等待函数来等待某一个或某些内核对象变为已通知状态。

你可以使用WaitForSingleObject函数来等待一个内核对象变为已通知状态:

DWORD WaitForSingleObject(
HANDLE hObject, //指明一个内核对象的句柄
DWORD dwMilliseconds); //等待时间
  该函数需要传递一个内核对象句柄,该句柄标识一个内核对象,如果该内核对象处于未通知状态,则该函数导致线程进入阻塞状态如果该内核对象处于已通知状态,则该函数立即返回WAIT_OBJECT_0。第二个参数指明了需要等待的时间(毫秒),可以传递INFINITE指明要无限期等待下去,如果第二个参数为0,那么函数就测试同步对象的状态并立即返回。如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED。

函数返回值:

WAIT_OBJECT_0表示函数成功执行且指定的对象被触发处于signaled有信号状态;
WAIT_TIMEOUT表示函数成功执行但超时且对象没有被触发处于nonsignaled无信号状态;
WAIT_FAILED表示函数执行失败,通常是由hHandle句柄不可用,可以调用GetLastError()查看。
详细说明:该函数检测指定的对象的当前状态,若为nonsignaled则调用者线程将处于等待状态,在此状态下消耗少量的处理器时间直到对象状态改变为signaled或超时。值得注意的是超时时限范围为0~0x7FFF FFFF,上限是0x7FFF FFFF而不是无穷大0xFFFF FFFF。若设定值为0x8000 0000~0xFFFF FFFF将等同于0x7FFFF FFFF。若要使用大于上限值请使用无穷大。在函数返回之前,有一个等待函数会修改引起函数返回的对象的状态,比如将信号量减1。目前该函数可以等待的对象包括事件event、互斥体mutex、信号量semaphore、进程process和线程thread。

把上面代码循环中添加

for (int i = 0; i < 100; i++)
	{
		//_beginthread 返回值为 unsigned int 或 unsigned __int64
		hd[i]=(HANDLE)_beginthread(run, 0,NULL);
		//等待单个线程结束,才会执行下一步
		WaitForSingleObject(hd[i], INFINITE);//同步
	}

使用 临界区解决多线程冲突:CRITICAL_SECTION

临界区又称关键代码段,指的是一小段代码在代码执行前,他需要独占一些资源。程序中通常将多线程同时访问的某个资源作为临界区,需要定义一个CRITICAL_SECTION类型的变量,然后调用InitializeCriticalSection函数对变量进行初始化;

函数声明:VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );

lpCriticalSection:一个CRITICAL_SECTION结构指针,表示用于初始化的临界区;

InitializeCriticalSection函数在内部设置了CRITICAL_SECTION结构的某些成员变量,所以他不会失败。

为了将某一段代码定义为临界区,需要调用EnterCriticalSection函数;

VOID WINAPI EnterCriticalSection(__inout LPCRITICAL_SECTION lpCriticalSection);

该函数的作用是判断是否有线程访问临界区资源,如果没有,就改变CRITICAL_SECTION结构的成员变量的值,赋予当前线程访问权,函数立即返回;如果有线程正在访问资源,则进入等待状态,直到没有线程访问。

释放资源函数:void WINAPI LeaveCriticalSection( _Inout_LPCRITICAL_SECTION lpCriticalSection);

释放CRITICAL_SECTION结构指针:void WINAPI DeleteCriticalSection(_Inout_ LPCRITICAL_SECTION lpCriticalSection);

#include<stdio.h>
#include<stdlib.h>
#include<process.h>//线程头文件
#include<Windows.h>
#include<time.h>

#define N 10

CRITICAL_SECTION cs; //声明临界区结构体
int num = 0;

void run(void* vPtr)
{
	for (int i = 0; i < N; i++)
	{
		EnterCriticalSection(&cs);//进入临界区
		num++;
		LeaveCriticalSection(&cs);
	}
}

//声明操作系统可调用函数
DWORD WINAPI myrun(void* vPtr)
{
	EnterCriticalSection(&cs);//进入临界区
	for (int i = 0; i < 1000; i++)
	{
		num++;
	}
	LeaveCriticalSection(&cs);
	return 0;
}


void main()
{
	InitializeCriticalSection(&cs);//初始化临界区

	time_t strat, end;
	time(&strat);//开始时间

	HANDLE hd[N]; //函数句柄   typedef void * ; 创建一个线程数组
	for (int i = 0; i < N; i++)
	{
		//_beginthread 返回值为 unsigned int 或 unsigned __int64
		//hd[i]=(HANDLE)_beginthread(run, 0,NULL);
		//等待单个线程结束,才会执行下一步
		//WaitForSingleObject(hd[i], INFINITE);
		hd[i]=CreateThread(NULL, 0, myrun, NULL, 0, NULL);//创建线程
	}
	
	//等待线程全部结束
	WaitForMultipleObjects(N, hd, TRUE, INFINITE);
	//WaitForMultipleObjectsEx(N, hd, TRUE, INFINITE,TRUE);

	time(&end);//结束时间
	double dtime= difftime(end, strat);
	printf("time=%f\n", dtime);

	printf("num=%d\n", num);

	DeleteCriticalSection(&cs);//释放CRITICAL_SECTION结构指针
	system("pause");
}

 

 

 

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐