最近更新于 2023-02-03 22:07

环境

Windows 11 专业工作站版 22H2
Visual Studio 2022 专业版

Unicode 字符集;
C17 标准,64 位编译

使用多线程

file

我写的示例代码中创建了三个子线程,每个线程运行时间 5 秒,每秒输出一次自己的序号(按创建顺序编的号),线程 ID 以及执行次号。
主线程每创建完一个子线程,对应子线程就已经开始运行,创建完 3 个子线程后,主线程挂起 3 秒,醒来后将线程 0 挂起,其它两个线程是继续运行的,主线程自己又挂起 3 秒,这次醒来后又将线程 0 唤醒。线程 0 醒来的时候已经是第 6 秒了,另外两个线程在第 5 秒的时候就结束了。线程 0 最开始执行了 3 秒,所以再执行 2 秒后,也结束运行了。
最后一行输出的 3 个数字是从主线程中获取到创建的子线程的 ID。

#include <Windows.h>
#include <stdio.h>

#define NUMBER_OF_THREADS   3 // 创建线程数

HANDLE ghStdOutput = NULL; // 存放控制台标准输出句柄

/**
 * @brief 线程函数
 * @param lpParam 创建线程函数的第四个参数会传到这里来
 * @return 
*/
DWORD WINAPI fnThread(LPVOID lpParam)
{
    WCHAR wcsOutput[128] = {0};

    for (int i = 0; i < 5; ++i)
    {
        memset(wcsOutput, 0, sizeof(wcsOutput));
        wsprintf(wcsOutput, L"线程序号 = %d,当前线程 ID = %d,当前执行次号 = %d\n", *(PINT)lpParam, GetCurrentThreadId(), i);
        WriteConsole(ghStdOutput, wcsOutput, wcslen(wcsOutput), NULL, NULL);

        Sleep(1000);
    }
    wsprintf(wcsOutput, L"线程 %d 结束\n", *(PINT)lpParam);
    WriteConsole(ghStdOutput, wcsOutput, wcslen(wcsOutput), NULL, NULL);

    return 0;
}

int main()
{
    HANDLE hStdError = GetStdHandle(STD_ERROR_HANDLE); // 获取控制台标准错误句柄
    ghStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    PINT pThreadNumber[NUMBER_OF_THREADS] = {NULL}; // 用于存放传入线程的数据
    HANDLE hNewThreadHandle[NUMBER_OF_THREADS] = {NULL}; // 用于存放创建的新线程的句柄
    DWORD dwNewThreadID[NUMBER_OF_THREADS] = {0}; // 用于存放创建的线程的 ID

    for (int i = 0; i < NUMBER_OF_THREADS; ++i)
    {
        // 从堆中分配内存
        pThreadNumber[i] = (PINT)HeapAlloc(GetProcessHeap(), // 从当前进程的堆中分配内存
                                           HEAP_ZERO_MEMORY, // 分配的内存全部初始化为零
                                           sizeof(PINT)); // 要分配的字节数
        if (pThreadNumber[i] == NULL)
        {
            WriteConsole(hStdError, L"pThreadNumber 分配内存失败!\n", wcslen(L"pThreadNumber 分配内存失败!\n"), NULL, NULL);
            ExitProcess(1);
        }

        *pThreadNumber[i] = i; // 存入线程序号

        hNewThreadHandle[i] = CreateThread(NULL, // 默认安全属性
                                        0, // 默认栈大小
                                        fnThread, // 线程函数名
                                        pThreadNumber[i], // 线程函数的参数
                                        0, // 使用默认创建标志
                                        &dwNewThreadID[i]); // 返回线程 ID
        if (!hNewThreadHandle[i])
        {
            WriteConsole(hStdError, L"hNewThreadHandle 创建线程失败!\n", wcslen(L"hNewThreadHandle 创建线程失败!\n"), NULL, NULL);
            ExitProcess(2);
        }
    }

    Sleep(3000); // 主线程挂起三秒后苏醒
    SuspendThread(hNewThreadHandle[0]); // 将第一个子线程挂起
    WriteConsole(ghStdOutput, L"线程 0 挂起\n", wcslen(L"线程 0 挂起\n"), NULL, NULL);
    Sleep(3000);
    ResumeThread(hNewThreadHandle[0]); // 唤醒第一个子线程
    WriteConsole(ghStdOutput, L"线程 0 苏醒\n", wcslen(L"线程 0 苏醒\n"), NULL, NULL);

    // 等待子线程结束,再解除主线程阻塞,保证主线程最后退出
    WaitForMultipleObjects(NUMBER_OF_THREADS, // 等待线程数量
                            hNewThreadHandle, // 等待的线程句柄
                            TRUE, // 等待前一个参数中所有句柄对应的线程发出信号时,本函数才解除阻塞
                            INFINITE); // 一直等待

    // 查看返回的线程 ID
    for (int i = 0; i < NUMBER_OF_THREADS; ++i)
    {
        wprintf(L"%d ", dwNewThreadID[i]);
    }
    wprintf(L"\n");

    // 善后处理
    for (int i = 0; i < NUMBER_OF_THREADS; ++i)
    {
        CloseHandle(hNewThreadHandle[i]); // 关闭线程句柄
        if (pThreadNumber[i])
        {
            HeapFree(GetProcessHeap(), 0, pThreadNumber[i]); // 释放申请的堆空间
            pThreadNumber[i] = NULL;
        }

        return 0;
    }
}

