Windows API笔记(三)线程同步

Windows API笔记(一)内核对象
Windows API笔记(二)进程和进程间通信、进程边界
Windows API笔记(三)线程和线程同步、线程局部存储
Windows API笔记(三)线程同步
Windows API笔记(四)win32内存结构
Windows API笔记(五)虚拟内存
Windows API笔记(六)内存映射文件
Windows API笔记(七)堆
Windows API笔记(八)文件系统
Windows API笔记(九)窗口消息
Windows API笔记(十)动态链接库
Windows API笔记(十一)设备I/O



5中主要的同步对象:

  1. 临界区(critical section)
  2. 互斥量(mutex)
  3. 信号量(semaphore)
  4. 事件(event)
  5. 可等的计时器(waitable timer)

这5中同步对象,除临界区外都是内核对象。

1. 线程同步概述

一般来说,一个线程使自己与另一个线程同步的方法是让自己睡眠。但线程睡眠之前,需要告诉系统让它恢复执行必须要有什么“特殊事件”发生。
操作系统记住该线程的请求,监视着“特殊事件”是否发生以及何时发生。当它发生时,线程就能被再调度给CPU了。此时,线程就将它的执行与事件的发生取得了同步。
在本章对各种不同同步对象的讨论中,将要说明如何指定“特殊事件”,以及在通知系统为线程监视该事件后,如何使线程睡眠。

1.1 最坏的事情

使用多个线程通过不断查询由多个线程共享或都能访问的某个变量的状态来与另一线程中某项任务取得同步。

// 最不好的线程同步方式,轮询共享变量

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

int flag = 0;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    printf("enter child thread,waiting...\n");
    Sleep(5000);
    flag = 1;
    printf("exit child thread\n");
}

void CreateThreadFunc()
{
    DWORD mainThreadId = GetCurrentThreadId();
    DWORD tid;
    HANDLE thnd = CreateThread(NULL, 0, MyThreadFunc, &mainThreadId, 0, &tid);
    printf("create thread id : %d \n", tid);
    CloseHandle(thnd);
}

int main()
{
    CreateThreadFunc();

    int i = 0;
    while (TRUE)
    {
        printf("polling times: %d\n", i++);

        if (1 == flag) // 通过轮询共享变量的状态实现同步
        {
            printf("main thread sync... \n");
            break;
        }

        Sleep(1000);    // 若此处不调用Sleep会发生什么情况?
    }

    printf("exit");
}

以上是通过轮询共享变量的方式取得同步,这种方式有两个问题:

  1. 主线程通过轮询方式会占用CPU时间,从其他线程夺走了本来可以执行更有用的代码的时间
  2. 若主线程轮询时不睡眠,有可能因为主线程优先级高于子线程而导致子线程永远得不到CPU时间,从而不执行子线程,最终共享变量有可能永远不会被修改为指定值(flag 永远不为 1)

若一定要使用轮询方式同步,一定要注意:要通过使线程睡眠来取得同步!

1.2 临界区

临界区是一小段代码,它要求在执行以前取得对某些共享数据的独占的访问权。在所有的同步对象中,临界区是最容易使用的,但它们只能用于同步单个进程中的线程。临界区一次只允许一个线程取得对某个数据区的访问权。

未同步线程,将导致数据无序输出:

#include <Windows.h>

int flag = 0;
DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    for (int i = 0; i < 10; i++)
    {
        printf("%d\n", ++flag); // 单个线程运行时,会按由小到大的排序输出;多个线程同时运行时,将无法保证输出排序
    }
}

void CreateThreadFunc()
{
    DWORD mainThreadId = GetCurrentThreadId();
    DWORD tid;
    HANDLE thnd = CreateThread(NULL, 0, MyThreadFunc, &mainThreadId, 0, &tid);
    CloseHandle(thnd);
}

int main()
{
    CreateThreadFunc();
    CreateThreadFunc();
    CreateThreadFunc();

    system("pause");

    return 0;
}

2.1 使用临界区同步线程

临界区使用流程:

CRITICAL_SECTION g_critical_section;

// 初始化临界区对象
InitializeCriticalSection(&g_critical_section);

