您的位置:首页 > 编程语言 > C语言/C++

c++创建多线程的三种方式的比较 和 多线程通信

2013-10-14 10:07 417 查看

1、介绍

参考
http://blog.sina.com.cn/s/blog_a9303fd901019cwb.html
http://www.docin.com/p-542443867.html
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。

为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。


2、创建线程的三种方式

CreateThread是Windows的API函数(SDK函数的标准形式,直截了当的创建方式,任何场合都可以使用),提供操作系统级别的创建线程的操作,且仅限于工作者线程。不调用MFC和RTL函数(标准C语言函数),即只调用win32API时,可以用CreateThread,其它情况不要轻易。在使用的过程中要考虑到进程的同步与互斥的关系(防止死锁)。
线程函数定义为:DWORD WINAPI _yourThreadFun(LPVOID>CreateThread
没有考虑:

(1)C Runtime中需要对多线程进行纪录和初始化,以保证C函数库工作正常(典型的例子是strtok函数)。

(2)MFC也需要知道新线程的创建,也需要做一些初始化工作(当然,如果没用MFC就没事了)。

AfxBeginThread是MFC中线程创建的MFC函数,首先创建了相应的CWinThread对象,然后调用CWinThread::CreateThread, 在CWinThread::CreateThread中,完成了对线程对象的初始化工作,然后,调用_beginthreadex(AfxBeginThread相比较更为安全)创建线程。它简化了操作或让线程能够响应消息,即可用于界面线程,也可以用于工作者线程,但要注意不要在一个MFC程序中使用_beginthreadex()或CreateThread()。
线程函数定义为:UINT _yourThreadFun(LPVOID pParam)

_beginthreadex是MS对C Runtime库的扩展SDK函数,首先针对C Runtime库做了一些初始化的工作,以保证C Runtime库工作正常。然后,调用CreateThread真正创建线程。仅使用Runtime Library时,可以用_BegingThread。
uintptr_t _beginthread( 

   void( __cdecl *start_address )( void * ),

   unsigned stack_size,

   void *arglist 

);

uintptr_t _beginthreadex( 

   void *security,

   unsigned stack_size,

   unsigned ( __stdcall *start_address )( void * ),

   void *arglist,

   unsigned initflag,

   unsigned *thrdaddr 

);  
//第1个参数:安全属性,NULL为默认安全属性   

//第2个参数:指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0   

//第3个参数:指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)   

//第4个参数:传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针   

//第5个参数:线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)   //第6个参数:用于记录线程ID的地址

_beginthreadex( NULL, 0, threadProc, &pagram, 0,(unsigned int *) idThread );
注意:
_beginthread需要__cdecl的线程函数地址,_beginthreadex和CreateThread需要__stdcall的线程函数地址,如下:

void _cdecl XXX(vooid *);  

unsinged int stdcall(void *);
 _beginthreadex内部会自动调用 _endthreadex.

 _beginthread内部会自动调用_endthread.         

_endthread内部会自动调用CloseHandle关闭当前Thread内核对象的句柄,所以在用_beginthread 时我们不需要在主线程中调用CloseHandle来关闭子线程的句柄。 _endthreadex相比_endthread而言更安全。它不会自动关闭当前Thread内核对象的句柄。所以在用_beginthreadex时我们需要用CloseHandle来关闭子线程的句柄。

小节:实际上,这三个函数之间存在一定的调用关系,第一个纯粹一些,后两个完成自己相应的工作之后,调用前者实现线程的创建。其中CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。
小节:用_beginthreadex()、_endthreadex函数应该是最佳选择,且都是C Run-time Library中的函数,函数的参数和数据类型都是C Run-time Library中的类型,这样在启动线程时
160cd
就不需要进行Windows数据类型和C Run-time Library中的数据类型之间的转化,从而,减低了线程启动时的资源消耗和时间的消耗。但使用_beginthread,无法创建带有安全属性的新线程,无法创建暂停的线程,也无法获得线程ID,_endthread的情况类似,它不带参数,这意味这线程的退出代码必须硬编码为0。
MFC也是C++类库(只不过是Microsoft的C++类库,不是标准的C++类库),在MFC中也封装了new和delete两中运算符,所以用到new和delete的地方不一定非要使用_beginthreadex() 函数,用其他两个函数都可以。
_beginthreadex和_beginthread在回调入口函数之前进行一些线程相关的CRT的初始化操作。

另外一些讨论:
CreateThread 使用不当引起内在泄露?

CreateThread 和 _beginthreadex 区别。在 Win32 API 中,创建线程的基本函数是 CreateThread,而 _beginthread(ex) 是C++ 运行库的函数。为什么要有两个呢?因为C++运行库里面有一些函数使用了全局量,如果使用 CreateThread 的情况下使用这些C++运行库的函数,就会出现不安全的问题。而_beginthreadex 为这些全局变量做了处理,使得每个线程都有一份独立的“全局”量。所以,如果你的编程只调用 Win32 API/SDK ,就放心用
CreateThread;如果要用到C++运行时间库,那么就要使用_beginthreadex ,并且需要在编译环境中选择 Use MultiThread Lib/DLL。

C++ 运行期库有两个创建线程的函数,另一个是 _beginthread, 它们两者的区别请自己去看MSDN。

通常他们的解释都是这容易造成内存泄漏。这个解释本身是没有错的,但是解释得不够完全和详细。以至于造成很多新手盲目的信任了那句话,在那里都是用_beginthreadex函数,或者是装作没有看到使用CreateThread函数。曾经有一段时间我也对这个问题很是困惑,不知道到底用那个才是对的。因为我不止一次在很多权威性的代码中看到对CreateThread函数的直接调用。难道是权威错了??抱着怀疑的态度查找了大量的资料和书籍,终于搞明白了这个问题的关键所在,在此做个说明,算是对那句话的一个完善。

关于_beginthreadex和CreateThread的区别我就不做说明了,这个很容易找到的。我们只要知道一个问题:_beginthreadex是一个C运行时库的函数,CreateThread是一个系统API函数,_beginthreadex内部调用了CreateThread。只所以所有的书都强调内存泄漏的问题是因为_beginthreadex函数在创建线程的时候分配了一个堆结构并和线程本身关联起来,我们把这个结构叫做tiddata结构,是通过线程本地存储器TLS于线程本身关联起来。我们传入的线程入口函数就保存在这个结构中。tiddata的作用除了保存线程函数入口地址之外,还有一个重要的作用就是:C运行时库中有些函数需要通过这个结构来
保存和获取一些数据,比如说errno之类的线程全局变量。这点才是最重要的。当一个线程调用一个要求tiddata结构的运行时库函数的时候,将发生下面的情况:运行时库函数试图TlsGetvalue获取线程数据块的地址,如果没有获取到,函数就会现场分配一个tiddata结构,并且和线程相关联,于是问题出现了,如果不通过_endthreadex函数来终结线程的话,这个结构将不会被撤销,内存泄漏就会出现了。但通常情况下,我们都不推荐使用_endthreadex函数来结束线程,因为里面包含了ExitThread调用。找到了内存泄漏的具体原因,我们可以这样说:只要在创建的线程里面不使用一些要求tiddata结构的运行时库函数,我们的内存时安全的。所以,前面说的那句话应该这样说才完善:“绝对不要调用系统自带的CreateThread函数创建新的线程,而应该使用_beginthreadex,除非你在线程中绝不使用需要tiddata结构的运行时库函数”这个需要tiddata结构的函数有点麻烦了,在侯捷的《win32多线程程序设计》一书中这样说到:”如果在除主线程之外的任何线程中进行一下操作,你就应该使用多线程版本的C
runtime library,并使用_beginthreadex和_endthreadex:

1 使用malloc()和free(),或是new和delete

2 使用stdio.h或io.h里面声明的任何函数

3 使用浮点变量或浮点运算函数

4 调用任何一个使用了静态缓冲区的runtime函数,比如:asctime(),strtok()或rand()
有些CRT的函数象malloc(),   fopen(),   _open(),   strtok(),   ctime(),   或localtime()等函数需要专门的线程局部存储的数据块,这个数据块通常需要在创建线程的时候就建立,如果使用CreateThread,这个数据块就没有建立,然后会怎样呢?在这样的线程中还是可以使用这些函数而且没有出错,实际上函数发现这个数据块的指针为空时,会自己建立一个,然后将其与线程联系在一起,这意味着如果你用CreateThread来创建线程,然后使用这样的函数,会有一块内存在不知不觉中创建,遗憾的是,这些函数并不将其删除,而CreateThread和ExitThread也无法知道这件事,于是就会有Memory
  Leak,在线程频繁启动的软件中(比如某些服务器软件),迟早会让系统的内存资源耗尽!   

因为对产生的线程而言,_beginthreadex比之CreateThread会为上述操作多做额外的簿记工作,比如帮助strtok()为每个线程准备一份缓冲区。

然而多线程程序极少情况不使用上述那些函数(比如内存分配或者io),所以与其每次都要思考是要使用_beginthreadex还是CreateThread,不如就一棍子敲定_beginthreadex。
当然你也许会借助win32来处理内存分配和Io,这时候你确实可以以单线程crt配合CreateThread,因为io的重任已经从crt转交给了win32。这时通常你应该使用HeapAlloc,HeapFree来处理内存分配,用CreateFile或者GetStdHandle来处理Io。
还有一点比较重要的是_beginthreadex传回的虽然是个unsigned long,其实是个线程Handle(事实上_beginthreadex在内部就是调用了CreateThread),所以你应该用CloseHandle来结束他。千万不要使用ExitThread()来退出_beginthreadex创建的线程,那样会丧失释放簿记数据的机会,应该使用_endthreadex.

_endthreadex使用注意事项

转自http://blog.csdn.net/kailan818/article/details/5928850

#include <string>
#include <process.h>
#include <windows.h>
#include <iostream>
using namespace std;

unsigned __stdcall th_ThreadA()
{
string a;

_endthreadex(0);

return 0;
}

这里如果显式调用_endthreadex则string会泄露,如果直接return则不会,但是为什么网上的示例代码包括msdn都显示调用_endthreadex,并且说这样可以防止内存泄露?

std::string的资源释放是由析构函数进行的,C++会保证但是析构函数是在return 以后发生的,这个代码return语句永远不会被运行,因为_endthreadex的存在,和exit是类似的。所以如果不是提前退出的话,你可以不写endthreadex,函数返回后系统会调用_endthreadex的,参见《Windows核心编程》。


如果你要提前退出的话,请使用大括号:

{

  string a;

  ...

} // a released here

_endthreadex(0);

那既然任何需要退出的地方都能用return完成是不是endthreadex就没有显式调用的意义了?

应该是的吧,看msdn上也没说有调用的必要,return的时候会自动调用_endthreadex。如果你是 _beginthreadex创建的话调用_endthreadex是必须的,但不是指显示调用,这是我对msdn有关说明的理解。所谓必须是清理_beginthreadex为thread创建的tiddata。

要说_endthreadex完全没有用肯定是不对的,_endthreadex并不是一个过时的函数,正确的使用并不会带来问题。比如在线程的主函数中,return是_endthreadex的一个良好替代,就像main函数里面return是exit()或ExitProccess()的良好替代一样,但是这不表示exit函数没用。比如线程调用了一个子函数,如果子函数决定退出线程,return是没用的,_endthreadex即可终结线程。

但是这个设计不好,因为可能造成LZ提出的资源泄漏。尤其考虑到后台线程终结后的资源泄漏比主线程的资源泄漏更要命(主线程退出后,进程就退出了,OS会清理一切资源,无所谓泄露不泄露,而子线程退出后主线程可能还会运行很久,并且可能有大量的同类型的子线程退出,会造成要命的泄露)。良好的设计还是返回线程的主函数,让threadproc来决定是不是要退出,从这个意义上说,_endthreadex没有必要。

微软也指出,有些程序员就是要调用exit系列函数(ExitThread,ExitProccess等),没辙,只好提供了。

转自:http://bbs.sjtu.edu.cn/bbscon,board,VC,file,M.1256696811.A.html

3、多线程通信的方法主要有以下三种 

1.全局变量

由于同一进程下的线程之间共享数据空间。当需要有多个线程来访问一个全局变量时,通常我们会在这个全局变量前加上volatile声明,以防编译器对此变量进行优化。

2.Message消息机制

常用的Message通信的接口主要有两个:PostMessage和PostThreadMessage,

PostMessage为线程向主窗口发送消息。而PostThreadMessage是任意两个线程之间的通信接口。

PostMessage() 

函数原型:

    B00L PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);

