线程(VC_Win32)

进程还可以通过套接字进行通信


线程概述

组成

  • 内核对象   操作系统用它来对线程实施管理,内核对象也是系统用来存放线程统计信息的地方,但创建线程时,系统创建一个内核对象,该线程内核对象不是线程本身,而是操作系统用来管理线程的较小数据结构,可以将线程内核对象视为有关于线程的统计信息组成的一个小型数据结构
  • 线程栈   它用于维护线程在执行代码时需要的所有函数参数和局部变量

注意

  • 线程总是在某个进程环境中创建的,系统从进程的地址工具中分配内存,供线程的栈使用,新的线程运行的进程环境与创建线程的环境相同,因此,新线程可以访问进程的内核对象的所有句柄,进程中的所有内存和在这个相同进程中的所有线程的堆栈.这使得单个进程中的多个线程确实能非常容易的相互通信
  • 线程只有一个内核和一个堆栈,保留记录很少,因此所需要的内存也很少,由于线程需要的开销比较少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程

线程运行机制

操作系统为每个运行线程安排一定的CPU时间----时间片,系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因为时间相当短,多个线程频繁地发生切换,因此给用户的感觉就是好像多个线程同时运行一样,但是如果计算机有多个CPU,线程就能真正意义上的同时运行了

生命周期

在创建子线程的线程中要是该线程结束了生命周期子线程也会结束生命周期

优点

  • 每一个线程可以独立地完成一个任务.当该程序一直到多CPU的平台上时,其中的多个线程就可以真正并发进行地同时运行了
  • 相对于进程来说
    • 对进程创建来说,系统要分配进程很大的私有空间,当然它占用的资源也就很多,而对多线程程序来说,多个线程共享一个地址空间,所以占用资源较少
    • 进程间切换时,需要交换整个地址空间,而线程之间切换时候只是切换执行环境,因此效率更高

单线程与多线程的执行区别 

线程(VC_Win32)_第1张图片

注意

多线程访问共享变量时要避免多个线程同时对共享变量进行操作


线程相关函数详解

[进程创建][互斥对象][事件对象][关键代码段/临界资源]

进程创建相关函数

[creathread][线程入口函数原型][Sleep][CloseHandle]

CreateThread线程创建函数
函数原型

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

参数说明

  • lpThreadAttributes   指向SECURITY_ATTRIBUTES结构体指针,这里可以传递NULL,让该线程使用默认的安全性.但是,如果希望所有的子进程能够继承该线程对象的句柄,就必须设定一个SECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE
  • dwStackSize:  设置线程初始堆栈大小,即线程可以将多少空间用于自己的栈,以字节为单位,系统会将这个参数值四舍五入到最接近页面大小,当保留地址空间的一块区域时,系统要确保该区域的大小是系统页面的大小的整数倍(页面是系统管理内存时使用的的内存单位,不同CPU其页面大小不同)
  • lpStartAddress:   指向应用程序定义的LPTHREAD_START_ROUTINE类型的函数指针,这个函数将由新线程执行,表明新线程的起始地址
  • lpParameter:   对于main函数来说可以寄售命令行的参数.同样我们也可以通过这个参数给创建的新线程传递参数.参数的值可以是一个数值,也可以使一个指向其他信息的指针
  • dwCreationFlags:   设置用于控制线程创建的附加标记,它可以是两个值得其中一个
    • CREATE_SUSPENDED  线程创建后处于暂停状态,直到程序调用了ResumeThread函数位置)
    • 0  线程在创建后立即运行
  • lpThreadId:   这个参数是一个返回值,它指向一个变量,来接收线程ID,当创建一个线程时,系统会为该线程分配一个ID(在windows 2000和windows NT4下,可以为NULL,但在windows95 和 windows 98下此参数不能为NULL)

返回值

新建立的线程句柄

threadProName 线程入口函数

函数原型

DWORD WINAPI threadProName(LPVOID lpParameter);

参数说明

  • lpParamer:   为创建线程CreateThread函数中的lpParameter参数的值

Sleep 线程睡眠函数

函数原型

void Sleep(DWORD dwMilliseconds);