// 进入临界区(独占锁定临界区资源)
EnterCriticalSection(&g_critical_section);

// 离开临界区(解锁临界区资源)
LeaveCriticalSection(&g_critical_section);

// 释放临界区对象
DeleteCriticalSection(&g_critical_section);

临界区使用示例:

// 使用临界区同步线程
// 通过临界区同步线程保证程序的顺序执行

#include <Windows.h>

int flag = 0;

CRITICAL_SECTION g_critical_section;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    for (int i = 0; i < 10; i++)
    {
        // 进入临界区
        EnterCriticalSection(&g_critical_section);

        printf("%d\n", ++flag); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序

        // 离开临界区
        LeaveCriticalSection(&g_critical_section);
    }
}

void CreateThreadFunc()
{
    DWORD mainThreadId = GetCurrentThreadId();
    DWORD tid;
    HANDLE thnd = CreateThread(NULL, 0, MyThreadFunc, &mainThreadId, 0, &tid);
    // printf("create thread id : %d \n", tid);
    CloseHandle(thnd);
}

int main()
{
    // 初始化临界区
    InitializeCriticalSection(&g_critical_section);

    CreateThreadFunc();
    CreateThreadFunc();
    CreateThreadFunc();

    system("pause");

    // 释放临界区
    DeleteCriticalSection(&g_critical_section);

    return 0;
}

3. 使用内核对象同步线程

临界区非常适合用于序列化对一个进程中的数据的访问,因为它们的速度很快。然而,读者或许想要使一些应用程序与计算机中发生的其他特殊事件或其他进程中执行的操作取得同步。

下列内核对象能被用来同步线程:

  • 进程
  • 线程
  • 文件
  • 控制台输入
  • 文件变化通知
  • 互斥量
  • 信号量
  • 事件(自动重设和手工重设事件)
  • 可等的计时器(只用于Windows NT 4或更高)

每个对象在任何时候都可以处于两种状态之一:有信号(signaled)和无信号(nonsignaled)。

进程:进程在创建后为无信号,进程结束后变为有信号
线程:线程被创建和正在运行时为无信号,线程终止变为有信号
文件:可设置线程使其与某个异步文件I/O的操作的完成取得同步
控制台输入:无输入时无信号,有输入时有信号

线程主要使用两个函数将它们设为睡眠来等待内核对象变为有信号:

// 等待单个内核对象
DWORD
WaitForSingleObject(
    _In_ HANDLE hHandle,
    _In_ DWORD dwMilliseconds
    );

// 等待多个内核对象
DWORD
WaitForMultipleObjects(
    _In_ DWORD nCount,
    _In_reads_(nCount) CONST HANDLE* lpHandles,
    _In_ BOOL bWaitAll,
    _In_ DWORD dwMilliseconds
    );

WaitForSingleObject返回值说明:

返回值定义含义
WAIT_OBJECT_00x00000000对象达到有信号状态
WAIT_TIMEOUT0x00000102对象在dwMilliseconds毫秒内未达到有信号状态
WAIT_ABANDONED0x00000080对象是一个互斥量,由于被放弃了而达到有信号状态
WAIT_FAILED0xFFFFFFFF发生了错误,调用GetLastError可得到扩展错误信息

WaitForMultipleObjects

返回值定义含义
WAIT_OBJECT_0到WAIT_OBJECT_0 + nCount - 10x00000000开始等待所有对象时,该值表示等待成功完成;等待任一对象时,该值给出数组中变为有信号的句柄的索引
WAIT_TIMEOUT0x00000102对象在dwMilliseconds毫秒内未达到有信号状态
WAIT_ABANDONED_0到WAIT_ABANDONED_0 + nCount - 10x00000080开始对象是一个互斥量,由于被放弃了而达到有信号状态
WAIT_FAILED0xFFFFFFFF发生了错误,调用GetLastError可得到扩展错误信息

使用WaitForMultipleObjects等待子线程执行退出:

// 使用临界区同步线程
// 通过临界区同步线程保证程序的顺序执行

#include <Windows.h>

int flag = 0;

