0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

了解多线程并深入分析CreateThread与_beginthreadex本质区别

C语言专家集中营 2018-01-09 17:08 次阅读

本文将带领你与多线程作第一次亲密接触,并深入分析CreateThread与_beginthreadex的本质区别,相信阅读本文后你能轻松的使用多线程并能流畅准确的回答CreateThread与_beginthreadex到底有什么区别,在实际的编程中到底应该使用CreateThread还是_beginthreadex?

使用多线程其实是非常容易的,下面这个程序的主线程会创建了一个子线程并等待其运行完毕,子线程就输出它的线程ID号然后输出一句经典名言——Hello World。整个程序的代码非常简短,只有区区几行。

[cpp]view plaincopy

//最简单的创建多线程实例

#include

#include

//子线程函数

DWORDWINAPIThreadFun(LPVOIDpM)

{

printf("子线程的线程ID号为:%d\n子线程输出HelloWorld\n",GetCurrentThreadId());

return0;

}

//主函数,所谓主函数其实就是主线程执行的函数。

intmain()

{

printf("最简单的创建多线程实例\n");

printf("--byMoreWindows(http://blog.csdn.net/MoreWindows)--\n\n");

HANDLEhandle=CreateThread(NULL,0,ThreadFun,NULL,0,NULL);

WaitForSingleObject(handle,INFINITE);

return0;

}

运行结果如下所示:

了解多线程并深入分析CreateThread与_beginthreadex本质区别

下面来细讲下代码中的一些函数

第一个CreateThread

函数功能:创建线程

函数原型:

HANDLEWINAPICreateThread(

LPSECURITY_ATTRIBUTESlpThreadAttributes,

SIZE_TdwStackSize,

LPTHREAD_START_ROUTINElpStartAddress,

LPVOIDlpParameter,

DWORDdwCreationFlags,

LPDWORDlpThreadId

);

函数说明:

第一个参数表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。

第二个参数表示线程栈空间大小。传入0表示使用默认大小(1MB)。

第三个参数表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。

第四个参数是传给线程函数的参数。

第五个参数指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。

第六个参数将返回线程的ID号,传入NULL表示不需要返回该线程ID号。

函数返回值:

成功返回新线程的句柄,失败返回NULL。

第二个WaitForSingleObject

函数功能:等待函数–使线程进入等待状态,直到指定的内核对象被触发。

函数原形:

DWORDWINAPIWaitForSingleObject(

HANDLEhHandle,

DWORDdwMilliseconds

);

函数说明:

第一个参数为要等待的内核对象。

第二个参数为最长等待的时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINITE表示无限等待。

因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。

函数返回值:

在指定的时间内对象被触发,函数返回WAIT_OBJECT_0。超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT。传入参数有错误将返回WAIT_FAILED

CreateThread()函数是Windows提供的API接口,在C/C++语言另有一个创建线程的函数_beginthreadex(),在很多书上(包括《Windows核心编程》)提到过尽量使用_beginthreadex()来代替使用CreateThread(),这是为什么了?下面就来探索与发现它们的区别吧。

首先要从标准C运行库与多线程的矛盾说起,标准C运行库在1970年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准C运行库的程序员根本没考虑多线程程序使用标准C运行库的情况。比如标准C运行库的全局变量errno。很多运行库中的函数在出错时会将错误代号赋值给这个全局变量,这样可以方便调试。但如果有这样的一个代码片段:

[cpp]view plaincopy

if(system("notepad.exereadme.txt")==-1)

{

switch(errno)

{

...//错误处理代码

}

}

假设某个线程A在执行上面的代码,该线程在调用system()之后且尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。

为了解决这个问题,Windows操作系统提供了这样的一种解决方案——每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的。下面列出_beginthreadex()函数的源代码(我在这份代码中增加了一些注释)以便读者更好的理解_beginthreadex()函数与CreateThread()函数的区别。

[cpp]view plaincopy

//_beginthreadex源码整理ByMoreWindows(http://blog.csdn.net/MoreWindows)

_MCRTIMPuintptr_t__cdecl_beginthreadex(

void*security,

unsignedstacksize,

unsigned(__CLR_OR_STD_CALL*initialcode)(void*),

void*argument,

unsignedcreateflag,

unsigned*thrdaddr

)

