专栏首页包子的书架C++ CreateThread的使用

C++ CreateThread的使用

函数原型:

HANDLE WINAPI CreateThread( In_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, {安全设置} In SIZE_T dwStackSize, {堆栈大小} In LPTHREAD_START_ROUTINE lpStartAddress, {入口函数} In_opt __drv_aliasesMem LPVOID lpParameter, {函数参数} In DWORD dwCreationFlags, {启动选项} Out_opt LPDWORD lpThreadId {输出线程id} );

函数解析

1、返回值:返回线程句柄 "句柄" 类似指针, 但通过指针可读写对象, 通过句柄只是使用对象; 有句柄的对象一般都是系统级别的对象(或叫内核对象); 之所以给我们的是句柄而不是指针, 目的只有一个: "安全"; 貌似通过句柄能做很多事情, 但一般把句柄提交到某个函数(一般是系统函数)后, 我们也就到此为止很难了解更多了; 事实上是系统并不相信我们.

不管是指针还是句柄, 都不过是内存中的一小块数据(一般用结构描述), 微软并没有公开句柄的结构细节, 猜一下它应该包括: 真实的指针地址、访问权限设置、引用计数等等.

既然 CreateThread 可以返回一个句柄, 说明线程属于 "内核对象". 实际上不管线程属于哪个进程, 它们在系统的怀抱中是平等的; 在优先级(后面详谈)相同的情况下, 系统会在相同的时间间隔内来运行一下每个线程, 不过这个间隔很小很小, 以至于让我们误以为程序是在不间断地运行.

这时你应该有一个疑问: 系统在去执行其他线程的时候, 是怎么记住前一个线程的数据状态的? 有这样一个结构 TContext, 它基本上是一个 CPU 寄存器的集合, 线程是数据就是通过这个结构切换的, 我们也可以通过 GetThreadContext 函数读取寄存器看看. 附上这个结构 TContext(或叫: CONTEXT、_CONTEXT) 的定义:

PContext = ^TContext; _CONTEXT = record ContextFlags: DWORD; Dr0: DWORD; Dr1: DWORD; Dr2: DWORD; Dr3: DWORD; Dr6: DWORD; Dr7: DWORD; FloatSave: TFloatingSaveArea; SegGs: DWORD; SegFs: DWORD; SegEs: DWORD; SegDs: DWORD; Edi: DWORD; Esi: DWORD; Ebx: DWORD; Edx: DWORD; Ecx: DWORD; Eax: DWORD; Ebp: DWORD; Eip: DWORD; SegCs: DWORD; EFlags: DWORD; Esp: DWORD; SegSs: DWORD; end;

2、参数6:输出线程ID CreateThread 的最后一个参数是 "线程的 ID"; 既然可以返回句柄, 为什么还要输出这个 ID? 现在我知道的是: 1、线程的 ID 是唯一的; 而句柄可能不只一个, 譬如可以用 GetCurrentThread 获取一个伪句柄、可以用 DuplicateHandle 复制一个句柄等等. 2、ID 比句柄更轻便. 在主线程中 GetCurrentThreadId、MainThreadID获取的都是主线程的 ID.

MainInstance: Indicates the instance handle for the main executable. Use MainInstance to obtain the instance handle for the main executable of an application. This is useful in applications that use runtime libraries or packages, when you need the handle for the executable rather than for the library.

3、参数5:启动选项 CreateThread 的倒数第二个参数 dwCreationFlags(启动选项) 有两个可选值: 0: 线程建立后立即执行入口函数; CREATE_SUSPENDED: 线程建立后会挂起等待. ResumeThread 恢复线程的运行; SuspendThread 挂起线程. 这两个函数的参数都是线程句柄, 返回值是执行前的挂起计数. 什么是挂起计数? SuspendThread 会给这个数 +1; ResumeThread 会给这个数 -1; 但这个数最小是 0. 当这个数 = 0 时, 线程会运行; > 0 时会挂起. 如果被 SuspendThread 多次, 同样需要 ResumeThread 多次才能恢复线程的运行. ResumeThread 和 SuspendThread 分别对应 TThread 的 Resume 和 Suspend 方法, 很好理解.

4、参数4:函数参数 线程入口函数的参数是个无类型指针(Pointer), 用它可以指定任何数据; 本例是把鼠标点击窗体的坐标传递给线程的入口函数, 每次点击窗体都会创建一个线程.