CRITICAL_SECTION g_critical_section;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    for (int i = 0; i < 10; i++)
    {
        // 进入临界区
        EnterCriticalSection(&g_critical_section);

        printf("%d\n", ++flag); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序

        Sleep(100);
        
        // 离开临界区
        LeaveCriticalSection(&g_critical_section);
    }
}

HANDLE CreateThreadFunc()
{
    DWORD mainThreadId = GetCurrentThreadId();
    DWORD tid;
    return CreateThread(NULL, 0, MyThreadFunc, &mainThreadId, 0, &tid);
    // printf("create thread id : %d \n", tid);
    // CloseHandle(thnd);
}

int main()
{
    HANDLE handles[3];

    // 初始化临界区
    InitializeCriticalSection(&g_critical_section);

    handles[0] = CreateThreadFunc();
    handles[1] = CreateThreadFunc();
    handles[2] = CreateThreadFunc();

    printf("wait 3 child thread signal...\n");
    // 等待多个内核对象变为有信号
    DWORD dw = WaitForMultipleObjects(3, handles, TRUE, INFINITE);
    printf("3 child thread has signal\n");

    // 释放临界区
    DeleteCriticalSection(&g_critical_section);
    CloseHandle(handles[0]);
    CloseHandle(handles[1]);
    CloseHandle(handles[2]);

    system("pause");
    return 0;
}

3.1 互斥量(mutex)

互斥量非常类似临界区,但它们能用来同步多个进程间的数据访问。

创建互斥量:

HANDLE
CreateMutex(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,   // 内核对象都有的安全属性
    _In_ BOOL bInitialOwner,    //
    _In_opt_ LPCWSTR lpName     // 内核对象名称,当用于跨进程同步时需要
    );

打开互斥量(一般用于跨进程同步):


HANDLE
OpenMutex(
    _In_ DWORD dwDesiredAccess,     // 访问方式
    _In_ BOOL bInheritHandle,       // 是否继承句柄
    _In_ LPCSTR lpName              // 互斥量内核对象名称,在创建时指定的
    );

互斥量使用方式:


HANDLE g_hMutex = NULL;

// 创建互斥量内核对象
g_hMutex = CreateMutex(NULL, FALSE, NULL);

// 等待互斥量内核对象变为有信号,内核对象转为无信号
WaitForSingleObject(g_hMutex, INFINITE);

// 释放互斥量,互斥量变为有信号
ReleaseMutex(g_hMutex);

// 关闭互斥量内核对象句柄
CloseHandle(g_hMutex);

使用互斥量修改临界区的测试代码:

// 互斥量同步线程

#include <Windows.h>

int flag = 0;
HANDLE g_hMutex = NULL;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    DWORD dw;
    for (int i = 0; i < 10; i++)
    {
        // 等待内核对象变为有信号
        dw = WaitForSingleObject(g_hMutex, INFINITE);

        if (dw == WAIT_OBJECT_0)
        {
            printf("%d\n", ++flag); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序

            // 释放互斥量,变为有信号
            ReleaseMutex(g_hMutex);
        }
        else
        {
            break;
        }
    }
}

void CreateThreadFunc()
{
    DWORD mainThreadId = GetCurrentThreadId();
    DWORD tid;
    HANDLE thnd = CreateThread(NULL, 0, MyThreadFunc, &mainThreadId, 0, &tid);
    // printf("create thread id : %d \n", tid);
    CloseHandle(thnd);
}

int main()
{
    // 初始化互斥量
    g_hMutex = CreateMutex(NULL, FALSE, NULL);

    CreateThreadFunc();
    CreateThreadFunc();
    CreateThreadFunc();

    system("pause");

    // 等待内核对象变为有信号
    WaitForSingleObject(g_hMutex, INFINITE);
    // 释放内核对象
    CloseHandle(g_hMutex);
    g_hMutex = NULL;

    return 0;
}

3.1.1 废弃的互斥量