参数:

    hWnd:其窗口程序接收消息的窗口的句柄。可取有特定含义的两个值:

    HWND.BROADCAST:消息被寄送到系统的所有顶层窗口,包括无效或不可见的非自身拥有的窗口、被覆盖的窗口

和弹出式窗口。消息不被寄送到子窗口。

    NULL:此函数的操作和调用参数dwThread设置为当前线程的标识符PostThreadMessage函数一样。

    Msg:指定被寄送的消息。

    wParam:指定附加的消息特定的信息。

    IParam:指定附加的消息特定的信息。

    返回值:如果函数调用成功,返回非零值:如果函数调用失败,返回值是零。

MS还提供了SendMessage方法进行消息间通讯,SendMessage(),他和PostMessage的区别是:

SendMessage是同步的,而PostMessage是异步的。SendMessage必须等发送的消息执行之后,才返回。

PostThreadMessage方法可以将消息发送到指定线程。

函数原型:BOOL PostThreadMessage(DWORD idThread,UINT Msg,WPARAM wParam, LPARAM lParam);

参数除了ThreadId之外,基本和PostMessage相同。

目标线程通过GetMessage()方法来接受消息。

注:使用这个方法时,目标线程必须已经有自己的消息队列。否则会返回ERROR_INVALID_THREAD_ID错误。可以用