{

_ptiddataptd;//pointertoper-threaddata见注1

uintptr_tthdl;//threadhandle线程句柄

unsignedlongerr=0L;//ReturnfromGetLastError()

unsigneddummyid;//dummyreturnedthreadID线程ID号

//validationsection检查initialcode是否为NULL

_VALIDATE_RETURN(initialcode!=NULL,EINVAL,0);

//InitializeFlsGetValuefunctionpointer

__set_flsgetvalue();

//Allocateandinitializeaper-threaddatastructurefortheto-be-createdthread.

//相当于new一个_tiddata结构,并赋给_ptiddata指针。

if((ptd=(_ptiddata)_calloc_crt(1,sizeof(struct_tiddata)))==NULL)

gotoerror_return;

//Initializetheper-threaddata

//初始化线程的_tiddata块即CRT数据区域见注2

_initptd(ptd,_getptd()->ptlocinfo);

//设置_tiddata结构中的其它数据,这样这块_tiddata块就与线程联系在一起了。

ptd->_initaddr=(void*)initialcode;//线程函数地址

ptd->_initarg=argument;//传入的线程参数

ptd->_thandle=(uintptr_t)(-1);

#ifdefined(_M_CEE)||defined(MRTDLL)

if(!_getdomain(&(ptd->__initDomain)))//见注3

{

gotoerror_return;

}

#endif//defined(_M_CEE)||defined(MRTDLL)

//Makesurenon-NULLthrdaddrispassedtoCreateThread

if(thrdaddr==NULL)//判断是否需要返回线程ID号

thrdaddr=&dummyid;

//Createthenewthreadusingtheparameterssuppliedbythecaller.

//_beginthreadex()最终还是会调用CreateThread()来向系统申请创建线程

if((thdl=(uintptr_t)CreateThread(

(LPSECURITY_ATTRIBUTES)security,

stacksize,

_threadstartex,

(LPVOID)ptd,

createflag,

(LPDWORD)thrdaddr))

==(uintptr_t)0)

{

err=GetLastError();

gotoerror_return;

}

//Goodreturn

return(thdl);//线程创建成功,返回新线程的句柄.

//Errorreturn

error_return:

//EitherptdisNULL,oritpointstotheno-longer-necessaryblock

//calloc-edforthe_tiddatastructwhichshouldnowbefreedup.

//回收由_calloc_crt()申请的_tiddata块

_free_crt(ptd);

//Maptheerror,ifnecessary.

//Note:thisroutinereturns0forfailure,justliketheWin32

//APICreateThread,but_beginthread()returns-1forfailure.

//校正错误代号(可以调用GetLastError()得到错误代号)

if(err!=0L)

_dosmaperr(err);

return((uintptr_t)0);//返回值为NULL的效句柄

}

讲解下部分代码:

注1._ptiddataptd;中的_ptiddata是个结构体指针。在mtdll.h文件被定义:

typedefstruct_tiddata*_ptiddata

微软对它的注释为Structure for each thread's data。这是一个非常大的结构体,有很多成员。本文由于篇幅所限就不列出来了。

注2._initptd(ptd,_getptd()->ptlocinfo);微软对这一句代码中的getptd()的说明为:

/* return address of per-thread CRT data */

_ptiddata__cdecl_getptd(void);

对_initptd()说明如下:

/* initialize a per-thread CRT data block */

void__cdecl_initptd(_Inout_ _ptiddata_Ptd,_In_opt_ pthreadlocinfo_Locale);

注释中的CRT(C Runtime Library)即标准C运行库。

注3.if(!_getdomain(&(ptd->__initDomain)))中的_getdomain()函数代码可以在thread.c文件中找到,其主要功能是初始化COM环境。

由上面的源代码可知,_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。相信阅读到这里时,你会对这句简短的话有个非常深刻的印象,如果有面试官问起,你也可以流畅准确的回答了^_^。

接下来,类似于上面的程序用CreateThread()创建输出“Hello World”的子线程,下面使用_beginthreadex()来创建多个子线程:

[cpp]view plaincopy

//创建多子个线程实例

#include

#include

#include

//子线程函数

unsignedint__stdcallThreadFun(PVOIDpM)

{

printf("线程ID号为%4d的子线程说:HelloWorld\n",GetCurrentThreadId());

return0;

}

//主函数,所谓主函数其实就是主线程执行的函数。

intmain()

{

printf("创建多个子线程实例\n");

printf("--byMoreWindows(http://blog.csdn.net/MoreWindows)--\n\n");

constintTHREAD_NUM=5;

HANDLEhandle[THREAD_NUM];

for(inti=0;i< THREAD_NUM; i++)  

handle[i]=(HANDLE)_beginthreadex(NULL,0,ThreadFun,NULL,0,NULL);

WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);

return0;

}

运行结果如下:

了解多线程并深入分析CreateThread与_beginthreadex本质区别

图中每个子线程说的都是同一句话,不太好看。能不能来一个线程报数功能,即第一个子线程输出1,第二个子线程输出2,第三个子线程输出3,……。要实现这个功能似乎非常简单——每个子线程对一个全局变量进行递增并输出就可以了。代码如下:

[cpp]view plaincopy

//子线程报数

#include

#include

#include

intg_nCount;

//子线程函数

unsignedint__stdcallThreadFun(PVOIDpM)

{

g_nCount++;

printf("线程ID号为%4d的子线程报数%d\n",GetCurrentThreadId(),g_nCount);

return0;

}

//主函数,所谓主函数其实就是主线程执行的函数。

intmain()