互斥量对象与所有其他同步内核对象的不同之处在于它是被线程所拥有的。其他所有同步对象要么有信号,要么无信号,仅此而已。而互斥量对象除了有信号无信号之外,还要记住哪个线程拥有它们。如果一个线程在得到一个互斥量对象(将其置为无信号状态)后就终结了,互斥量就被废弃了。这种情况下,互斥量将永远保持无信号态,因为没有其他线程能通过调用ReleaseMutex来释放它。当系统发现发生这种情况时,就自动将互斥量设回有信号态。其通过调用WaitForSingleObject而等待该互斥量的线程就会被唤醒,而函数将返回WAIT_ABANDONED而不是WAIT_OBJECT_0。这样,线程就知道互斥量不是被正常释放的。

互斥量对象有关联的拥有计数。如果已经拥有某个互斥量的线程再次调用WaitForSingleObject,调用会立刻成功返回,因为系统知道该线程已经拥有那个互斥量了。而且,每次该互斥量的引用计数都会被增加。这意味着在互斥量变为有信号状态之前,必须调用同样多次的ReleaseMutex。

WaitForSingleObject和ReleaseMutex调用次数不一致会怎样:

// 互斥量同步线程

#include <Windows.h>

int flag = 0;
HANDLE g_hMutex = NULL;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    DWORD tid = GetCurrentThreadId();

    DWORD dw;
    for (int i = 0; i < 10; i++)
    {
        // 等待内核对象变为有信号
        dw = WaitForSingleObject(g_hMutex, 400);

        // 多次调用WaitForSingleObject会怎么样?WaitForSingleObject与ReleaseMutex调用次数必须一致
        dw = WaitForSingleObject(g_hMutex, 400);

        if (dw == WAIT_OBJECT_0)
        {
            printf("%d , tid = %ld\n", ++flag, tid); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序

            Sleep(100);

            // 释放互斥量,变为有信号
            ReleaseMutex(g_hMutex);

            // 调用多次,必须要释放多次
            // ReleaseMutex(g_hMutex);
        }
        else
        {
            printf("WaitForSingleObject 1, dw = %ld\n", dw);
            break;
        }
    }
}

HANDLE CreateThreadFunc()
{
    return CreateThread(NULL, 0, MyThreadFunc, NULL, 0, NULL);
}

int main()
{
    HANDLE handles[3];

    // 创建互斥量
    g_hMutex = CreateMutex(NULL, FALSE, NULL);

    handles[0] = CreateThreadFunc();
    handles[1] = CreateThreadFunc();
    handles[2] = CreateThreadFunc();

    printf("wait 3 child thread signal...\n");
    // 等待线程内核对象变为有信号
    WaitForMultipleObjects(3, handles, TRUE, INFINITE);
    printf("3 child thread has signal\n");

    // 释放内核对象
    CloseHandle(g_hMutex);
    g_hMutex = NULL;

    CloseHandle(handles[0]);
    CloseHandle(handles[1]);
    CloseHandle(handles[2]);

    system("pause");
    return 0;
}

3.3 信号量(Semaphore)

信号量是内核对象用于资源计数的。在创建信号量对象时会初始化一个资源个数(计数);当线程请求信号量对象资源时,计数减1,当计数为0时,线程请求信号量对象资源将等待直至信号量计数大于0;释放信号量对象时,信号量对象计数加1。

多个线程都可能影响信号量的资源计数,这是信号量与临界区和互斥量的不同,它不能被任务属于某一个线程。这意味着一个线程可以等待信号量对象而另一个线程释放该对象。

创建信号量对象:

HANDLE
CreateSemaphore(
    _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
    _In_     LONG lInitialCount,    // 初始化计数器的值
    _In_     LONG lMaximumCount,    // 计数器最大值
    _In_opt_ LPCSTR lpName          // 信号量名称,用于跨进程同步
    );

打开信号量对象:

HANDLE
OpenSemaphore(
    _In_ DWORD dwDesiredAccess,
    _In_ BOOL bInheritHandle,
    _In_ LPCSTR lpName              // 信号量名称,打开已存在的信号量
    );

释放信号量(减少资源计数):

BOOL
ReleaseSemaphore(
    _In_ HANDLE hSemaphore,
    _In_ LONG lReleaseCount,            // 释放计数个数,>= 1
    _Out_opt_ LPLONG lpPreviousCount    // 在增加计数前,设置计数器值;不感兴趣可设为NULL
    );