PeekMessage()给线程创建消息队列。

PostThreadMessage可以用于线程之间的异步通讯,因为它不用等待调用者返回,这也许是线程通讯中最简单的一种方法了。但是要注意以下问题。

1 .PostThreadMessage有时会失败,报1444错误(Invalid thread identifier. )其实这不一定是线程不存在的原因,也有可能是线程不存在消息队列(message queue)造成的。事实上,并不是每个thread都有message queue,那如何让thread具有呢?答案是,至少调用message相关的function一次,比如GetMessage,PeekMessage。

2.如果是post动态分配的memory给另外一个thread,要注意内存的正确释放。

3.PostThreadMessage不能够post WM_COPYDATE之类的同步消息,否则会报错

4.最好不要使用PostThreadMessage post message给一个窗口,使用PostMessage替代。

下面是我写的一个比较严整的例子,仅供参考。

// 123.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include <stdlib.h>
#include <stdio.h>
#include <windows.h>

#define MY_MSG (WM_USER + 1)

DWORD WINAPI Thread1(LPVOID para)
{
DWORD dwThreadId = *(DWORD *)para;
DWORD i = 0;
TCHAR *p;
char strTmp[100];

while(TRUE)
{
Sleep(1700);
p = new TCHAR[10];
sprintf_s(strTmp, 100, "Hello %d %x ", i++, p);
PostThreadMessage(dwThreadId, MY_MSG, (WPARAM)strTmp, (LPARAM)p);
//delete []p;
}
return 0;
}

