6.【Windows API程序设计】线程的通信(解决调用线程时CPU开销高居不下的问题)

关于二进制安全技能树
【二进制安全学习技能树】
关于多线程
5.【Windows API程序设计】为什么使用多线程?
关于线程和进程函数
4.【Windows API程序设计】线程与进程的相关API函数

在学习了这么半个月二进制,感觉基础知识很枯燥但是很重要,虽然我们最后的目的不是写出一个操作系统或者一个编译器,我们可以很严格要求自己,总之,不想在某个关键时刻忘记某个参数或者某个函数。

1x0 线程的通信

三种方式:

  • 全局变量
  • 自定义消息
  • 事件对象

1x1 全局变量

在Counter()例子中就有用到全局变量g_nOption,在多线程的解释时,有提到这个通信方式,没有看过的可以看一下5.【Windows API程序设计】为什么使用多线程?

弊端:多个工作线程使用同一个全局变量时,由于每个工作线程都可以修改全局变量,可能会引起同步问题。在后文会探讨这个问题。

1x2 自定义消息

  1. 主线程向工作线程发送自定义消息

    如果主线程要向工作线程发送自定义消息,那么工作线程就要维护一个消息循环,如果工作线程创建了窗口,那么就要有一个窗口过程,但是这违背了多线程设计原则。即主线程负责用户界面,工作线程负责耗时的后台处理。如果工作线程开始处理了用户窗口,那么使用多线程就失去了原本设计的意义。

  2. 工作线程向主线程发送自定义消息

    工作线程向主线程发送自定义消息,比较简单,调用SendMessagePostMessage函数即可。

一个例子:

#include <windows.h>
#include "resource.h"

#pragma comment(linker,"\"/manifestdependency:type='win32' \
    name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
    processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

// 自定义消息,用于计数线程向显示线程发送消息报告工作进度(这两个都是工作线程)
#define WM_WORKPROGRESS (WM_APP + 1)
// 自定义消息,计数线程发送消息给主线程告知工作已完成
#define WM_CALCOVER     (WM_APP + 2)

// 全局变量
HWND g_hwndDlg;
BOOL g_bRuning;         // 计数线程没有消息循环,主线程通过把这个标志设为FALSE通知其终止线程

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 线程函数声明
DWORD WINAPI ThreadProcShow(LPVOID lpParameter);    // 把数值显示到编辑控件中
DWORD WINAPI ThreadProcCalc(LPVOID lpParameter);    // 模拟执行一项任务,定时把一个数加1

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
    return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    static HANDLE hThreadShow, hThreadCalc;
    static DWORD dwThreadIdShow;

    switch (uMsg)
    {
    case WM_INITDIALOG:
        g_hwndDlg = hwndDlg;
        // 禁用停止按钮
        EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), FALSE);
        return TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_BTN_START:
            g_bRuning = TRUE;
            // 创建显示线程和计数线程
            hThreadShow = CreateThread(NULL, 0, ThreadProcShow, NULL, 0, &dwThreadIdShow);
            hThreadCalc = CreateThread(NULL, 0, ThreadProcCalc, (LPVOID)dwThreadIdShow, 0, NULL);

            EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_START), FALSE);
            EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), TRUE);
            break;

        case IDC_BTN_STOP:
            // 通知计数线程退出
            g_bRuning = FALSE;
            // 通知显示线程退出
            PostThreadMessage(dwThreadIdShow, WM_QUIT, 0, 0);

            if (hThreadShow != NULL)
            {
                CloseHandle(hThreadShow);
                hThreadShow = NULL;
            }
            if (hThreadCalc != NULL)
            {
                CloseHandle(hThreadCalc);
                hThreadCalc = NULL;
            }
            EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_START), TRUE);
            EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), FALSE);
            break;

        case IDCANCEL:
            EndDialog(hwndDlg, 0);
            break;
        }
        return TRUE;

    case WM_CALCOVER:
        if (hThreadShow != NULL)
        {
            CloseHandle(hThreadShow);
            hThreadShow = NULL;
        }
        if (hThreadCalc != NULL)
        {
            CloseHandle(hThreadCalc);
            hThreadCalc = NULL;
        }
        EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_START), TRUE);
        EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), FALSE);

        MessageBox(hwndDlg, TEXT("计数线程工作已完成"), TEXT("提示"), MB_OK);
        return TRUE;
    }

    return FALSE;
}

DWORD WINAPI ThreadProcShow(LPVOID lpParameter)
{
    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        switch (msg.message)
        {
        case WM_WORKPROGRESS:
            SetDlgItemInt(g_hwndDlg, IDC_EDIT_COUNT, (UINT)msg.wParam, FALSE);
            break;
        }
    }

    return msg.wParam;
}

