Windows编程_Lesson007_内核对象之二

可等待计时器内核对象

我们在开发的时候经常会遇到这样的事情,在某一些时间点上或者按照某个频率启动一个线程来做一些事情。前面我们已知的事件内核对象,它可以帮助我们决定线程执行的顺序,但是它没有办法帮我们频繁的启动或者暂停一个线程。而可等待计时器内核对象就可以帮助我们很容易的完成这些事情!
我们使用CreateWaitableTimer函数可以创建一个可等待计时器内核对象,同样的,跟其它的一些函数一样,也有一个增强版的CreateWaitableTimerEx函数,下面分别是这两个函数的原型,从函数原型可以看出,增强版的函数比普通的函数多了一个访问权限的参数。对于我们目前来说,增强版的的函数一般用不着,除非我们能够明确的知道我们的程序不会在XP系统或者之前的系统上运行,那么可以使用增强版的,否则我们就要使用普通版的。下面我们重点解释一下普通版函数的参数。

HANDLE WINAPI CreateWaitableTimer(
  _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes,
  _In_     BOOL                  bManualReset,
  _In_opt_ LPCTSTR               lpTimerName
);

HANDLE WINAPI CreateWaitableTimerEx(
  _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes,
  _In_opt_ LPCTSTR               lpTimerName,
  _In_     DWORD                 dwFlags,
  _In_     DWORD                 dwDesiredAccess
);

lpTimerAttributes安全属性结构体指针,它决定了子进程是否能继承这个函数返回的句柄。如果lpTimerAttributes为NULL,计时器内核对象获取一个默认的安全属性并且返回的句柄不能被子进程继承。
bManualReset如果是TRUE,那么就是手动的通知计时器,否则将会是同步的计时器。
lpTimerName计时器内核对象名字,如果为NULL,那么就是一个匿名的计时器内核对象。

我们创建完成一个手动计时器内核对象,它默认是无信号的,所以我们还需要手动设置使它变成有信号状态,使用的函数是SetWaitableTimer,其函数原型如下所示:

BOOL WINAPI SetWaitableTimer(
  _In_           HANDLE           hTimer,
  _In_     const LARGE_INTEGER    *pDueTime,
  _In_           LONG             lPeriod,
  _In_opt_       PTIMERAPCROUTINE pfnCompletionRoutine,
  _In_opt_       LPVOID           lpArgToCompletionRoutine,
  _In_           BOOL             fResume
);

hTimer可等待计时器内核对象句柄;
pDueTime,它是以100纳秒为单位,这个参数有点儿小复杂,它涉及到一些时间格式的转换,我们就先不做详细解释了;
lPeriod,它是以毫秒为单位;
pfnCompletionRoutine,函数指针,这个函数将会在特定的时期被调用,
lpArgToCompletionRoutine,结构体指针,作为回调函数的参数;
fResume,当定时器状态被设置为有信号状态时,是否恢复恢复系统暂停权力保护模式。

下面我们下来看一个简单的例子:

#include <Windows.h>
#include <process.h>
#include <tchar.h>

HANDLE hTimer = INVALID_HANDLE_VALUE;
CRITICAL_SECTION cs;
unsigned int __stdcall ThreadFunc(void *lParam)
{
    while (WaitForSingleObject(hTimer, INFINITE) != WAIT_TIMEOUT)
    {
        EnterCriticalSection(&cs);
        _tprintf(TEXT("%d Thread in Timer...\r\n"), (int)lParam);
        LeaveCriticalSection(&cs);
    }

    return 0;
}

int main()
{
    InitializeCriticalSection(&cs);

    hTimer = CreateWaitableTimer(nullptr, TRUE, nullptr);
    HANDLE hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, reinterpret_cast<void *>(1), 0, nullptr);

    LARGE_INTEGER liDueTime;
    liDueTime.QuadPart = -1; 
    SetWaitableTimer(hTimer, &liDueTime, 1000, nullptr, nullptr, FALSE);

    WaitForSingleObject(hThread, INFINITE);
    DeleteCriticalSection(&cs);

    system("pause");
    return 0;
}

当我们运行这个程序的时候,我们会发现它会一直不停地打印,而不是跟我们想象的每隔一秒钟打印一次呢?这是为什么呢?
是因为我们的可等待计时器是一个手动设置的,一般的改进的办法是将它设置为自动的,也就是修改一个参数即可。将

hTimer = CreateWaitableTimer(nullptr, TRUE, nullptr);