参数说明

  • dwMilliseconds 指定线程的睡眠时间,单位为毫秒

CloseHandle 关闭线程句柄

函数原型

BOOL CloseHandle(
HANDLE hObject // handle to object to close
);

参数说明

  • hObject 要关闭的句柄

返回值

操作成功时候返回非零值,操作失败返回0

用途说明

若想要子线程可以运行而主线程不执行操作①可以让主线程睡眠,②可以让主线程执行循环空操作,但是用这种方式的话,对于主线程来说主线程是可以运行的,并且它会占有一定的CPU时间,这样会影响到MultiThread程序的执行效率.
说明 CreateThread启动了一个线程,同时产生一个句柄让你好操纵这个线程,如果你不要用这个句柄了就CloseHandle关掉它.调用这个CloseHandle并不意味着结束线程,而是表示不关心此句柄的状态了,也就无法控制子进程的线程了.如果需要关心,可以在子进程结束后再CloseHandle,但一定得CloseHandle.
CloseHandle使指定的句柄无效,减少对象的句柄计数,进行对象保持检验.当对象的最后一个句柄关闭时,对象将从系统中删除.关闭一个线程句柄并 不会终止一个线程,要释放一个线程对象,必须terminate线程,然后关闭所有的线程句柄.用CloseHandle只能关闭由CreateFile 函数返回的句柄.用FindClose来关闭由FindFirstFile返回的句柄.

互斥对象相关函数

[CreateMutex][ReleaseMutex][WaitForSingleObject]

CreateMutex 创建或打开互斥对象

函数原型 

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwer,
LPCTSTR lpName
);

参数说明

  • lpMutexAttributes:   一个指向LPSECURITY_ATTRIBUTES结构的指针,可以将该参数传递NULL值,让互斥对象使用默认的安全性
  • bInitialOwer:   BOOL类型,指定互斥对象初始的拥有者,如果该值为true,则创建这个互斥对象的线程获得该互斥对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权
  • lpName:   指定互斥对象的名称,如果此参数为NULL,则创建一个匿名的互斥对象

返回值

如果调用成功,该函数将返回所创建的互斥对象的句柄.如果创建的是命名的互斥对象,并且在CreateMutex函数调用之前,该命名的互斥对象存在,那么该函数将返回ERROR_ALREADY_EXISTS.

注意

  • 当线程对共享资源访问结束后,应释放对象的所有权,也就是让该对象处于已通知状态.这时需要调用ReleaseMutex函数,释放指定对象的所有权
  • 可以用这个函数来保证应用程序只有一个实例
  • 调用CreateMutex创建或打开一个命名的互斥对象或者创建一个匿名的互斥对象

ReleasMutex 释放互斥对象

函数原型

BOOL ReleaseMutex(HANDLE hMutex);

参数说明

  • hMutex:  指向需要释放互斥对象的句柄

返回值

调用成功返回非零值,调用失败返回0值

注意

调用ReleaseMutex函数释放互斥对象的所有权时候,操作系统会判断线程的ID与互斥对象内部所维护的线程ID是否一致,只有一致才执行释放操作.也就是说互斥对象谁拥有谁释放

WaitForSingleObject 等待互斥对象被释放后获得互斥对象拥有权

函数原型

DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);

参数说明

  • hHandle:   所请求的对象句柄.一旦互斥对象处于有型号状态,则该函数就返回,如果互斥对象始终无信号状态,即为通知的状态,则该函数就会一直等待,这样就会暂停线程的执行
  • dwMilliseconds:   指定等待的时间间隔,以毫秒为单位.如果指定的时间间隔已过,即所请求的对象仍然处于无信号状态,则该函数就返回,如果此参数设置为0,那么函数将测试该对象的状态并立即返回,如果此参数设置为INFINITE,则该函数将会永远等待,直到等待到对象处于有信号状态才会返回

返回值

  • WAIT_OBJECT_0:   请求的对象是有信号
  • WAIT_TIMEOUT:   指定的时间间隔已经过,并且所请求的对象是无信号的
  • WAIT_ABANDONED:   所请求的对象是一个互斥对象,并且先前拥有的该对象的线程在终止前没有释放该对象,这时,该对象的所有权将授予但其调用的进程,并且该互斥对象被设置为无信号状态