DWORD WINAPI ThreadProcCalc(LPVOID lpParameter)
{
    // lpParameter参数是传递过来的显示线程ID
    DWORD dwThreadIdShow = (DWORD)lpParameter;
    int nCount = 0;

    while (g_bRuning)
    {
        PostThreadMessage(dwThreadIdShow, WM_WORKPROGRESS, nCount++, NULL);
        Sleep(50);

        // nCount到达100,说明工作完成
        if (nCount > 100)
        {
            // 通知显示线程退出
            PostThreadMessage(dwThreadIdShow, WM_QUIT, 0, 0);

            // 发送消息给主线程告知工作已完成
            PostMessage(g_hwndDlg, WM_CALCOVER, 0, 0);

            // 本计数线程也退出
            g_bRuning = FALSE;
            break;
        }
    }

    return 0;
}

运行结果:
在这里插入图片描述
程序说明:

	CustomMSG程序有“开始”和“停止”两个按钮。
	用户按下“开始”按钮,创建显示线程和计数线程,计数线程模拟执行一项任务,每50ms计数加1。
	创建计数线程时需要将显示线程的ID作为线程函数参数,以便计数线程定时通过 PostThreadMessage 函数向显示线程发送自定义消息WM_WORKPROGRESS报告工作进度,显示线程获取到WM_WORKPROGRESS消息后将工作进度显示在程序的编辑控件中。
	如果计数线程的计数已经达到100,则说明工作已经完成,向显示线程发送WM_QUIT 消息通知其终止线程,向主线程发送自定义消息 WM_CALCOVER 告知工作已完成,主线程获取到WM_CALCOVER消息后会关闭两个线程句柄,启用/禁用相关按钮,然后显示一个消息框。
	在计数线程工作过程中,用户随时可以按下“停止”按钮,主线程将全局变量g_bRuning设置为FALSE告知计数线程终止线程,调用PostThreadMessage函数向显示线程发送WM_QUIT消息告知其终止线程,然后关闭两个线程句柄,启用/禁用相关按钮。

1x3 事件对象

先看一个例子,也就是Couter程序里的那个线程函数。
如下所示:

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
     int n = 0;
     while (!(g_nOption & F_STOP))
     {
          if (g_nOption & F_START)

               SetDlgItemInt(g_hwndDlg, IDC_EDIT_COUNT, n++, FALSE);
     }
     return 0;
}

当调用这个线程函数之后,会进入while循环,为了判断条件,需要时刻检测F_STOP标志的值,以确保程序及时终止,但是这样会使CPU开销越来越大,所以需要一个解决方案。

在学习事件对象之前,可以使用SuspendThread函数和ResumeThread函数。既然工作线程不知道停止以节省CPU开销,那么就让系统介入,让系统主动挂起工作线程。
优点:可以节省CPU开销。
缺点:主线程并不知道工作线程应该在哪里停止,如果此时工作线程在取堆栈的值,挂起线程之后,堆栈将被锁定,而其他需要调用堆栈数据的线程就在等待状态,从而导致死锁。

而事件对象可以解决以上问题。

事件对象创建函数:

HANDLE WINAPI CreateEvent(

     _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,// 指向事件对象安全属性结构的指针

     _In_     BOOL                    bManualReset,     // 手动重置还是自动重置,TRUE或FALSE

     _In_     BOOL                    bInitialState,    // 事件对象的初始状态,TRUE或FALSE

     _In_opt_ LPCTSTR               lpName);           // 事件对象的名称字符串,区分大小写

事件对象是内核对象,内核对象受系统管理,供同一个进程的任何线程进行使用。但是另一个进程的线程却不能使用,调用创建内核对象的函数后,会返回一个句柄值,这个句柄值与进程相关联。


  • 如何允许其他进程使用这个内核对象呢?
    使用CreateEvent函数或OpenEvent函数并指定"lpName"参数即可。

    例如:创建一个命名事件对象"MyEventObject",那么其他进程调用
    hEvent = CreateEvent(NULL, TRUE, FALSE, TEXT("MyEventObject"));

即可使用此命名事件对象,其获得的句柄值不一定与其他进程的相同,但是指向同一个对象。此时调用GetLastError会返回ERROR_ALREADY_EXEISTS。
如果其他内核对象已经被命名为"MyEventObject",调用GetLastError就会返回ERROR_INVALID_HANDLE,即无效的句柄值。


  • 事件对象的有无信号是什么意思?
    有信号:事件已经发生或条件满足,所有等待该事件的线程会被力立即唤醒。调用SetEvent(hEvent)函数将事件状态设置为有信号。
    无信号:事件还未发生或条件不满足,所有等待该事件的线程被阻塞(挂起)。调用ResetEvent(hEvent)函数将事件状态设置为无信号。

  • 事件对象的手动重置和自动重置?
    手动重置:有信号状态,唤醒所有等待线程,保持有信号状态,直到手动重置(需调用ResetEvent)。
    自动重置:有信号状态,唤醒一个等待线程,并自动重置为无信号。

  • 如何检查事件状态?
    调用WaitForSingleObject(hEvent,INFINITE),此函数只能检测一个事件对象,如果要同时检测多个对象,要使用WaitForSingleObjects,用法网上自查,这里不再赘述。
    若有信号,线程继续执行,若无信号,线程挂起等待。
    INFINITE(0xFFFFFFFF秒)指函数一直等待直到指定的对象变为有信号状态才返回,但是这个秒数可以自己把握。