DWORD WINAPI Thread2(LPVOID para)
{
MSG msg;
PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

while(true)
{
if(GetMessage(&msg,0,0,0)) //get msg from message queue
{
switch(msg.message)
{
case MY_MSG:
char * pInfo = (char *)msg.wParam;
printf("recv:%s\n",pInfo);
delete [](TCHAR *)msg.lParam;
break;
}
}
};
return 0;
}

int main()
{

DWORD dwValue = GetCurrentThreadId();
HANDLE hThread1 = CreateThread(NULL, 0, &Thread2, &dwValue, 0, &dwValue); // &Thread1可写作Thread1, 都是函数指针.
HANDLE hThread2 = CreateThread(NULL, 0, &Thread1, &dwValue, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
switch(msg.message)
{
case MY_MSG:
printf("msg: 0x%x      w: %x   ws: %s     l: %x\n", msg.message, &msg.wParam, msg.wParam, msg.lParam);
delete [](TCHAR *)msg.lParam;  //注释掉这句你就会看到堆内存地址变化
break;
default:
printf("Unknown msg:0x%x\n",msg.message);
break;
}
Sleep(1);
}

return 0;
}


3.CEvent对象

CEvent为MFC中的一个对象,可以通过对CEvent的触发状态进行改变,从而实现线程间的通信和同步,这个主要是实现线程直接同步的一种方法。
 