注意

由函数的返回值可以获得担心线程是如何得到互斥对象的所有权,以便在程序中做出对应的处理

事件对象相关函数

[CreateEvent][SetEvent][ResetEvent]

创建事件对象 CreateEvent

函数原型

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // pointer to security attributes
BOOL bManualReset, // flag for manual-reset event
BOOL bInitialState, // flag for initial state
LPCTSTR lpName // pointer to event-object name
);

参数说明

  • pEventAttributes:   指向SECURITY_ATTRIBUTES结构体的指针,如果其值为NULL的话使用默认的安全性
  • bManualReset:   用于指定创建的是人工重置事件还是自动重置事件.
    • TRUE:     人工重置事件
    • FALSE:    自动重置事件
  • bInitialState:   指定了对象的初始状态
    • TRUE:     事件对象初始值为有信号状态
    • FALSE:    事件对象初始值为无信号状态
  • lpName:   指定该事件的名称,如果为NULL则创建一个匿名的事件对象(通过创建一个命名的事件对象,也可以实现引用程序只有一个实例运行)

返回值

如果函数成功,返回创建新的该事件的句柄,如果是命名事件且已经存在,则返回前面声明的事件对象句柄,和调用GetLastError函数会返回ERROR_ALREADY_EXISTS..如果失败,则返回NULL,可以在GetLastError获得错误信息

设置事件对象状态 SetEvent

函数原型

BOOL SetEvent(HANDLE hEvent);

参数说明

  • hEvent:   指定要设置其状态的事件对象的句柄

返回值

若成功返回非零值,若失败返回零,错误信息可以调用GetLastError函数获得

作用

把指定事件设置为有信号状态

重置事件对象状态 ResetEvent

函数原型

BOOL ResetEvent(HANDLE hEvent);

参数说明

  • hEvent:    指定要设置其状态的事件对象的句柄

返回值

若成功返回非零值.若失败返回零,错误信息可以调用GetLastError函数获得

作用

把指定事件设置为无信号状态

关键代码段/临界资源相关函数

[InitializeCriticalSection][DeleteCriticalSection][EnterCriticalSection][LeaveCriticalSection]

初始化关键字函数 InitializeCriticalSection

函数原型

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

参数说明

  • lpCriticalSection:   指向CRITICAL_SECTION结构体的指针该参数是out类型,即做返回类型使用的

删除关键代码段 DeleteCriticalSection

函数原型

VOID DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

参数说明

  • lpCriticalSection:   指向要回收的CRITICAL_SECTION结构体的指针该参数

进入关键代码段 EnterCriticalSection

函数原型 

VOID EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

参数说明

  • lpCriticalSection:   指向要进入的CRITICAL_SECTION结构体的指针该参数

退出关键代码段 LeaveCriticalSection

函数原型

VOID LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

参数说明

  • lpCriticalSection:   指向要退出的CRITICAL_SECTION结构体的指针该参数

线程创建

创建过程:

线程(VC_Win32)_第2张图片

程序样例

程序源码:

#include 
#include 
#include 
using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParamete);

int tickets=10;