修改为:

hTimer = CreateWaitableTimer(nullptr, FALSE, nullptr);

即可,这样就会一秒钟输出一次结果了。

上面的实验我们只是使用了一个线程,我们看不出太多的效果,下面我们要是用两个线程,看看结果是不是和我们想象的一样呢?

#include <Windows.h>
#include <process.h>
#include <tchar.h>

HANDLE hTimer = INVALID_HANDLE_VALUE;
CRITICAL_SECTION cs;
unsigned int __stdcall ThreadFunc(void *lParam)
{
    while (WaitForSingleObject(hTimer, INFINITE) != WAIT_TIMEOUT)
    {
        EnterCriticalSection(&cs);
        _tprintf(TEXT("%d Thread in Timer...\r\n"), (int)lParam);
        LeaveCriticalSection(&cs);
    }

    return 0;
}

int main()
{
    InitializeCriticalSection(&cs);

    hTimer = CreateWaitableTimer(nullptr, FALSE, nullptr);
    HANDLE hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, reinterpret_cast<void *>(1), 0, nullptr);
    HANDLE hThread2 = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, reinterpret_cast<void *>(2), 0, nullptr);

    LARGE_INTEGER liDueTime;
    liDueTime.QuadPart = -1; 
    SetWaitableTimer(hTimer, &liDueTime, 1000, nullptr, nullptr, FALSE);

    WaitForSingleObject(hThread, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);
    DeleteCriticalSection(&cs);

    system("pause");
    return 0;
}

执行结果如下:
这里写图片描述
这个结果是不是有点出乎医疗呢?按照前面学的多线程知识,线程1和线程2是抢占式执行的,无论是线程1先运行还是线程2先运行,都不应该交替执行这两个线程函数呀!!!我们应该觉得要不一直打印1,要么一直打印2。这是为什么呢?
因为在等待的时候,CPU并不存在抢占的行为,因为我们的线程已经进入了不可调度的状态。那么调度哪个线程,其实是由操作系统来决定的,操作系统中有一套算法,在我们将可等待计时器内核对象设置为自动后,操作系统中的一套算法就会尽可能的保证每一个WaitForSingleObject都能够得到信号,尽可能的保证公平。此时和之前的抢占CPU时间片变得不一样了。
所以使用WaitForSingleObject函数,操作系统就会为我们做一些事情,使得我们的每一个线程都能够被调用,这是WaitForSingleObject函数做的最好的东西。
并且,WaitForSingleObject如果碰到自动内核对象的话,它就会将这个自动内核对象从有信号变成无信号
对于事件内核对象既可以使用自动,也可以使用手动,但是我们更多是使用手动,因为这样可以使我们方便的控制线程的执行顺序,而使用可等待计时器内核对象的时候,很少使用手动,原因有2:
1. 调用非常的麻烦;
2. 在线程函数中,更多的是希望它能够自动的达到我们想要的结果。
3.
我们平时做的一些垃圾处理线程一般都是通过上面的方式做的,我们还可以将这样的线程函数的优先级做的更低一些,可以等到合适的时间再来执行。

任务使用事件内核对象,达到像上面计时器内核对象的效果!!!!!!!!!!

下面我们再来看以下SetWaitableTimer函数的回调函数的例子:

#include <Windows.h>
#include <process.h>
#include <tchar.h>

VOID CALLBACK TimerAPCProc(
    _In_opt_ LPVOID lpArgToCompletionRoutine,
    _In_     DWORD  dwTimerLowValue,
    _In_     DWORD  dwTimerHighValue
)
{
    _tprintf(TEXT("TimerAPCProc\r\n"));
}

int main()
{
    HANDLE hTimer = CreateWaitableTimer(nullptr, TRUE, nullptr);
    LARGE_INTEGER li = { 0 };
    SetWaitableTimer(hTimer, &li, 1000, TimerAPCProc, nullptr, FALSE);
    // 会在本线程中将回调函数要入到调用函数的APC(异步调用过程)中,当线程空闲下来之后,
    // 会去清理APC里面的一些东西,在之前的IO操作的学习中提到过APC
    // SetWaitableTimer具有将一个函数压入到APC栈中的功能。
    // 当然了,在这种情况下我们所使用的将不再是时间间隔这个特性,而是使用了压入APC栈中的函数的特性,
    // 这种特性我们平时的并不是很多,稍作一下了解即可,
    SleepEx(INFINITE, TRUE); // SleepEx函数才能调用TimerAPCProc函数。

    system("pause");
    return 0;
}