在调用事件对象后,其引用计数会增加1,所以在使用完事件对象后,要调用CloseHandle函数来关闭句柄。

有了事件对象对线程的控制,那样就可以轻松解决CPU开销高居不下的问题了。

改良完的Counter.cpp

#include <windows.h>
#include "resource.h"

#pragma comment(linker,"\"/manifestdependency:type='win32' \
    name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
    processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

// 全局变量
HWND g_hwndDlg;
HANDLE g_hEventStart;       // 事件对象句柄,作为开始标志
HANDLE g_hEventStop;        // 事件对象句柄,作为停止标志

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
    return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    static HWND hwndBtnStart, hwndBtnStop, hwndBtnPause, hwndBtnContinue;
    HANDLE hThread = NULL;

    switch (uMsg)
    {
    case WM_INITDIALOG:
        g_hwndDlg = hwndDlg;
        hwndBtnStart = GetDlgItem(hwndDlg, IDC_BTN_START);
        hwndBtnStop = GetDlgItem(hwndDlg, IDC_BTN_STOP);
        hwndBtnPause = GetDlgItem(hwndDlg, IDC_BTN_PAUSE);
        hwndBtnContinue = GetDlgItem(hwndDlg, IDC_BTN_CONTINUE);

        // 禁用停止、暂停、继续按钮
        EnableWindow(hwndBtnStop, FALSE);
        EnableWindow(hwndBtnPause, FALSE);
        EnableWindow(hwndBtnContinue, FALSE);

        // 创建事件对象
        g_hEventStart = CreateEvent(NULL, TRUE, FALSE, NULL);
        g_hEventStop = CreateEvent(NULL, TRUE, FALSE, NULL);
        return TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_BTN_START:
            hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
            if (hThread != NULL)
            {
                CloseHandle(hThread);
                hThread = NULL;
            }

            SetEvent(g_hEventStart);    // 设置开始标志
            ResetEvent(g_hEventStop);   // 清除停止标志

            EnableWindow(hwndBtnStart, FALSE);
            EnableWindow(hwndBtnStop, TRUE);
            EnableWindow(hwndBtnPause, TRUE);
            break;

        case IDC_BTN_STOP:
            SetEvent(g_hEventStop);     // 设置停止标志
            EnableWindow(hwndBtnStart, TRUE);
            EnableWindow(hwndBtnStop, FALSE);
            EnableWindow(hwndBtnPause, FALSE);
            EnableWindow(hwndBtnContinue, FALSE);
            break;

        case IDC_BTN_PAUSE:
            ResetEvent(g_hEventStart);  // 清除开始标志
            EnableWindow(hwndBtnStart, FALSE);
            EnableWindow(hwndBtnStop, TRUE);
            EnableWindow(hwndBtnPause, FALSE);
            EnableWindow(hwndBtnContinue, TRUE);
            break;

        case IDC_BTN_CONTINUE:
            SetEvent(g_hEventStart);    // 设置开始标志
            EnableWindow(hwndBtnStart, FALSE);
            EnableWindow(hwndBtnStop, TRUE);
            EnableWindow(hwndBtnPause, TRUE);
            EnableWindow(hwndBtnContinue, FALSE);
            break;

        case IDCANCEL:
            // 关闭事件对象句柄
            CloseHandle(g_hEventStart);
            CloseHandle(g_hEventStop);
            EndDialog(hwndDlg, 0);
            break;
        }
        return TRUE;
    }

    return FALSE;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
    int n = 0;

    while (WaitForSingleObject(g_hEventStop, 0) != WAIT_OBJECT_0)       // 是否设置了停止标志
    {
        if (WaitForSingleObject(g_hEventStart, 100) == WAIT_OBJECT_0)   // 是否设置了开始标志
            SetDlgItemInt(g_hwndDlg, IDC_EDIT_COUNT, n++, FALSE);
    }

    return 0;
}

运行之后会发现当按下"开始"时,CPU占用率不断增加,当按下"暂停"时,CPU占用率直线下降。

1x4 总结

在线程之间的通信中,有三个主要的通信方法,分别是全局变量、自定义消息和事件对象。

  • 全局变量的优点是一次定义,全局使用,但缺点却很明显,所有线程均可修改,容易出现问题。

  • 自定义消息最常用于子线程向主线程通信,但是也有局限性。

  • 事件对象很好解决了前面两个的问题,只是频繁调用函数和内核对象,对于引用计数的部分要格外关注,通常CreateEvent函数和CloseHandle一起使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值