4、多线程通信的互斥和同步

在一个多线程的应用程序中,所有线程共享进程资源,协同工作。所以,线程之间的通信是编写多线程应用的必不可少的环节。线程之间的通信包括互斥、同步等,它是多线程设计中最难控制的部分,也是关键部分。

1、线程间的互斥(a执行时b不能执行)

 (1) 临界区

  在一个多线程的应用程序中,可能存在这样的危险:一个线程以某种其他线程不可预料的方式修改资源。

  例如两个线程都对同一个文件进行读写,两个线程都在进行绘图,一个线程正在使用一段内存而另一个线程却正在修改这段内存的值等,都可能出现不可预料的结果。

  可以把不允许多个线程交叉运行的一段程序称为临界区。它是由于多个线程共享公用数据或公用变量而引起的。

  临界区也被称为:访问公用数据的那段程序。

 (2) 互斥

  为使多个线程在进入自己的临界区时不出现问题,需要实现线程的互斥运行。它应满足:

   ·各线程平等,都可随时进入临界区。

   ·一个不在临界区执行的线程,不可以阻止其他线程进入临界区。

   ·当一个线程正在临界区内执行时,必须阻止其他线程进入临界区。

   ·多个线程申请进入临界区时,只能允许一个线程进入。

   ·一个申请进入临界区的线程,应该能在有限时间内进入,不会发生死锁。

 (3) 临界区类

  MFC定义了临界区封装类CCriticalSection,可以比较方便的实现线程间的互斥。

  首先定义一个CCriticalSection类对象,该对象通常应被定义为全局变量,以便跨线程使用。

  当线程要进入临界区时,调用其成员函数Lock()。若临界区空闲,该函数将锁定临界区;若临界区已经被锁定,该函数将被阻塞(不耗费机时),直至临界区解锁。

  当线程要退出临界区时,调用其成员函数Unlock()解锁,以便其他线程可以进入临界区。

  使用临界区,实现线程互斥。

2、线程间的同步(b执行的前提是a执行完毕)

 

  有时一个线程的执行条件是另一个线程的执行结果,所以只有等待另一个线程完成某项操作后,该线程才可继续执行。例如计算线程和打印线程。

  这种由于线程间的功能逻辑关系引起的,称为线程间的直接制约。而由于共享资源引起的线程执行速度的制约,称为间接制约。

  存在直接制约关系的一个线程可以在另一个线程的执行条件满足后,给对方发送相应消息或信号。这样,被制约的线程可以在条件不满足时处于阻塞状态,条件满足后,被对方唤醒继续工作。

  这就是线程间的同步方式。

     