测试代码:

// 信号量

/*

启动5个线程,同时最多只能有3个进程可访问资源

在输出中可看到,前部分数据都是有3个线程输出,后部分数据都有另外2个线程输出
*/

#include <Windows.h>

#define HANDLE_SIZE 5

HANDLE g_hSemaphore = NULL;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    DWORD tid = GetCurrentThreadId();
    DWORD dw = WaitForSingleObject(g_hSemaphore, INFINITE);

    if (dw == WAIT_OBJECT_0)
    {
        for (int i = 0; i < 10; i++)
        {
            printf("My Thread Id = %ld , i = %d\n", tid, i); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序
            Sleep(100);
        }
    }

    ReleaseSemaphore(g_hSemaphore, 1, NULL);
}

HANDLE CreateThreadFunc()
{
    return CreateThread(NULL, 0, MyThreadFunc, NULL, 0, NULL);
}

int main()
{
    HANDLE handles[HANDLE_SIZE];

    g_hSemaphore = CreateSemaphore(NULL, 3, 3, NULL);

    for (int i = 0; i < HANDLE_SIZE; i++)
    {
        handles[i] = CreateThreadFunc();
    }

    printf("wait %d child thread signal...\n", HANDLE_SIZE);
    // 等待多个内核对象变为有信号
    DWORD dw = WaitForMultipleObjects(HANDLE_SIZE, handles, TRUE, INFINITE);
    printf("%d child thread has signal\n", HANDLE_SIZE);

    // 释放临界区
    for (int i = 0; i < HANDLE_SIZE; i++)
    {
        CloseHandle(handles[i]);
    }

    CloseHandle(g_hSemaphore);

    system("pause");
    return 0;
}

3.4 事件(Event)

事件对象是同步对象中最简单的形式,与互斥量和信号量大不相同。互斥量和信号量是用于控制对数据的访问的,而事件是用来发信号表示某一操作已经完成了。有两种事件对象:人工重设(manual_reset)事件和自动重设(auto_reset)事件。人工重设事件用于向几个线程同时发信号,表示某一操作已经完成;而自动重设事件用于向一个线程发信号,表示某一操作以及完成。
事件最常用于一个进程进行初始化工作后,发信号给另一个线程,让其完成剩余的工作。

创建事件:

HANDLE
CreateEvent(
    _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,   
    _In_ BOOL bManualReset,     // TRUE 人工手动重设(WaitFirSingleObject不改变信号状态);FLASE 自动重设
    _In_ BOOL bInitialState,    // 初始信号状态,TRUE有信号,FALSE无信号
    _In_opt_ LPCWSTR lpName     // 事件内核对象名称
    );

打开事件:

HANDLE
OpenEventA(
    _In_ DWORD dwDesiredAccess,
    _In_ BOOL bInheritHandle,
    _In_ LPCSTR lpName          // 已创建的事件内核对象名称
    );

设置事件为有信号:

BOOL
SetEvent(
    _In_ HANDLE hEvent
    );

重置事件为无信号(一般用于人工手动重设事件):

BOOL
ResetEvent(
    _In_ HANDLE hEvent
    );

脉冲式设置事件信号:

BOOL
PulseEvent(
    _In_ HANDLE hEvent
    );

PulseEvent的设置后的事件对象状态:无信号->有信号(WaitForSingleObject)->无信号
如果当前没有线程在等待该事件,调用将无意义。

示例代码:

// 事件

// 一个线程负责输出;另一个线程负责控制输出

#include <Windows.h>

HANDLE g_hEvent = NULL;

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    DWORD tid = GetCurrentThreadId();

    for (int i = 0; i < 10; i++)
    {
        WaitForSingleObject(g_hEvent, INFINITE);
        // 设置事件为无信号状态
        ResetEvent(g_hEvent);
        Sleep(100);
        printf("My Thread Id = %ld , i = %d\n", tid, i); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序
    }
}

