• 利用事件对象实现线程同步


    本文利用事件对象解决多线程计数问题和打印问题,问题描述见认识多线程-实例2

    事件对象介绍

    事件对象和互斥量属于内核对象,它包含三个成员:

    1. 使用计数;
    2. 用于指明该事件是一个自动重置事件还是一个人工重置事件的布尔值;
    3. 用于指明该事件处于已通知状态还是未通知状态的布尔值;

    事件对象API

    1. 创建事件对象 - CreateEvent

    通过CreateEvent函数创建或者打开一个命名的或者匿名的事件对象,其函数原型如下:

    HANDLE CreateEvent(  
      LPSECURITY_ATTRIBUTES lpEventAttributes, 
      BOOL bManualReset,   
      BOOL bInitialState,   
      LPTSTR lpName 
      ); 
    • lpEventAttributes
      一般使用默认的安全性,设置为NULL;

    • bManualReset
      BOOL类型, TRUE是人工重置事件, FALSE 自动重置事件;

    • bInitialState
      BOOL类型, TRUE 该事件的初始状态是有信号状态,FALSE 初始是无信号状态;

    2. 设置事件对象状态-SetEvent

    SetEvent函数将指定的事件对象设置为有信号状态,其函数声明如下:

    BOOL SetEvent(HANDLE hEvent);

    3. 重置事件对象状态-ResetEvent

    ResetEvent函数将指定的事件对象设置为无信号状态,其函数声明如下:

    BOOL ResetEvent(HANDLE hEvent);

    事件对象实现线程同步

    从CreateEvent函数中可以看出,事件对象有两种类型;其一是人工重置的事件对象,其二是自动重置的事件对象,其区别主要如下:

    当一个人工重置的事件对象处于有信号状态时,等待该事件对象的所有线程均变为可以调度状态;当一个线程得到该事件对象后,操作系统不会自动地将事件对象设置为无信号状态,需要显示地调用ResetEvent函数才会将其设置为无信号状态,否则状态不变;


    当一个自动重置的事件对象处于有 信号状态时,等待该事件对象的所有线程中只有一个线程处于可以调度线程;同时操作系统会将该事件对象自动设置为无信号状态;当执行完保护代码后,需要显示调用下SetEvent函数将该事件对象设置为有信号状态;

    为了实现线程间的同步,应该使用自动重置类型的事件对象,而不应该使用人工重置类型的事件对象,具体见如下分析;

    1. 人工重置事件对象

    主要原因是人工重置事件对象,可以使等待该事件对象的所有线程都变成可调度状态,导致两个线程可能同时执行或者调用WaitForSingleObject后不能及时设置事件对象为无信号状态,也就无法实现同步;

    问题代码:

    //创建人工重置事件对象,初始有信号
    g_hEvent = CreateEvent(NULL,TRUE,TRUE,NULL);
    
    DWORD WINAPI Thread1SynByEvent(LPVOID lpParameter)
    {
        while (TRUE)
        {
            //当该线程得到CPU时间片,并且事件处于通知状态,程序执行
            WaitForSingleObject(g_hEvent,INFINITE);
            ResetEvent(g_hEvent);
            ...
            g_nIndex++;
            ...
    
            //事件对象从无信号状态->有信号状态
            SetEvent(g_hEvent);
        }
    
        return 0;
    }
    
    DWORD WINAPI Thread2SynByEvent(LPVOID lpParameter)
    {
        while (TRUE)
        {
            //当该线程得到CPU时间片,并且事件处于通知状态,程序执行
            WaitForSingleObject(g_hEvent,INFINITE);
            ResetEvent(g_hEvent);
            ...
            g_nIndex++;
            ...
            //事件对象从无信号状态->有信号状态
            SetEvent(g_hEvent);
        }
    
        return 0;
    }

    分析:由于两个线程都是出于可调度状态,当线程1刚好执行完WaitForSingleObject函数后,CPU时间片就分配给了线程2,并且事件对象被设置为无信号状态;此时两个线程都可以访问被保护的对象g_nIndex,就无法线程同步,带来意想不到的结果;因为这里设置事件对象为无信号状态,需要显示调用ResetEvent;

    运行结果:

    打印出来的字符串顺序不正常,以及打印字符串有丢失和变多,如下图所示:

    这里写图片描述

    2. 自动重置事件对象

    自动重置对象就不存在这个问题,这是因为初始时只有线程1或者线程2处于可调度状态,并且只有事件对象处于有信号状态WaitForSingleObject函数才会返回,否则一直阻塞;并且WaitForSingleObject在返回之前还会自动将事件对象设置为无信号状态,不需要显示调用SetEvent函数;

    其完整代码如下:

    // MultiThread.cpp : 定义控制台应用程序的入口点。
    //
    
    #include "stdafx.h"
    #include <windows.h>
    #include <iostream>
    using namespace std;
    
    int g_nIndex = 0;
    const int nMaxCnt = 20;
    HANDLE g_hEvent = NULL;
    
    DWORD WINAPI Thread1SynByEvent(LPVOID lpParameter)
    {
        while (TRUE)
        {
            //等待线程的执行权,无限期等待
            //当该线程得到CPU时间片,并且事件处于通知状态,程序执行
            //该线程得到执行权后,系统自动将该事件对象置为无信号状态
            WaitForSingleObject(g_hEvent,INFINITE);
            if (g_nIndex++ < nMaxCnt)
            {
                cout << "Index = "<< g_nIndex << " ";
                cout << "Thread1 is runing" << endl;
            }
            else
            {
                break;
            } 
            //事件对象从无信号状态->有信号状态
            SetEvent(g_hEvent);
        }
    
        return 0;
    }
    
    DWORD WINAPI Thread2SynByEvent(LPVOID lpParameter)
    {
        while (TRUE)
        {
            //等待线程的执行权,无限期等待
            //当该线程得到CPU时间片,并且事件处于通知状态,程序执行
            //该线程得到执行权后,系统自动将该事件对象置为无信号状态
            WaitForSingleObject(g_hEvent,INFINITE);
            ResetEvent(g_hEvent);
            if (g_nIndex++ < nMaxCnt)
            {
                cout << "Index = "<< g_nIndex << " ";
                cout << "Thread2 is runing" << endl;
            }
            else
            {
                break;
            }
            //事件对象从无信号状态->有信号状态
            SetEvent(g_hEvent);
        }
    
        return 0;
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        HANDLE hThread1 = NULL;
        HANDLE hThread2 = NULL;
        //创建自动重置事件, 初始状态有信号
        g_hEvent = CreateEvent(NULL,TRUE,TRUE,NULL);
    
        //创建新的线程
        hThread1 = CreateThread(NULL,0,Thread1SynByEvent,NULL,0,NULL);//立即执行
        hThread2 = CreateThread(NULL,0,Thread2SynByEvent,NULL,0,NULL);//立即执行
    
        //无须对新线程设置优先级等操作,关闭之
        //良好的编码习惯
        CloseHandle(hThread1);
        CloseHandle(hThread2);
    
        Sleep(3000);
    
        CloseHandle(g_hEvent);
        return 0;
    }

    运行结果:

    这里写图片描述

    可以看出打印顺序和内容均没有异常;

  • 相关阅读:
    C语言调用汇编函数 实现超过32位数的加法
    【Qt学习笔记】13_富文本及打印页面
    Java初级回顾
    Java中FileInputStream和FileOutputStream类实现文件夹及文件的复制粘贴
    Java中File类如何扫描磁盘所有文件包括子目录及子目录文件
    学习笔记之循环链表
    练习 hdu 5523 Game
    学习笔记之集合ArrayList(1)和迭代器
    学习笔记之工厂方法模式
    学习笔记之基本数据类型-包装类-String之间的转换
  • 原文地址:https://www.cnblogs.com/jinxiang1224/p/8468303.html
Copyright © 2020-2023  润新知