线程同步

原子锁

多个线程对同一个数据执行原子操作,会产生结果丢失,比如自增(先从内存将变量值读入寄存器,然后在寄存器中加一操作,最后将结果写回内存)
假如一个线程刚从内存读取了一个全局变量到寄存器中就失去了 CPU 执行权,另外一个线程对同一个变量操作,恰好此时获得 CPU 执行权,并做完了一次自增,然后又失去了执行权限,此时原先的线程又获得了 CPU 执行权,继续上次的工作,使用最初读取的值进行加一操作再写回内存。两个线程各执行一次,但是都是对最初值加一,而不是在其中一个线程自增的基础上进行,那么问题就产生了。
所有线程对同一个变量进行修改的地方加上原子锁后,当某个线程准备修改变量时,会先检查是否已经上锁,已经上锁就阻塞,没有上锁就给变量上锁,当这个线程对变量上锁后,其它线程试图修改时也会判断是否上锁,只要当前线程没完成修改操作就不会解锁,其它线程就会一直阻塞,直到完成修改解开锁,其它线程才能继续修改。
当然这样操作的话,一个线程在修改变量的时候,其它试图在此时修改同一变量的线程就会被阻塞在那里,实际就只有一个线程在运行。以降低效率为代价换取程序正确运行。

这里写了一个例子,两个子线程对同一个全局变量各自自增(自减) 100000 次
第一个数是直接自增 100000 次,第二个是使用了原子锁自增;第三个是自减 100000 次,第四个是使用了原子锁的自减。
file

不过使用原子锁比较麻烦,每个操作符都对应一个函数,不是经常用估计都记不住,要去现查文档。局限性,原子锁只能对变量进行操作符运算。

#include <Windows.h>
#include <stdio.h>

INT gIcre = 0;
INT gLockedIncre = 0;
INT gDecre = 200000;
INT gLockedDecre = 200000;

DWORD WINAPI fnThread(LPVOID lpParam)
{
    for (int i = 0; i < 100000; ++i)
    {
        ++gIcre;
        InterlockedIncrement(&gLockedIncre); // 原子锁递加
        --gDecre;
        InterlockedDecrement(&gLockedDecre); // 原子锁递减
    }

    return 0;
}

int main()
{
    HANDLE hStdError = GetStdHandle(STD_ERROR_HANDLE);
    HANDLE hNewThreadHandle[2] = {NULL};

    if (!(hNewThreadHandle[0] = CreateThread(NULL, 0, fnThread, NULL, 0, NULL)))
    {
        WriteConsole(hStdError, L"创建线程 0 失败\n", wcslen(L"创建线程 0 失败\n"), NULL, NULL);
        ExitProcess(1);
    }
    if (!(hNewThreadHandle[1] = CreateThread(NULL, 0, fnThread, NULL, 0, NULL)))
    {
        WriteConsole(hStdError, L"创建线程 1 失败\n", wcslen(L"创建线程 0 失败\n"), NULL, NULL);
        ExitProcess(1);
    }

    WaitForMultipleObjects(2, hNewThreadHandle, TRUE, INFINITE);

    wprintf(L"%d\n", gIcre);
    wprintf(L"%d\n", gLockedIncre);
    wprintf(L"%d\n", gDecre);
    wprintf(L"%d\n", gLockedDecre);

    CloseHandle(hNewThreadHandle[0]);
    CloseHandle(hNewThreadHandle[1]);
}