{

printf("子线程报数\n");

printf("--byMoreWindows(http://blog.csdn.net/MoreWindows)--\n\n");

constintTHREAD_NUM=10;

HANDLEhandle[THREAD_NUM];

g_nCount=0;

for(inti=0;i< THREAD_NUM; i++)  

handle[i]=(HANDLE)_beginthreadex(NULL,0,ThreadFun,NULL,0,NULL);

WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);

return0;

}

对一次运行结果截图如下:

了解多线程并深入分析CreateThread与_beginthreadex本质区别

显示结果从1数到10,看起来好象没有问题。

答案是不对的,虽然这种做法在逻辑上是正确的,但在多线程环境下这样做是会产生严重的问题。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C语言
    +关注

    关注

    180

    文章

    7604

    浏览量

    136714
  • 多线程
    +关注

    关注

    0

    文章

    278

    浏览量

    19946

原文标题:多线程CreateThread与_beginthreadex本质区别

文章出处:【微信号:C_Expert,微信公众号:C语言专家集中营】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    FPGA与ARM的本质区别是什么?

    FPGA(Field-Programmable Gate Array)与ARM在多个方面存在本质区别。 首先,从它们的定义和结构上来看,FPGA是一种现场可编程门阵列,属于可编程器件的一种。它的内部
    发表于 04-28 08:56

    FPGA与ARM的本质区别

    FPGA(Field-Programmable Gate Array)与ARM在多个方面存在本质区别。 首先,从它们的定义和结构上来看,FPGA是一种现场可编程门阵列,属于可编程器件的一种。它的内部
    发表于 04-28 09:00

    uCOS任务堆栈的深入分析(转)

    uCOS任务堆栈的深入分析(转)
    发表于 08-24 23:30

    PLC与单片机的本质区别

    PLC与单片机的本质区别是什么?
    发表于 01-13 07:55

    多线程和多进程的区别

    6.你的数据库一会又500个连接数,一会有10个,你分析一下情况7.udp和tcp的区别8.多线程和多进程的区别9.有一台web服务器,你选择用多线
    发表于 07-19 07:21

    请问PLC与单片机的本质区别在哪里?

    PLC与单片机的本质区别在哪里?
    发表于 11-09 06:04

    笔记本的结构深入分析

    笔记本的结构深入分析  电脑技术的应用为我们的生活和工作带来了巨大改变,使我们的生活学习工作有了质的转变。普通的用户对电脑的了解
    发表于 01-21 15:53 4285次阅读

    基于SWT的多线程解决方案

    介绍了在基于 SWT 的C / S 结构的项目开发中,当用UI 主线程进行后台数据读取或交换时导致的UI 线程堵塞现象的解决方案。通过对UI 线程深入了解,利用
    发表于 06-07 17:08 0次下载

    多核与多线程技术的区别

    毫无疑问的,多核、多线程此二词已快成为当今处理器架构设计中的两大显学,如同历史战国时代以儒、墨两大派的显学,只不过当年两大治世思想学派是争得你死我亡,而多核、多线程则是相互兼容蓄,今日几乎任何
    发表于 10-19 16:26 0次下载

    多线程好还是单线程好?单线程多线程区别 优缺点分析

    摘要:如今单线程多线程已经得到普遍运用,那么到底多线程好还是单线程好呢?单线程多线程
    发表于 12-08 09:33 8.1w次阅读

    光纤和光缆的本质区别是什么

    相信大家都听过光纤盒光缆,那光纤和光缆一样吗?本质区别在哪里?科兰综合布线小编指出:其实两者都是一种传输介质。但严格意义上讲,两者是不相同的产品,下面一起来了解一下两者区别
    发表于 03-23 10:24 6284次阅读

    分析unidbg(unidbgMutil)多线程机制

    由于在工作中遇到了某翻译so中有多线程调用,因此使用unidbg分析(基于unidbgMutilThread)增加阻塞唤醒机制(futex系统调用),但仍未调用成功
    的头像 发表于 05-20 17:23 2849次阅读
    <b class='flag-5'>分析</b>unidbg(unidbgMutil)<b class='flag-5'>多线程</b>机制

    线程是什么的基本单位 进程与线程本质区别

    的代码、数据以及用于执行这些代码的上下文信息。一个进程可以由一个或多个线程组成,从而并发执行多个任务。 本质区别: 资源拥有方式:进程是资源分配的基本单位,每个进程拥有独立的内存空间、文件描述符、页面表等资源,之
    的头像 发表于 02-02 16:30 927次阅读

    聚徽触控-工控机和商用电脑本质区别是什么

    工控机和商用电脑在多个方面存在本质区别,具体如下:
    的头像 发表于 07-16 09:19 304次阅读

    Python中多线程和多进程的区别

    Python作为一种高级编程语言,提供了多种并发编程的方式,其中多线程与多进程是最常见的两种方式之一。在本文中,我们将探讨Python中多线程与多进程的概念、区别以及如何使用线程池与进
    的头像 发表于 10-23 11:48 394次阅读
    Python中<b class='flag-5'>多线程</b>和多进程的<b class='flag-5'>区别</b>