5、参数3:入口函数指针 到了入口函数了, 学到这个地方, 我查了一个入口函数的标准定义, 这个函数的标准返回值应该是 DWORD, 不过这函数在 Delphi 的 System 单元定义的是: TThreadFunc = function(Parameter: Pointer): Integer; 我以后会尽量使用 DWORD 做入口函数的返回值. 这个返回值有什么用呢? 等线程退出后, 我们用 GetExitCodeThread 函数获取的退出码就是这个返回值! 如果线程没有退出, GetExitCodeThread 获取的退出码将是一个常量 STILL_ACTIVE (259); 这样我们就可以通过退出码来判断线程是否已退出. 还有一个问题: 前面也提到过, 线程函数不能是某个类的方法! 假如我们非要线程去执行类中的一个方法能否实现呢? 尽管可以用 Addr(类名.方法名) 或 MethodAddress('published 区的方法名') 获取类中方法的地址, 但都不能当做线程的入口函数, 原因可能是因为类中的方法的地址是在实例化为对象时动态分配的. 后来换了个思路, 其实很简单: 在线程函数中再调用方法不就得了, 估计 TThread 也应该是这样. CreateThread 第三个参数是函数指针, 新线程建立后将立即执行该函数, 函数执行完毕, 系统将销毁此线程从而结束多线程的故事.

6、参数2:堆栈大小 栈是私有的但堆是公用的

CreateThread 的第二个参数是分配给线程的堆栈大小. 这首先这可以让我们知道: 每个线程都有自己独立的堆栈(也拥有自己的消息队列). 什么是堆栈? 其实堆是堆、栈是栈, 有时 "栈" 也被叫做 "堆栈". 它们都是进程中的内存区域, 主要是存取方式不同(栈:先进后出; 堆:先进先出); "栈"(或叫堆栈)适合存取临时而轻便的变量, 主要用来储存局部变量; 譬如 for i := 0 to 99 do 中的 i 就只能存于栈中, 你把一个全局的变量用于 for 循环计数是不可以的. 现在我们知道了线程有自己的 "栈", 并且在建立线程时可以分配栈的大小. 前面所有的例子中, 这个值都是 0, 这表示使用系统默认的大小, 默认和主线程栈的大小一样, 如果不够用会自动增长; 那主线程的栈有多大? 这个值是可以设定的: Project -> Options -> Delphi Compiler -> Linking(如图) 栈是私有的但堆是公用的, 如果不同的线程都来使用一个全局变量有点乱套; 为解决这个问题 Delphi 为我们提供了一个类似 var 的 ThreadVar 关键字, 线程在使用 ThreadVar 声明的全局变量时会在各自的栈中留一个副本, 这样就解决了冲突. 不过还是尽量使用局部变量, 或者在继承 TThread 时使用类的成员变量, 因为 ThreadVar 的效率不好, 据说比局部变量能慢 10 倍.

7、参数1:安全设置 CreateThread 的第一个参数 lpThreadAttributes 是指向 TSecurityAttributes 结构的指针, 一般都是置为 nil, 这表示没有访问限制; 该结构的定义是:

//TSecurityAttributes(又名: SECURITY_ATTRIBUTES、_SECURITY_ATTRIBUTES) _SECURITY_ATTRIBUTES = record nLength: DWORD; {结构大小} lpSecurityDescriptor: Pointer; {默认 nil; 这是另一个结构 TSecurityDescriptor 的指针} bInheritHandle: BOOL; {默认 False, 表示不可继承} end;

//TSecurityDescriptor(又名: SECURITY_DESCRIPTOR、_SECURITY_DESCRIPTOR) _SECURITY_DESCRIPTOR = record Revision: Byte; Sbz1: Byte; Control: SECURITY_DESCRIPTOR_CONTROL; Owner: PSID; Group: PSID; Sacl: PACL; Dacl: PACL; end;

例子:实现线程函数传参