等待函数和事件对象、互斥对象、信号量对象等一起使用,实现线程同步。



 (1) 等待函数

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

函数WaitForMultipleObjects()可以同时等待多各对象的信号状态。

DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWaitAll, DWORD dwMilliseconds);

  其中:nCount为等待的对象的个数。

     lpHandles为存放等待的对象的数组。

     fWaitAll等于TRUE指明所有对象均有信号时返回。

     fWaitAll等于FALSE指明其中之一有信号时返回。
参数
hHandle[in]对象句柄。可以指定一系列的对象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
当等待仍在挂起状态时,句柄被关闭,那么函数行为是未定义的。该句柄必须具有 SYNCHRONIZE 访问权限。

 
线程的句柄在WIN32中可以作为信号量使用(创建线程后,不能马上CloseHandle)。当线程结束时,其状态由非信号状态转变为信号状态。可以使用WaitForSingleObject函数来等线程对象。
创建线程后,是不是关闭线程句柄?不关闭不会引起内存泄露,不过会占用一些系统资源。如果不再使用(比如得到退出代码,等待退出之类),还是关了好,毕竟是良好的编程习惯。

dwMilliseconds[in]定时时间间隔,单位为milliseconds(毫秒).如果指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。如果dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它总是立即返回。如果dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。
返回值
执行成功,返回值指示出引发函数返回的事件。它可能为以下值:

  
WAIT_ABANDONED 0x00000080L
The specified object is a mutex object that was not released by the thread that owned the mutex object before the owning thread terminated. Ownership of the mutex object is granted to the calling thread and the mutex state is set to nonsignaled.
If the mutex was protecting persistent state information, you should check it for consistency.
WAIT_OBJECT_0 0x00000000L
The state of the specified object is signaled.
WAIT_TIMEOUT 0x00000102L
The time-out interval elapsed, and the object's state is nonsignaled.
WAIT_FAILED(DWORD)0xFFFFFFFF
The function has failed. To get extended error information, callGetLastError.
 
WaitForSingleObject函数用来检测hHandle事件的信号状态,在某一线程中调用该函数时,线程暂时挂起,如果在挂起的dwMilliseconds毫秒内,线程所等待的对象变为有信号状态,则该函数立即返回;如果超时时间已经到达dwMilliseconds毫秒,但hHandle所指向的对象还没有变成有信号状态,函数照样返回。参数dwMilliseconds有两个具有特殊意义的值:0和INFINITE。若为0,则该函数立即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。
返回值:
WAIT_ABANDONED 0x00000080:当hHandle为mutex时,如果拥有mutex的线程在结束时没有释放核心对象会引发此返回值。
WAIT_OBJECT_0 0x00000000 :核心对象已被激活
WAIT_TIMEOUT 0x00000102:等待超时
WAIT_FAILED 0xFFFFFFFF :出现错误,可通过GetLastError得到错误代码
在这里举个例子:
先创建一个全局Event对象g_event:
CEvent g_event;
在程序中可以通过调用CEvent::SetEvent设置事件为有信号状态。
下面是一个线程函数MyThreadPro()
UINT MyThreadProc( LPVOID pParam )
{
    WaitForSingleObject(g_event,INFINITE);
    For(;;)
    {
      ………….
     }
     return 0;
}
在这个线程函数中只有设置g_event为有信号状态时才执行下面的for循环,因为g_event是全局变量,所以我们可以在别的线程中通过g_event. SetEvent控制这个线程。
还有一种用法就是我们可以通过WaitForSingleObject函数来间隔的执行一个线程函数的函数体
UINT MyThreadProc( LPVOID pParam )
{
    while(WaitForSingleObject(g_event,MT_INTERVAL)!=WAIT_OBJECT_0)
    {
        ………………
    }
    return 0;
}
在这个线程函数中可以通过设置MT_INTERVAL来控制这个线程的函数体多久执行一次,当事件为无信号状态时函数体隔MT_INTERVAL执行一次,当设置事件为有信号状态时,线程就执行完毕了。

          (2) CEvent事件对象

  MFC定义了CEvent类封装了系统的事件对象。它可以延迟一个线程的运行以等待另一个线程。

  事件对象可以是有信号状态或无信号状态,可以被等待函数调用。函数SetEvent()和ResetEvent()用于将事件置为有信号状态或无信号状态。

  事件对象有两种复位方式:

   ·手工复位:当有信号时,无论释放了多少等待的线程,只有在ResetEvent()后才置为无信号。

   ·自动复位:当有信号时,只要释放了一个等待的线程,就自动转为无信号。其他等待线程或同一线程的下一次等待不被释放。

  事件对象的初始化:

    CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE);

  其中:bInitiallyOwn =FALSE初始化为无信号状态,=TRUE初始化为有信号状态。

     bManualReset =FALSE为自动复位,=TRUE为手工复位。