void main()
{
    HANDLE hThread1,hThread2;
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    
    Sleep(2000);
    system("pause");
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    while(true)
    {
        Sleep(1);
        if(tickets>0)
        {
            Sleep(10);
            cout<<"thread1 sell ticket : "<0)
        {
            Sleep(10);
            cout<<"thread2 sell ticket : "<

运行结果:


线程睡眠(CPU时间片分配方式)

Unix系统

Unix系统使用的是时间片算法

定义

在时间片算法中,所有的进程排成一个队列.操作系统按照他们的顺序,给每个进程分配一段时间,即该进程 允许运行的时间.如果在 时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程.如果进程在时间片结束前阻塞或结束,则CPU当即进行切换.调度程序所要做的就是维护一张就绪进程列表,,当进程用完它的时间片后,它被移到队列的末尾

说明

我们用分蛋糕的场景来描述这这种算法.假设有源源不断的蛋糕(源源不断的时间),一副刀叉(一个CPU),10个等待吃蛋糕的人(10 个进程)
Windows 操作系统来负责分蛋糕的,那么场面就很有意思了.他会这样定规矩:我会根据你们的优先级、饥饿程度去给你们每个人计算一个优先级.优先级最高的那个人,可 以上来吃蛋糕——吃到你不想吃为止.等这个人吃完了,我再重新根据优先级、饥饿程度来计算每个人的优先级,然后再分给优先级最高的那个人.这样看来,这个场面就有意思了——可能有些人是PPMM,因此具有高优先级,于是她就可以经常来吃蛋糕.可能另外一个人是个丑男,而去很ws,所以优先级 特别低,于是好半天了才轮到他一次(因为随着时间的推移,他会越来越饥饿,因此算出来的总优先级就会越来越高,因此总有一天会轮到他的).而且,如果一不 小心让一个大胖子得到了刀叉,因为他饭量大,可能他会霸占着蛋糕连续吃很久很久,导致旁边的人在那里咽口水...而且,还可能会有这种情况出现:操作系统现在计算出来的结果,5号PPMM总优先级最高,而且高出别人一大截.因此就叫5号来吃蛋糕.5号吃了一小会儿, 觉得没那么饿了,于是说“我不吃了”(挂起).因此操作系统就会重新计算所有人的优先级.因为5号刚刚吃过,因此她的饥饿程度变小了,于是总优先级变小 了;而其他人因为多等了一会儿,饥饿程度都变大了,所以总优先级也变大了.不过这时候仍然有可能5号的优先级比别的都高,只不过现在只比其他的高一点点 ——但她仍然是总优先级最高的啊.因此操作系统就会说:5号mm上来吃蛋糕……(5号mm心里郁闷,这不刚吃过嘛……人家要减肥……谁叫你长那么漂亮,获 得了那么高的优先级). 那么,Thread.Sleep 函数是干吗的呢?还用刚才的分蛋糕的场景来描述.上面的场景里面,5号MM在吃了一次蛋糕之后,觉得已经有8分饱了,她觉得在未来的半个小时之内都不想再 来吃蛋糕了,那么她就会跟操作系统说:在未来的半个小时之内不要再叫我上来吃蛋糕了.这样,操作系统在随后的半个小时里面重新计算所有人总优先级的时候, 就会忽略5号mm.Sleep函数就是干这事的,他告诉操作系统“在未来的多少毫秒内我不参与CPU竞争”.

两个问题

假设现在是 2008-4-7 12:00:00.000,如果我调用一下 Thread.Sleep(1000) ,在 2008-4-7 12:00:01.000 的时候,这个线程会不会被唤醒?

不一定.因为你只是告诉操作系统:在未来的1000毫秒内我不想再参与到 CPU竞争.那么1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束;况 且,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去. 与此相似的,Thread有个Resume函数,是用来唤醒挂起的线程的.好像上面所说的一样,这个函数只是“告诉操作系统我从现在起开始参与CPU竞争了”,这个函数的调用并不能马上使得这个线程获得CPU控制权

某人的代码中用了一句看似莫明其妙的话:Thread.Sleep(0) .既然是 Sleep 0 毫秒,那么他跟去掉这句代码相比,有啥区别么?

有,而且区别很明显.假设我们刚才的分蛋糕场景里面,有另外一个PPMM 7号,她的优先级也非常非常高(因为非常非常漂亮),所以操作系统总是会叫道她来吃蛋糕.而且,7号也非常喜欢吃蛋糕,而且饭量也很大.不过,7号人品很好,她很善良,她没吃几口就会想:如果现在有别人比我更需要吃蛋糕,那么我就让给他.因此,她可以每吃几口就跟操作系统说:我们来重新计算一下所有人的总 优先级吧.不过,操作系统不接受这个建议——因为操作系统不提供这个接口.于是7号mm就换了个说法:“在未来的0毫秒之内不要再叫我上来吃蛋糕了”.这个指令操作系统是接受的,于是此时操作系统就会重新计算大家的总优先级——注意这个时候是连7号一起计算的,因为“0毫秒已经过去了”嘛.因此如果没有比 7号更需要吃蛋糕的人出现,那么下一次7号还是会被叫上来吃蛋糕. 因此,Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”.竞争 的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权.这也是我们在大循环里面经常会写一句Thread.Sleep(0) ,因为这样就给了其他线程比如Paint线程获得CPU控制权的权力,这样界面就不会假死在那里. 末了说明一下,虽然上面提到说“除非它自己放弃使用 CPU ,否则将完全霸占 CPU”,但这个行为仍然是受到制约的——操作系统会监控你霸占CPU的情况,如果发现某个线程长时间霸占CPU,会强制使这个线程挂起,因此在实际上不 会出现“一个线程一直霸占着 CPU 不放”的情况.至于我们的大循环造成程序假死,并不是因为这个线程一直在霸占着CPU.实际上在这段时间操作系统已经进行过多次CPU竞争了,只不过其他 线程在获得CPU控制权之后很短时间内马上就退出了,于是就又轮到了这个线程继续执行循环,于是就又用了很久才被操作系统强制挂起...因此反应到界面 上,看起来就好像这个线程一直在霸占着CPU一样. 末了再说明一下,文中线程、进程有点混乱,其实在Windows原理层面,CPU竞争都是线程级的,本文中把这里的进程、线程看成同一个东西就好了

Windows系统

Windows系统使用的是抢占式

定义

所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU .因此可以看出,在抢 占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU . 在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一 个总的优先级来.操作系统就会把 CPU 交给总优先级最高的这个进程.当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他

说明

我们用分蛋糕的场景来描述这这种算法.假设有源源不断的蛋糕(源源不断的时间),一副刀叉(一个CPU),10个等待吃蛋糕的人(10 个进程)
Unix 操作系统来负责分蛋糕,那么他会这样定规矩:每个人上来吃 1 分钟,时间到了换下一个.最后一个人吃完了就再从头开始.于是,不管这10个人是不是优先级不同、饥饿程度不同、饭量不同,每个人上来的时候都可以吃 1 分钟.当然,如果有人本来不太饿,或者饭量小,吃了30秒钟之后就吃饱了,那么他可以跟操作系统说:我已经吃饱了(挂起).于是操作系统就会让下一个人接着来


线程的同步

[互斥对象][事件对象][临界资源/关键代码段][三种比较]

多线程访问共享变量的问题

问题:

在多线程访问共享变量的时候,有可能出现一种多个线程同时对同一个变量进行操作,导致变量数据无法正确使用.例如火车售票系统(源至VC++深入详解第十六章),有两个窗口可以进行售票,但是两个窗口如果没有进行适当的通信可能在销售最后一张票的时候发生错误.即一个更早的售出了最后一张票而稍微后的窗口售出了第0张票.下面用一个程序说明此问题.

代码示例:

程序源码:

#include 
#include 
#include 
using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets=10;

void main()
{
    HANDLE hThread1,hThread2;

    //创建子线程
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);

    Sleep(500);
    system("pause");
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    while(TRUE)
    {
        if(tickets>0)
        {
            Sleep(10);
            cout<<"窗口1 售出车票 : 第 "<0)
        {
            Sleep(10);
            cout<<"窗口1 售出车票 : 第 "<

运行结果:

线程(VC_Win32)_第3张图片

为了解决以上问题需要线程同步,即共享变量在同一时刻只有一个线程可以访问,具体实现有互斥对象,事件对象,临界区资源

互斥对象(mutex)

说明

  • 互斥对象(mutex)属于内核对象
  • 它能够包确保线程拥有对单个资源的互斥访问权限,

组成

  • 一个使用数量
  • 一个线程IP  ID用于标识系统中那个线程当前拥有互斥对象
  • 计数器   指明该线程拥有互斥对象的次数

注意

  • 多次在同一个线程中请求同一个互斥对象,那么就有需要相应地多次调用ReleaseMutex函数释放该互斥对象
  • 在程序运行时,操作系统维护了线程的信息以及该线程相关的互斥对象信息,因此它知道那个线程终止了,如果某个线程得到其所有虎刺对象的所有权,完成其线程代码的运行,但没有释放该互斥对象的所有权就退出之后,操作系统一旦发现该线程已经终止,它将自动把该线程拥有的所有互斥对象的线程ID设为0,并将其计数器归0,并且把互斥对象设置为有信号
  • 线程主动请求共享资源的使用所有权才有可能获得该所有权,调用WaitForSingleObject函数实现
  • 谁拥有互斥对象,谁才有权限释放互斥对象

执行流程

  • 解决共享变量访问问题

    线程(VC_Win32)_第4张图片

  • 保证运行实例单一性

    线程(VC_Win32)_第5张图片

函数详解(链接)

代码样例

解决共享变量访问问题(用互斥对象方法)

程序源码:

#include 
#include 
#include 
using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets=10;
HANDLE hMutex;

void main()
{
    HANDLE hThread1,hThread2;

    //创建互斥对象
    hMutex=CreateMutex(NULL,false,"tickets");

    //创建子线程
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);

    Sleep(500);
    system("pause");

    //关闭互斥对象
    CloseHandle(hMutex);
    
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    while(TRUE)
    {
        //等待互斥对象被释放后获得互斥对象拥有权
        WaitForSingleObject(hMutex,INFINITE);    
        
        //数据操作
        if(tickets>0)
        {
            Sleep(10);
            cout<<"窗口1 售出车票 : 第 "<0)
        {
            Sleep(10);
            cout<<"窗口1 售出车票 : 第 "<

运行结果:

线程(VC_Win32)_第6张图片

实现程序只有一个实例运行(用互斥对象方法)

程序源码:

#include 
#include 
#include 
using namespace std;

void main()
{
    HANDLE hMutex;

    //创建互斥对象
    hMutex=CreateMutex(NULL,false,"tickets");
    if(hMutex)
    {
        //判断命名互斥对象是否已存在
        if(ERROR_ALREADY_EXISTS==GetLastError())
        {
            cout<<"已经有一个运行实例了!"<

运行结果(上面为第一个运行实例的运行结果,下面为第二个实例运行结果):

线程(VC_Win32)_第7张图片

事件对象(Event)

说明

  • 事件对象(mutex)属于内核对象
  • 自动重置事件能够包确保线程拥有对单个资源的互斥访问权限.
  • 要实现线程同步时候人工重置事件是实现不了的,因为当人工事件对象得到通知时候,等待该事件对象的所有对象都变成可调用线程,并且一个事件得到该事件对象时候,该事件对象还是处于有信号状态的,必须手工调用ResetEvent才能取消事件的有信号状态,所有无法实现线程同步.

组成

  • 一个使用数量
  • 事件类型
    • 人工重置事件   当人工重置的事件对象受到通知时,等待事件对象的所有线程均变成可调用的线程(当现场等待到该对象的所有权之后,需要调用ResetEvent函数手动的将该事件对象设置为无信号状态)
    • 自动重置事件   当自动重置的事件对象受到通知时,等待事件对象的线程中只有一个线程变成可调用线程(当现场等待到该对象的所有权之后,系统会自动将该对象设置为无信号状态)
  • 状态值 用于指明事件是处于通知还是为通知状态的布尔值

执行流程

  • 解决共享变量问题(人工重置事件不适合在火车售票例子)

    线程(VC_Win32)_第8张图片

  • 保证运行实例的单一性

    线程(VC_Win32)_第9张图片

代码样例:

解决共享变量访问问题(用事件对象方法,人工重置事件不适合在个例子中使用,运行结果也会显示出卖出第0张票)

程序源码

#include 
#include 
#include 
using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets=10;
HANDLE g_hEvent;

void main()
{
    HANDLE hThread1,hThread2;

    //创建人工重置事件
    //g_hEvent=CreateEvent(NULL,true,false,NULL);

    //创建自动重置事件
    g_hEvent=CreateEvent(NULL,false,false,NULL);
    
    //创建完事件后为无信号,这里设置事件为有信号
    SetEvent(g_hEvent);

    //创建线程
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);

    Sleep(500);
    system("pause");
    CloseHandle(g_hEvent);
    
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    while(TRUE)
    {
        //人工重置事件:        等待该事件为有信号状态
        //自动重置事件:        等待该事件为有信号状态,并把事件设置为无信号状态
        WaitForSingleObject(g_hEvent,INFINITE);
        
        //手动设置信息为有无信号
        //ResetEvent(g_hEvent);     
        
        //数据操作
        if(tickets>0)
        {
            Sleep(10);
            cout<<"窗口1 售出车票 : 第 "<0)
        {
            Sleep(10);
            cout<<"窗口2 售出车票 : 第 "<

运行结果

线程(VC_Win32)_第10张图片

保证程序运行实例单一性(用事件对象方法)

程序源码:

#include 
#include 
#include 
using namespace std;

void main()
{
    HANDLE g_hEvent;

    //事件互斥对象
    g_hEvent=CreateEvent(NULL,false,false,"tickets");
    if(g_hEvent)
    {
        //判断命名事件对象是否已存在
        if(ERROR_ALREADY_EXISTS==GetLastError())
        {
            cout<<"已经有一个运行实例了!"<

运行结果(上面为第一个运行实例的运行结果,下面为第二个实例运行结果):

线程(VC_Win32)_第11张图片

关键代码段/临界资源

定义说明:

  • 关键代码段,也称为临界区,工作在用户方式下,它指向一个小段代码,在代码能够执行前,它必须独占对某些资源的访问权(在使用多个临界区的时候要注意防止死锁的发生)
  • 关键代码段非常类似于我们平常使用的公用电话亭,当我们想进入公用电话亭使用电话这种资源的时候,首先需要判断电话亭里时候有人,如果有人正在里面使用电话,那么我们只能在电话亭外等待,当那个人使用完电话,并离开电话亭后,我们才能进入电话亭使用电话这种资源,容易的我们使用完电话亭后,也要离开电话亭,但是有人在使用完电话始终不出来电话亭的时候,即使我们知道他已经没有使用电话这个资源我们也无法进入电话亭使用电话这个资源

执行流程

线程(VC_Win32)_第12张图片

注意死锁

用临界区资源使多线程同步时候要特别注意线程死锁问题,假设程序有两临界资源(g_csA,g_csB)与两个子线程(子线程A,子线程B),子线程执行体流程如下图(图1)表示,当子线程A先获得临界资源g_csA后由于子线程A的时间片用完了,所以跳到子线程B进行执行,这时B将获得临界资源g_csB,然后由于A获得临界资源g_csA,所以B只好等待直至子线程B时间片用完,然后跳到子线程A继续执行,但是这时的临界资源g_csB已经被子线程B占有,所以子线程A有进行等待直至时间片用完.于是子线程A与子线程B就进入了死锁现象流程如下图所示(图2).

线程(VC_Win32)_第13张图片

代码示例

程序源码:

#include 
#include 
#include 
using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets=10;

//声明关键代码量CRITICAL_SECTION结构体变量
CRITICAL_SECTION g_csA;


void main()
{
    HANDLE hThread1,hThread2;

    //初始化关键代码段
    InitializeCriticalSection(&g_csA);

    //线程的创建
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);    
    
    Sleep(500);
    system("pause");
    //释放关键代码段相关资源
    DeleteCriticalSection(&g_csA);
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    while(TRUE)
    {
        //等待关键代码不被占用时获得关键代码段所有权进入关键代码段
        EnterCriticalSection(&g_csA);

        //数据操作
        if(tickets>0)
        {
            Sleep(10);
            cout<<"窗口1 售出车票 : 第 "<0)
        {
            Sleep(10);
            cout<<"窗口2 售出车票 : 第 "<

运行结果:

线程(VC_Win32)_第14张图片

三种线程同步的比较

比较

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

说明

  • 编写多线程程序并需要实现线程同步时,首选关键代码段,由于它的使用比较简单,如果在MFC程序中使用的话,可以再类的构造函数中调用InitializeCriticalSection函数,在所需要保护的代码段前调用EnterCriticalSection函数,在访问完所需要的资源时候调用LeaveCriticalSection函数
  • 需要多个进程的各个线程实现同步的话,可以使用互斥对象或事件对象

 

你可能感兴趣的:(VC)