DWORD WINAPI MyThreadFunc2(LPVOID lpParm)
{
    for (int i = 0; i < 10; i++)
    {
        // 设置事件为有信号状态
        SetEvent(g_hEvent);
        printf("SetEvent Times = %d\n", i); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序
        Sleep(500);
    }
}

HANDLE CreateThreadFunc()
{
    return CreateThread(NULL, 0, MyThreadFunc, NULL, 0, NULL);
}

HANDLE CreateThreadFunc2()
{
    return CreateThread(NULL, 0, MyThreadFunc2, NULL, 0, NULL);
}

int main()
{
    HANDLE handles[2];
    // 人工手动重设,初始为无信号状态
    g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

    handles[0] = CreateThreadFunc();
    handles[1] = CreateThreadFunc2();

    printf("wait 2 child thread signal...\n");
    // 等待多个内核对象变为有信号
    DWORD dw = WaitForMultipleObjects(2, handles, TRUE, INFINITE);
    printf("%d child thread has signal\n", 2);

    // 释放临界区
    for (int i = 0; i < 2; i++)
    {
        CloseHandle(handles[i]);
    }

    CloseHandle(g_hEvent);

    system("pause");
    return 0;
}

PulseEvent示例代码:

// 事件

// 一个线程负责输出;另一个线程负责控制输出

#include <Windows.h>

HANDLE g_hEvent = NULL;

DWORD WINAPI MyThreadFunc_PulseEvent(LPVOID lpParm)
{
    DWORD tid = GetCurrentThreadId();

    for (int i = 0; i < 10; i++)
    {
        WaitForSingleObject(g_hEvent, INFINITE);
        Sleep(100);
        printf("My Thread Id = %ld , i = %d\n", tid, i); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序
    }
}

DWORD WINAPI MyThreadFunc2_PulseEvent(LPVOID lpParm)
{
    for (int i = 0; i < 10; i++)
    {
        // 设置事件为有信号状态
        PulseEvent(g_hEvent);
        printf("PulseEvent Times = %d\n", i); // 单个线程运行时,会按由小到大的排序输出;多个程序运行时,将无法保证输出排序
        Sleep(500);
    }
}

HANDLE CreateThreadFunc()
{
    return CreateThread(NULL, 0, MyThreadFunc_PulseEvent, NULL, 0, NULL);
}

HANDLE CreateThreadFunc2()
{
    return CreateThread(NULL, 0, MyThreadFunc2_PulseEvent, NULL, 0, NULL);
}

int main()
{
    HANDLE handles[2];
    // 人工手动重设,初始为无信号状态
    g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

    handles[0] = CreateThreadFunc();
    handles[1] = CreateThreadFunc2();

    printf("wait 2 child thread signal...\n");
    // 等待多个内核对象变为有信号
    DWORD dw = WaitForMultipleObjects(2, handles, TRUE, INFINITE);
    printf("%d child thread has signal\n", 2);

    // 释放临界区
    for (int i = 0; i < 2; i++)
    {
        CloseHandle(handles[i]);
    }

    CloseHandle(g_hEvent);

    system("pause");
    return 0;
}

3.5 可等的计时器

在特定时间和/或固定的间隔使自己有信号的内核对象。

4. 线程挂起

WaitForSingleObject和WaitForMutipleObjects是线程最常用来挂起自己直到满足某种特定条件的函数。不过,线程还可以调用其他几个函数来自我挂起。

4.1 Sleep

VOID Sleep(DWORD dwMilliseconds);

Sleep使线程自动放弃了时间片的剩余部分。即使调用时传递0,也会使CPU停止执行当前线程而分配自己给下一个等待的线程。

4.2 异步文件I/O

异步文件I/O使得线程可以启动一个读写文件操作,而不必等待操作结束。文件对象是客同步的内核对象,着意味着可以将文件句柄传递给WaitForSingleObject。当系统执行异步文件I/O时,文件对象处于无信号状态;一旦文件操作完成,系统就会将文件对象的状态改为有信号,使得线程知道文件操作已经完成了。

4.3 WaitForInputIdle

线程可以调用WaitForInputIdle来自我挂起,该函数等待直到hProcess标识的进程的创建应用程序第一个窗口的线程中不再有输入为止。