当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程,同时操作系统会将该事件对象设置为无信号状态,这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置的事件对象,在一个线程得到该事件对象之后,操作系统不会将该事件对象设置为无信号状态,除非显式地调用ResetEvent函数将其设置为无信号状态,否则该对象会一直是有信号状态。

  (3)互斥对象

 

  MFC定义了CMutex类封装了系统的互斥对象。它可以保护共享资源防止其为多个线程同时访问。

  互斥对象与临界区非常相似,只是互斥对象可在进程间使用,而临界区只能用于同一进程的线程,但其速度相对较快。

  互斥对象在未被任何线程拥用时为有信号状态,当被任一个线程拥有时为无信号。

  当关于互斥对象的等待函数返回成功时,互斥对象变为无信号。当进程结束后,则需调用Unlock()函数以使互斥对象变为有信号
。(注意互斥对象和事件对象的相似和不同之处)
  互斥对象的初始化:

  CMutex(BOOL bInitiallyOwn = FALSE);

  其中:bInitiallyOwn 为互斥对象的初始状态。





 

(4)信号量

      MFC定义了CSemaphore类封装了系统的信号量对象。它是维护一个从0到指定的最大值的计数的同步对象。

当信号量对象创建时,拥有一个初始计数,当线程使用和释放信号量时,计数增减。只要计数大于0,信号量都处于有信号状态,若计数为0,信号量处于无信号状态。

信号量主要用于限制线程的最大数目。当关于信号量的等待函数返回成功时,信号量的计数自动减一。当进程结束后,则需调用Unlock()函数以使计数加一。

信号量对象的初始化:

   CSemaphore(LONG lInitialCount = 1, LONG lMaxCount = 1);

其中: lInitialCount 为信号量的初始计数。

     lMaxCount 为信号量的最大计数。

(5)互斥对象、事件对象与临界区的比较

互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但是利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
临界区工作在用户方式下,同步速度较快,但在使用临界区时,很容易进入死锁状态,因为在等待进入临界区时无法设定超时值。

5、为了保证界面的用户体验经常要把数据处理等放到子线程中进行,然后把结果更新到主界面,通常有这样几种方法。

   1.启动线程时把控件关联变量的指针传参给线程函数,这种方法无疑是最简单的方法。

   2.就是先进一点的方法,把控件的句柄传给线程函数,在子线程中通过SendNotifyMessage or PostMessage等进行操作。这种方法是线程安全的,但对许多未公开控件你根本不知道要发送什么消息的,比如绝大多数的ActiveX控件,像MSFLEXGRID、DBGRID,它们的消息ID是无从知晓的,这种时候第二种方法就没用了。

   3.这种方法我感觉是最万能的方法了,而且这样代码的逻辑也最清楚。就是在窗口类中针对线程要进行的操作自定义消息,启动线程时直接把窗口的句柄传给线程函数,线程要控制界面时直接给窗口发消息就可以了。窗口类中对自定义消息添加消息映射,在其中进行具体的控制操作。

   4.定时器。





内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息