这里写图片描述

信号量

我们可能会有这样的一个需求,要求一个线程能启动多少次,信号量就能做到这一点,CreateSemaphore是创建信号的函数,其原型如下:

HANDLE WINAPI CreateSemaphore(
  _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  _In_     LONG                  lInitialCount,
  _In_     LONG                  lMaximumCount,
  _In_opt_ LPCTSTR               lpName
);

lpSemaphoreAttributes安全属性结构体指针;
lInitialCount初始化信号量内核对象个数,这个值必须大于等于0,小于等于lMaximumCount。
lMaximumCount信号量内核对象的的最大个数,这个值必须要大于0;
lpName信号量内核对象的名字,如果为NULL,那么将是一个匿名信号量。

我们先来看一个小示例:

#include <Windows.h>
#include <process.h>
#include <tchar.h>

int main()
{
    HANDLE hSemaphore = CreateSemaphore(nullptr, 0, 5, TEXT("Demo"));

    // Semaphore中几点:
        // 1.计数,就是CreateSemaphore函数中的第二个参数count,它能够决定WaitForSingleObject多少次
        // 每一次的WaitForSingleObject都会使count递减,当count为0时,那么就会一直WaitForSingleObject下去

    // 我们可以使用下面的方法在其他线程或者进程中打开上面创建的名字为Demo的信号量
    //HANDLE hSemaphoreOther = OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, TEXT("Demo"));

    _tprintf(TEXT("Enter WaitForSingleObject 1\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 2\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 3\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 4\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 5\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);

    return 0;
}

这里写图片描述
从上面结果中可以看出,在进入第四次WaitForSingleObject的时候被阻塞住了。

我们还可以使用ReleaseSemaphore 函数来累加信号量内核对象的个数,函数原型如下:

BOOL WINAPI ReleaseSemaphore(
  _In_      HANDLE hSemaphore,
  _In_      LONG   lReleaseCount,
  _Out_opt_ LPLONG lpPreviousCount
);

hSemaphore信号量对象的句柄;
lReleaseCount当前信号量个数的基础上要增加的信号量的个数,这个值必须要大于0,如果累加的信号量个数大于最大值,那么函数执行失败,返回FALSE;
lpPreviousCount如果需要获取先前的信号量个数,就可以传递一个变量的地址,如果不需要,则可以传递NULL。

#include <Windows.h>
#include <process.h>
#include <tchar.h>

int main()
{
    HANDLE hSemaphore = CreateSemaphore(nullptr, 1, 5, TEXT("Demo"));

    // Semaphore中几点:
        // 1.计数,就是CreateSemaphore函数中的第二个参数count,它能够决定WaitForSingleObject多少次
        // 每一次的WaitForSingleObject都会使count递减,当count为0时,那么就会一直WaitForSingleObject下去

    // 我们可以使用下面的方法在其他线程或者进程中打开上面创建的名字为Demo的信号量
    //HANDLE hSemaphoreOther = OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, TEXT("Demo"));

    _tprintf(TEXT("Enter WaitForSingleObject 1\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    ReleaseSemaphore(hSemaphore, 2, nullptr);   // 我们在这里累加两个信号
    _tprintf(TEXT("Enter WaitForSingleObject 2\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 3\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 4\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Enter WaitForSingleObject 5\r\n"));
    WaitForSingleObject(hSemaphore, INFINITE);

    return 0;
}

这里写图片描述
从结果可以看出,和上次的执行结果一样,说明ReleaseSemaphore函数执行成功。

互斥体内核对象

这是Windows中比较特殊但也比较常用的内核对象,互斥体的作用比较多,这是因为它的结构比较特殊,在每一个互斥体中,会保存一个线程ID,它会决定线程是否处于有信号状态。当线程ID等于0的时候,当前的线程是处于有信号的状态,可以被激活,当前的线程被激活之后,会将激活它的线程ID传递到内核对象中,会使得内核对象变为无信号的状态。这种原理使得它跟我们之前学过的关键段有异曲同工之处,可以做线程同步的事情。当然了互斥体还可以做其它的事情。
CreateMutex函数可以创建一个互斥体内核对象,原型如下:

HANDLE WINAPI CreateMutex(
  _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
  _In_     BOOL                  bInitialOwner,
  _In_opt_ LPCTSTR               lpName
);

lpMutexAttributes安全属性结构体指针;
bInitialOwner在初始化的时候,是否拥有这个互斥体内核对象。如果是TRUE,那么将会是一个无信号状态,否则,将是一个有信号的状态。
lpName互斥体内核对象的名字。

我们来看一个小例子:
CreateMutex第二个参数传递的是TRUE,它的线程ID非0,因为会将主线程的线程ID传递进去,当线程ID非0的时候,那么这个互斥体对象是一个无信号的状态,也就是说在调用WaitForSingleObject函数的时候会被阻塞住。
但是实际情况并不是这样,调用WaitForSingleObject函数之后,并没有被阻塞住。那么这是为什么呢?因为互斥体内核对象比较特殊,Mutex实际上是和线程绑定的,对于本线程来说,它已经拥有了Mutex里面的权限,所以对于本线程来说,无论调用多少次WaitForSingleObject函数,都不会产生阻塞,都是处于有信号状态。但是对于其它的线程来说,它将是无信号状态的。

#include <Windows.h>
#include <process.h>
#include <tchar.h>

HANDLE g_hMutex;
unsigned int __stdcall ThreadFunc(void *lParam)
{
    WaitForSingleObject(g_hMutex, INFINITE);

    return 0;
}

int main()
{
    g_hMutex = CreateMutex(nullptr, TRUE, nullptr);
    HANDLE hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, nullptr, 0, nullptr);
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    system("pause");
    return 0;
}

这里写图片描述
从运行结果我们可以看出,它被阻塞在ThreadFunc线程函数的WaitForSingleObject函数里面了,说明g_hMutex 在ThreadFunc线程函数中是无信号的。

我们再来确认一下在主线程中会不会阻塞,测试代码如下:

#include <Windows.h>
#include <process.h>
#include <tchar.h>

HANDLE g_hMutex;
unsigned int __stdcall ThreadFunc(void *lParam)
{
    _tprintf(TEXT("In ThreadFunc\r\n"));
    WaitForSingleObject(g_hMutex, INFINITE);
    _tprintf(TEXT("Out ThreadFunc\r\n"));

    return 0;
}

int main()
{
    g_hMutex = CreateMutex(nullptr, TRUE, nullptr);

    _tprintf(TEXT("main 1\r\n"));
    WaitForSingleObject(g_hMutex, INFINITE);
    _tprintf(TEXT("main 2\r\n"));
    WaitForSingleObject(g_hMutex, INFINITE);
    _tprintf(TEXT("main 3\r\n"));
    WaitForSingleObject(g_hMutex, INFINITE);
    _tprintf(TEXT("main 4\r\n"));
    WaitForSingleObject(g_hMutex, INFINITE);
    HANDLE hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, nullptr, 0, nullptr);
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    system("pause");
    return 0;
}

这里写图片描述
从结果可以看出,在主线程中,无论等待多少次,都也不会产生阻塞,最终还是在线程函数中发送阻塞。

我们通过在主线程函数中调用

ReleaseMutex(g_hMutex);

此时线程函数就不会阻塞了,结果如下所示:
这里写图片描述

但是我们的一般做法是在创建Mutext的时候,CreateMutex函数中第二个参数传递的是FALSE,我们不需要占用Mutex,结果如下:
这里写图片描述
我们对Mutex稍微做一下总结:
Mutex是一个较为特殊的内核对象,叫做互斥体,里面有一个成员变量来记录线程ID,包括互斥体内核对象的信号的成员变量也会随着线程ID发生改变。如果当前的Mutex没有任何一个线程拥有它,即它的线程ID为0,Mutex变成了有信号的状态。当有任何一个线程ID的时候,它就变成了无信号状态。我们使用WaitForSingleObject等待一个Mutex之后,它会填充线程ID,并且使线程变为无信号状态。我们必须通过ReleaseMutex函数它进行释放,才能够被其它的对象使用。

下面我们就来通过实例来验证上面的结论!!!
这里写图片描述

上面的结果正好验证了使用WaitForSingleObject等待一个Mutex之后,它会填充线程ID,并且使线程变为无信号状态的结论。
如果我们想让ThreadFunc线程函数中的g_hMutex也有信号,那么我们就应该在主线程函数处理完逻辑之后,使用ReleaseMutex(g_hMutex)函数将互斥体变为有信号。
结果如下:
这里写图片描述

虽然上面解决了一些问题,但是新的问题又来了,当一个线程函数拥有了Mutex的信号之后,它可以多次的调用WaitForSingleObject函数并且也不会产生阻塞。但是如果我们只ReleaseMutex一次,那么TreadFunc函数还是会被阻塞的,示例如下:
这里写图片描述
这是为什么呢?原因很简单!!!
因为Mutex除了我们所说的线程ID和信号这两个成员变量以外,它还有另一个类似于使用计数的变量,我们可以叫做等待计数,它会随着每一次的WaitForSingleObject进行递增,每一次ReleaseMutex会是得它递减。所以我们就知道为什么上面的例子中的线程函数中的Mutex没有信号了。

所以信号量是由:
1.线程ID;
2.信号;
3.等待计数。
三者构成。

所以上面的方法会产生一定的危险性,因为假如在某一个线程函数中的WaitForSingleObject次数大于ReleaseMutex次数,它将会使得其它线程得不到信号,从而被阻塞住。

还有一点需要注意的是,ReleaseMutex只是针对本线程的等待计数有用,对于其它线程的等待计数是不能减小的,我们通过下面的例子来进行说明。
这里写图片描述

上面的例子说明了,在主线程中等待WaitForSingleObject了两次,释放ReleaseMutex了一次,还差一次,那么我在ThreadFunc函数中是进行释放ReleaseMutex三次,还是不能将它设置为有信号,最终还是被阻塞住了,正好验证了我们上面的结论!!!

还有一个知识点,如果某一个线程函数调用了多次WaitForSingleObject,而ReleaseMutex的次数少于WaitForSingleObject的次数,那么当这个线程在消亡的时候,Mutex会自动的将这个线程ID设置为0,并清除它所对应的等待计数,是Mutex变为有信号状态,这就是Mutex的遗弃机制。

我们还可以通过Mutex中的互斥体和线程ID之间的绑定这一特点,来解决游戏中多开问题,用A进程创建一个互斥体并拥有它(CreateMutex(nullptr, TRUE, nullptr)),其它进程打开它将不能拥有这个互斥体的权限,所以能够防止多开。

使用Mutex内核对象防止程序多次打开

我们一直在强调一件事情,内核对象是由内核所拥有的,内核是属于操作系统的。为什么我们要这样一直强调呢? 这是因为内核是可以进行跨进程通信的。我们现在使用Mutex内核对象防止进程被多次打开。当然,我们也可以使用信号量来限制我们的进程做多能够被打开几次。

我们两个进程的代码都是如下所示:

#include <Windows.h>
#include <process.h>
#include <tchar.h>

int main()
{
    // 安全属性在普通的开发时关注的不太多,直接传递NULL即可
    HANDLE hMutex = CreateMutex(nullptr, TRUE, TEXT("Demo"));   // 第二次被创建的时候,会被当成打开的操作

    if (ERROR_ALREADY_EXISTS == GetLastError())
    {
        _tprintf(TEXT("ERROR_ALREADY_EXISTS\r\n"));
    }
    else
    {
        _tprintf(TEXT("first open! success\r\n"));
    }

    WaitForSingleObject(hMutex, INFINITE);

    while (true)
    {
        Sleep(1000);
    }
}

这里写图片描述

从结果来看,确实做到了防止程序多开。

下面我们还要看一个限制同一个程序打开次数的例子,它是通过信号量Semaphore来完成的,就不多解释了,直接上源码和结果。

#include <Windows.h>
#include <process.h>
#include <tchar.h>

int main()
{
    HANDLE hSemaphore = CreateSemaphore(nullptr, 3, 10, TEXT("Demo"));

    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Successful execution!\r\n"));

    while (true)
    {
        Sleep(1000);
    }
    CloseHandle(hSemaphore);

    return 0;
}

这里写图片描述

我们可以看到,程序在执行到第四次的时候被阻塞住了,这正是我们想要的最多打开三次的效果。

下面的例子只能被打开三次,第四次就会直接退出

#include <Windows.h>
#include <process.h>
#include <tchar.h>

int main()
{
    HANDLE hSemaphore = CreateSemaphore(nullptr, 3, 10, TEXT("Demo"));

    long lPreCount;
    ReleaseSemaphore(hSemaphore, 1, &lPreCount);
    if (lPreCount == 0)
    {
        _tprintf(TEXT("Falid execution!\r\n"));
        WaitForSingleObject(hSemaphore, INFINITE);
        return 0;
    }

    WaitForSingleObject(hSemaphore, INFINITE);

    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf(TEXT("Successful execution!\r\n"));

    while (true)
    {
        Sleep(1000);
    }
    CloseHandle(hSemaphore);

    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值