typedef struct SParam  
{  
    int No;  
    unsigned short chnlID;  
    unsigned short sessionID;  
}uParam,*sParam;  
DWORD WINAPI  AccountManager(PVOID pParam);  
void main()  
{  
     DWORD dwThreadId;  
     HANDLE     hThrd = NULL;   // thread handle  
  
    SParam sparam;  
    SParam *p;  
  
    sparam.No = 1;  
    sparam.chnlID = 1;  
    sparam.sessionID = 1;  
    p = &sparam;  
  
            hThrd = (HANDLE)CreateThread(NULL,  
                0,  
                AccountManager,  
                p,  
                0,  
               dwThreadId;  
}  
  
DWORD WINAPI  AccountManager(PVOID pParam)  
{  
    sParam sparam;  
    sparam = (sParam)pParam;  
  
    try  
    {  
          /*Run为自己写的一个方法,Run(int i,unsigned short chnlID,unsigned short sessionID)*/  
          Run(sparam->No,sparam->chnlID,sparam->sessionID);  
    }  
    catch (...)  
    {  
        logger.error("AccountManager(%d): System error./r/n", threadId);  
    }  
      
}  

延伸 WaitForSingleObject msdn

WaitForSingleObject function msdn的原文:Waits until the specified object is in the signaled state or the time-out interval elapses. 等待,直到指定的对象是在信号状态或超时间隔。 To enter an alertable wait state, use the WaitForSingleObjectEx function. To wait for multiple objects, use WaitForMultipleObjects. 进入一个警戒的等待状态,使用waitforsingleobjectex函数。等多个对象,使用waitformultipleobjects函数。

WaitForSingleObject的原型: 当指定的对象处于有信号状态或者等待时间结束的状态时,此函数返回。 DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); 参数: hHandle:指定对象或事件的句柄; dwMilliseconds: 等待时间,以毫妙为单位,当超过等待时间时,此函数将返回。如果该参数设置为0,则该函数立即返回,如果设置为INFINITE,则该函数直到有信号才返回。 返回值: 如果此函数成功,该函数的返回之标识了引起该函数返回的事件。返回值如下: WAIT_ABANDONED(0x00000080L) 指定的对象是一个互斥对象,该对象没有被拥有该对象的线程在线程结束前释放。互斥对象的所有权被同意授予调用该函数的线程。互斥对象被设置成为无信号状态。 WAIT_OBJECT_0 (0x00000000L) 指定的对象出有有信号状态。 WAIT_TIMEOUT (0x00000102L) 超过等待时间,指定的对象处于无信号状态 如果失败,返回 WAIT_FAILED;

参考: 事件EVENT与waitforsingleobject的使用

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java的JVM介绍以及java的值传递和引用传递

    面试的时候碰到的了一个java基础问题,竟然给问蒙了,回来之后感觉针对这个问题总结一下

    包子388321
  • Android 通过DecorView计算statusBar、navigationBar的高度

    近期在做项目的时候碰到了底部虚拟按键在各个厂商适配的问题,闷逼了一圈,后面搜索一圈,发现即使各大厂商有变动,还是离不开原生本质

    包子388321
  • memset的含义及作用

    struct sample_struct { char csName[16]; int iSeq; int iType; };

    包子388321
  • 《实战Java高并发程序设计》读书笔记

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_35512245/articl...

    大黄大黄大黄
  • Java线程模型

    大家新年好。2018年的第一期来得晚了一些。因为年底有很多工作要做,加班多了一些,公众号停更了两周。 今天借着知乎上一个关于线程模型的问题,我正好可以讲一下Ja...

    海纳
  • JAVA\Android 多线程实现方式及并发与同步

    说到线程,就不得不先说线程和进程的关系,这里先简单解释一下,进程是系统的执行单位,一般一个应用程序即是一个进程,程序启动时系统默认有一个主线程,即是UI线程,我...

    Android技术干货分享
  • python基本常识

    tuple,str都可以看做是一种list,都可以进行切片操作。 利用切片操作,去掉一个字符串的前后空格。要注意是是前后空格是不止一个的,可能有很多个。

    西红柿炒鸡蛋
  • 第二家Amazon Go无人店开业,主要针对上班族

    亚马逊扩展到实体店的步伐并没有放缓的迹象。这家零售商在西雅图的第二家Amazon Go便利店开门,该便利店位于西雅图市中心第五和马里昂的拐角处。和第一个一样,它...

    AiTechYun
  • 亚马逊网红无人店第2家来了!面积×1.6倍,还用不用排长队?

    量子位
  • 尴尬!号称不用排队的亚马逊无人店,开店首日最大问题是:排队太长

    允中 假装发自 7th Ave 量子位 出品 | 公众号 QbitAI ? 51秒。 亚马逊无人店Amazon Go开放首日,第一位顾客从进店到购物离开,用时总...

    量子位

扫码关注云+社区

领取腾讯云代金券