互斥锁

这里写了一个例子,两个线程,其中一个线程输出 *,每输出 20 个换一次行,换行 10 次,另外一个线程输出的符号是 –
但是运行结果并不是预想的

********************
--------------------
********************
。。。 这个样子

实际是
file

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI fnThread1(LPVOID lpParam)
{
    for (int i = 0; i < 10; ++i)
    {
        for (int j = 0; j < 20; ++j)
        {
            wprintf(L"-");
            Sleep(10);
        }
        wprintf(L"\n");
    }
    return 0;
}

DWORD WINAPI fnThread2(LPVOID lpParam)
{
    for (int i = 0; i < 10; ++i)
    {
        for (int j = 0; j < 20; ++j)
        {
            wprintf(L"*");
            Sleep(10);
        }
        wprintf(L"\n");
    }
    return 0;
}

int main()
{
    HANDLE hThreadHandle[2] = {NULL};

    hThreadHandle[0] = CreateThread(NULL, 0, fnThread1, NULL, 0, NULL);
    hThreadHandle[1] = CreateThread(NULL, 0, fnThread2, NULL, 0, NULL);

    WaitForMultipleObjects(2, hThreadHandle, TRUE, INFINITE);

    CloseHandle(hThreadHandle[0]);
    CloseHandle(hThreadHandle[1]);

    return 0;
}

这次的资源不再是变量,而是控制台,两个线程向同一个控制台写东西显示出来。在操作系统的调度下,系统中各个线程轮流使用 CPU,就导致某个线程的一段工作不能连续进行,两个线程都是工作一会停一会,最终显示结果就是 * 和 – 交叉在一起。

要想达到预想的效果,这里可以使用互斥锁,将需要连续向控制台输出的部分加锁,当锁被某个线程获取后,其它线程试图获取锁时发现无法获取就会阻塞在那里,等待已经获取锁的线程执行完并释放锁,其它线程才可继续参与获取锁。当然这样也会有性能损失,当几个线程都到了要使用某个资源的时候,只有获取到互斥锁的能工作,其它都会被阻塞在那里,一个一个轮流使用那个资源。

加上互斥锁后

#include <Windows.h>
#include <stdio.h>

HANDLE g_mutex; // 存储互斥锁句柄

DWORD WINAPI fnThread1(LPVOID lpParam)
{
    for (int i = 0; i < 10; ++i)
    {
        WaitForSingleObject(g_mutex, INFINITE); // 等待互斥锁,没等到就一直阻塞
        for (int j = 0; j < 20; ++j)
        {
            wprintf(L"-");
            Sleep(10);
        }
        wprintf(L"\n");
        ReleaseMutex(g_mutex); // 释放互斥锁
    }
    return 0;
}

DWORD WINAPI fnThread2(LPVOID lpParam)
{
    for (int i = 0; i < 10; ++i)
    {
        WaitForSingleObject(g_mutex, INFINITE);
        for (int j = 0; j < 20; ++j)
        {
            wprintf(L"*");
            Sleep(10);
        }
        wprintf(L"\n");
        ReleaseMutex(g_mutex);
    }
    return 0;
}

int main()
{
    HANDLE hThreadHandle[2] = {NULL};
    g_mutex = CreateMutex(NULL, FALSE, NULL); // 创建互斥锁
    HANDLE outputHandle = GetStdHandle(STD_OUTPUT_HANDLE);

    hThreadHandle[0] = CreateThread(NULL, 0, fnThread1, NULL, 0, NULL);
    hThreadHandle[1] = CreateThread(NULL, 0, fnThread2, NULL, 0, NULL);

    WaitForMultipleObjects(2, hThreadHandle, TRUE, INFINITE);

    CloseHandle(hThreadHandle[0]);
    CloseHandle(hThreadHandle[1]);
    CloseHandle(outputHandle);

    return 0;
}

file

事件