4.4 MsgWaitForMultipleObjects

线程可以调用MsgWaitForMultipleObjects函数来使它等待自己的消息。

4.5 WaitForDebugEvent

基于Win32的操作系统提供了极好的调试支持。

4.6 SignalObjectAndWait

将一个内核对象设为有信号,并等待另一个内核对象变为有信号。

DWORD
SignalObjectAndWait(
    _In_ HANDLE hObjectToSignal,    // 设为有信号的内核对象
    _In_ HANDLE hObjectToWaitOn,    // 等待有信号的内核对象
    _In_ DWORD dwMilliseconds,      // 等待时间
    _In_ BOOL bAlertable            // 线程是否能够在等待的时候处理任意队列化的异步过程调用(可警觉的I/O和APC)
    );

每当调用的函数使线程从用户模式跳到内核模式代码时,大约需要600条CPU指令被执行(在x86平台上),所以诸如:

ReleaseMutex(hMutex);
WaitForSingleObject(hEvent,INFINITE);

的代码会执行1200条CPU指令。在高性能的服务器应用中,SingleObjectAndWait能节约很多处理时间。

4.7 Interlocked 函数族

保证长整数的原子性操作。

// Interlocked 函数族测试
#include <Windows.h>
#include <stdio.h>

#define HANDLE_SIZE 5

DWORD WINAPI MyThreadFunc(LPVOID lpParm)
{
    DWORD tid = GetCurrentThreadId();

    LONG *val = (LONG *)lpParm;
    for (int i = 0; i < 100; i++)
    {
        LONG ret = InterlockedIncrement(val);
        printf("val = %ld , thread = %ld \n", *val, tid);
    }
}

DWORD WINAPI MyThreadFunc_normal(LPVOID lpParm)
{
    DWORD tid = GetCurrentThreadId();

    LONG *val = (LONG *)lpParm;
    for (int i = 0; i < 100; i++)
    {
        (*val)++;
        printf("val = %ld , thread = %ld \n", *val, tid);
    }
}

HANDLE CreateThreadFunc(LONG *val)
{
    return CreateThread(NULL, 0, MyThreadFunc, val, 0, NULL);
}

int main()
{
    HANDLE handles[HANDLE_SIZE];
    LONG v1 = 0x000ffeee;

    for (int i = 0; i < HANDLE_SIZE; i++)
    {
        handles[i] = CreateThreadFunc(&v1);
    }

    printf("wait %d child thread signal...\n", HANDLE_SIZE);
    // 等待多个内核对象变为有信号
    DWORD dw = WaitForMultipleObjects(HANDLE_SIZE, handles, TRUE, INFINITE);
    printf("%d child thread has signal\n", HANDLE_SIZE);

    printf("v1 = %ld\n", v1);
    // 释放临界区
    for (int i = 0; i < HANDLE_SIZE; i++)
    {
        CloseHandle(handles[i]);
    }

    system("pause");
}

总结

线程同步是以线程睡眠,当特定事件发生时恢复执行实现的。最坏的事情就是线程睡眠后没有正确的恢复执行。
线程同步的实现方式及其特点:

方式是否内核对象实现方式特点
Sleep设置线程固定长度的睡眠时间,当唤醒时判断条件是否成立,以轮询指定条件方式实现同步实现简单,性能较差,睡眠时间过段且优先级较低可能导致永久睡眠
临界区(critical section)串行化访问数据区容易使用,速度快,适合控制数据访问,只能同步单个进程的线程,临界区一次只允许一个线程取得某个数据区的访问权
互斥量(mutex)协调共同对一个共享的资源的访问功能与临界区类似,但可跨进程;性能比临界区差
信号量(semaphore)控制有限个用户资源访问允许有限个线程取得某的数据区的访问权,可跨进程,互斥量可以被认为是1个用户资源的信号量
事件(event)线程睡眠,等待指定事件发生,当事件发生后恢复执行用来通知线程事件已发送,从而启动后继任务的开始
可等的计时器(waitable timer)在特定时间和/或固定的间隔使自己有信号
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值