这里写了一个例子,一个线程用两个事件分别控制另外一个线程定时输出一句话和线程退出。
负责控制的线程,每秒设置一次事件,另外一个线程会一直等待这个事件,等不到就阻塞,等到了就返回继续往下执行。另外负责控制的线程在第 4 秒设置另外一个事件,在另外一个线程中也在等这个事件,但是只等一毫秒,超时就返回继续往下执行,然后返回值判断是否等到了事件,等到了就执行退出。

#include <Windows.h>

HANDLE g_timerEvent, g_quitEvent; // 存储事件的句柄

DWORD WINAPI threadFunction1(LPVOID lpParam)
{
    HANDLE outputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
    while (TRUE)
    {
        WaitForSingleObject(g_timerEvent, INFINITE); // 等待事件,没等到就一直阻塞,等到就返回
        if (WaitForSingleObject(g_quitEvent, 1) == WAIT_OBJECT_0) // 等待 1 ms,判断是否等到事件,等到就退出运行
        {
            WriteConsole(outputHandle, L"退出\n", wcslen(L"退出\n"), NULL, NULL);
            CloseHandle(outputHandle);
            break;
        }
        WriteConsole(outputHandle, L"定时事件\n", wcslen(L"定时事件\n"), NULL, NULL);
        ResetEvent(g_timerEvent); // 复位事件
    }
    return 0;
}

DWORD WINAPI threadFunction2(LPVOID lpParam)
{
    INT counter = 0;
    while (TRUE)
    {
        Sleep(1000);
        SetEvent(g_timerEvent); // 定时设置事件,另外一个线程根据事件控制运行或阻塞
        if (counter == 4)
        {
            SetEvent(g_quitEvent); // 第 4 秒的时候设置事件,控制另外一个线程退出
            break;
        }
        ++counter;
    }
    return 0;
}

int main()
{
    // 创建事件
    g_timerEvent = CreateEvent(NULL, // 安全属性
                                TRUE, // 手动复位
                                FALSE, // 初始状态无信号
                                NULL); // 事件命名
    g_quitEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

    HANDLE threadHandle[2] = {NULL};

    threadHandle[0] = CreateThread(NULL, 0, threadFunction1, NULL, 0, NULL);
    threadHandle[1] = CreateThread(NULL, 0, threadFunction2, NULL, 0, NULL);

    WaitForMultipleObjects(2, threadHandle, TRUE, INFINITE);

    CloseHandle(threadHandle[0]);
    CloseHandle(threadHandle[1]);
    CloseHandle(g_timerEvent);
    CloseHandle(g_quitEvent);

    return 0;
}

file

信号量

这里写了一个例子,先创建一个信号量,初始计数为 5,子线程每等待一次信号量,信号量中的计数就会减一,减为 0 后等待就会阻塞。子线程每次执行后挂起 100 毫秒,初始 5 次大概花 500+ 毫秒。在子线程开始执行后,主线程挂起 600 毫秒,然后给信号量计数再增加 3 次。子线程等待到新增的计数又可以等待过 3 次,之后又阻塞。最后因为我设置的等待时间是 2 秒,然后 for 循环剩下的两次每次都要等过两秒超时执行,最后结束运行。

file

#include <Windows.h>
#include <stdio.h>

HANDLE g_semaphoreHandle; // 存放信号量句柄

DWORD WINAPI threadFunction(LPVOID lpParam)
{
    for (int i = 0; i < 10; ++i)
    {
        WaitForSingleObject(g_semaphoreHandle, 2000); // 每等待一次,计数减一,为 0 阻塞。最多等待 2 秒,超时也会返回
        wprintf(L"%d\n", i);
        Sleep(100);
    }
    return 0;
}

int main()
{
     g_semaphoreHandle = CreateSemaphore(NULL, // 安全属性,默认
                                         5, // 信号量初始计数
                                         10, // 最大计数,第二个参数不能超过这个值
                                         NULL); // 为信号量取名
    HANDLE threadHandle = CreateThread(NULL, 0, threadFunction, NULL, 0, NULL);
    Sleep(600);
    ReleaseSemaphore(g_semaphoreHandle,
                     3, // 计数增加 3。增加后的总计数不能超过创建时设置的最大计数,否则无效
                     NULL); // 接收上一个计数,不需要设置 NULL
    WaitForSingleObject(threadHandle, INFINITE);
    CloseHandle(threadHandle);
    return 0;
}