1 线程同步是一个什么样的问题???
    如果你编写的是多线程程序,那么多个线程是并发执行,可以认为他们是同时在执行代码。但是线程和线程之间并非是完全的没有关系。很多时候会有以下两种关系:
    第一种情况,线程A的继续执行,要以线程B完成了某一个操作之后为前提。 这种需求称之为同步
    第二种情况,多个线程在争抢一个资源,比如:全局变量,可以是文件,可以是一个数据结构,可以是一个对象。 这种需求称之为同步互斥。
    2 以下的这些机制怎么解决的这个问题??各自又什么区别,分别用于什么样的场景呢???
    a. 以下这些机制都能够比较方便的解决互斥问题。
    原子操作:原子操作适合去解决共享资源是全局变量的互斥问题。
    作用就是对于一个变量的基本算术运算保证是原子性的。

    1. #include <stdio.h>
    2. #include <windows.h>
    3. // 原子操作:
    4. // 特点: 将可能存在的多条汇编指令转换成一条不可拆分的指令
    5. // 缺点: 只能对整数类型进行基本的算数运算原子化,不能保护复杂的代码块
    6. // 函数: InterLockedXXX 系列函数
    7. // 全局变量 g_n,作为一个资源,它被多个线程访问,此时即有可能出现问题
    8. LONG g_n;
    9. DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
    10. {
    11. for (int i = 0; i < 100000; i++)
    12. {
    13. // 使用原子操作替代自增操作后,三条汇编指令被替换为了下面的
    14. // lock inc dword ptr [g_n (0ADA138h)]
    15. // lock 被用于锁死地址总线,此时 CPU 不允许其它核心访问目标内存
    16. InterlockedIncrement(&g_n);
    17. }
    18. return 0;
    19. }
    20. DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
    21. {
    22. for (int i = 0; i < 100000; i++)
    23. {
    24. InterlockedIncrement(&g_n);
    25. }
    26. return 0;
    27. }
    28. int main()
    29. {
    30. HANDLE hThread1 = 0, hThread2;
    31. hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    32. hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    33. WaitForSingleObject(hThread1, -1);
    34. WaitForSingleObject(hThread2, -1);
    35. printf("%d", g_n);
    36. return 0;
    37. }

    临界区解决互斥问题:
    被保护的代码(代码访问了共享资源)放置在
    EnterCriticalSection
    LeaveCriticalSection之间即可
    临界区具有线程所有权这个概念,必须进入临界区的线程,调用离开临界区,临界区才会被打开。假如加锁的线程崩溃了,其他线程就锁死了。

    #include <stdio.h>
    #include <windows.h>
    
    // 临界区(关键段):
    //  特点: 临界区是一个结构体,通过设置结构体的字段,可以保护连续的一片代码,有【线程拥有
    //      者】的概念,线程的拥有者可以重复的进入到临界区中。
    //  缺点: 如果拥有结构体的线程崩溃了,其它线程会一直等待,从而产生死锁
    //  函数: 初始化临界区 InitializeCriticalSection
    //       开始保护 EnterCriticalSection
    //       结束保护 LeaveCriticalSection
    
    
    // 创建一个临界区全局变量,用于进行原子操作
    CRITICAL_SECTION CriticalSection{};
    
    
    // 全局变量 g_n,作为一个资源,它被多个线程访问,此时即有可能出现问题
    LONG g_n;
    
    DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            // 结构体中的 RecursionCount 表示当前需要解锁的次数,进入了多少次
            //  临界区,相应的就需要离开多少次。OwningThread 字段表示当前占有
            //  临界区的线程
            EnterCriticalSection(&CriticalSection);
            g_n++;
            LeaveCriticalSection(&CriticalSection);
        }
        return 0;
    }
    
    DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            EnterCriticalSection(&CriticalSection);
            g_n++;
            LeaveCriticalSection(&CriticalSection);
        }
    
        return 0;
    }
    int main()
    {
        // 初始化临界区变量
        InitializeCriticalSection(&CriticalSection);
    
        HANDLE hThread1 = 0, hThread2;
    
        hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
        hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    
        WaitForSingleObject(hThread1, -1);
        WaitForSingleObject(hThread2, -1);
    
        printf("%d", g_n);
        return 0;
    }
    

    介绍另外3种机制前,先说两个重要概念
    1:
    激发态(有信号) 非激发态(没有信号)
    WaitForSignaleObject(内核对象,时间);函数的作用,当内核对象处于非激发态的时候,就阻塞住,内核对象处于激发态了,就立即返回。
    2:WaitForSignaleObject的副作用,WaitForSignaleObject对于被等待的内核对象有副作用。

    #include <stdio.h>
    #include <windows.h>
    
    // 可等待对象: 拥有信号状态的内核对象,信号状态通常分为
    //    - 有信号、激发态、可等待状态
    //    - 无信号、非激发态、不可等待状态
    
    // 等待函数:
    //    - 等待多个: WaitForMultipleObjects
    //    - 等待一个: WaitForSingleObjects
    //  - 等待函数的副作用: 【可能会将有信号状态变为无信号状态】
    
    DWORD CALLBACK WorkerThtead(LPVOID Paramater)
    {
        for (int i = 0; i < 100; ++i)
            printf("%d: %d\n", (DWORD)Paramater, i);
    
        return 0;
    }
    
    int main()
    {
        // 创建一个数组,用于保存所有的需要等待的内核对象句柄
        HANDLE ThreadHandles[10]{};
    
        // 使用循环创建 10 个线程,每个线程都需要等待
        for (int i = 0; i < 10; ++i)
            ThreadHandles[i] = CreateThread(NULL, 0, WorkerThtead, (LPVOID)i, 0, NULL);
    
        // 等待多个内核对象
        //    1. 需要等待多少个内核对象
        //    2. 保存内核对象的数组
        //    3. 是否需要等待数组内的所有句柄都有信号
        //    4. 等待时长
        WaitForMultipleObjects(10, ThreadHandles, FALSE, INFINITE);
    
        return 0;
    }
    

    互斥体解决互斥问题:
    被保护的代码(代码访问了共享资源)放置在
    WaitForSignalObject
    ReleaseMutex
    互斥体也具有线程所有权的概念,得到互斥体的线程,需要自己去释放互斥体。谁加锁,谁开锁。如果得到互斥体的线程崩溃了,互斥体会立即变为激发态。所有等待互斥体的线程中会立即有线程得到互斥体。不会造成死锁的问题。
    事件解决互斥问题:需要是自动模式的事件
    被保护的代码(代码访问了共享资源)放置在
    WaitForSignalObject
    SetEvent
    事件,没有线程所有权的概念,任何线程都可以释放事件。
    临界区的缺点:临界区是在一个进程内有效的,无法在多进程的环境下进行同步。
    其次,更为要命的情况,一个线程刚进去临界区,因为奇奇怪怪的原因崩溃了,导致临界区无法被释放,弄得其他线程也没办法再进来临界区,全部卡死造成死锁。

    #include <stdio.h>
    #include <windows.h>
    
    // 互斥体:
    //  特点: 是内核对象,拥有临界区的所有优点,不会死锁,且能够进行进程间同步。
    //  缺点: 慢
    //  函数: 创建互斥体 CreateMutex
    //       开始保护 WaitForXXXObject
    //       结束保护 ReleaseMutext
    //  作用: 虽然大多数的具名内核对象都可以防多开,但是互斥体用的最多
    
    // 创建一个互斥体内核对象
    //  1. 安全属性
    //  2. 默认不设置任何的拥有者,第一个等待成功的就是拥有者
    //  3. 互斥体的名称,进程间同步会用到
    HANDLE Mutex = CreateMutex(NULL, FALSE, NULL);
    
    
    // 全局变量 g_n,作为一个资源,它被多个线程访问,此时即有可能出现问题
    LONG g_n;
    
    DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            WaitForSingleObject(Mutex, INFINITE);
            g_n++;
            ReleaseMutex(Mutex);
        }
        return 0;
    }
    
    DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            WaitForSingleObject(Mutex, INFINITE);
            g_n++;
            ReleaseMutex(Mutex);
        }
    
        return 0;
    }
    int main()
    {
        HANDLE hThread1 = 0, hThread2;
    
        hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
        hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    
        WaitForSingleObject(hThread1, -1);
        WaitForSingleObject(hThread2, -1);
    
        printf("%d", g_n);
        return 0;
    }
    

    事件,没有线程所有权的概念,任何线程都可以释放事件。
    信号量:
    有信号数这么一个概念,只要信号数不为0,那么就处于激发态。WaitForSignaleObject函数对它的副作用是将信号数减1。
    最大信号数为1 的信号量,可以认为是一个事件,可以解决互斥问题。
    被保护的代码(代码访问了共享资源)放置在
    WaitForSignalObject
    ReleaseSemaphore
    b. 对于共享资源有序的访问,也可以更关注于要有序。
    事件和信号量更适合解决有序的问题。因为他们不要求谁上锁,谁开锁。
    用代码实现一个读文件线程,一个写文件线程,请实现先写后读,两个线程都结束之后,主线程结束。
    这种没有过多线程同时访问的有顺序的问题,比较适合用事件来解决。

    #include <stdio.h>
    #include <windows.h>
    
    // 事件:
    //  特点: 是内核对象,可以设置为手动的或者自动。
    //  缺点: 慢
    //  函数: 创建事件 CreateEvent
    //       开始保护 WaitForXXXObject
    //       结束保护 SetEvent
    //       关闭信号 ResetEvent
    //  作用: 如果设置为了手动,那么等待函数的副作用就没有了,无法进行互斥操作,但是可以用于同步
    
    
    // 创建一个事件内核对象
    //  1. 安全属性
    //  2. 是否需要设置为手动状态
    //  3. 默认的信号状态
    //  3. 事件的名称,进程间同步会用到
    HANDLE Event = CreateEvent(NULL, TRUE, TRUE, NULL);
    
    
    // 全局变量 g_n,作为一个资源,它被多个线程访问,此时即有可能出现问题
    LONG g_n;
    
    DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            WaitForSingleObject(Event, INFINITE);
            g_n++;
            SetEvent(Event);
        }
        return 0;
    }
    
    DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            WaitForSingleObject(Event, INFINITE);
            g_n++;
            SetEvent(Event);
        }
    
        return 0;
    }
    int main()
    {
        HANDLE hThread1 = 0, hThread2;
    
        hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
        hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    
        WaitForSingleObject(hThread1, -1);
        WaitForSingleObject(hThread2, -1);
    
        printf("%d", g_n);
        return 0;
    }
    

    多个信号数的信号量,比较适合解决多个线程之间有顺序需要协调的问题,最为经典的就是生产者消费者问题。
    线程同步 - 图1
    关键点有两个:1 必须有一个队列,可以有数量限制,也可以没有数量限制。我们考虑的是有数量限制的问题
    2 每一个生产者是一个线程,每一个消费者也是一个线程。队列满了,生产者需要等待。队列空了,消费者需要等待。
    整个的问题是多线程并发时的协调问题。

    #include <stdio.h>
    #include <windows.h>
    
    // 信号量:
    //  特点: 是内核对象,可以有多把锁。
    //  缺点: 慢
    //  函数: 创建事件 CreateSemaphore
    //       开始保护 WaitForXXXObject
    //       结束保护 ReleaseSemaphore
    
    
    // 创建一个互斥体内核对象
    //  1. 安全属性
    //  2. 初始的信号个数
    //  3. 最大的信号个数
    //  3. 事件的名称,进程间同步会用到
    HANDLE Semaphore = CreateSemaphore(NULL, 1, 1, NULL);
    
    
    // 全局变量 g_n,作为一个资源,它被多个线程访问,此时即有可能出现问题
    LONG g_n;
    
    DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            WaitForSingleObject(Semaphore, INFINITE);
            g_n++;
            ReleaseSemaphore(Semaphore, 1, NULL);
        }
        return 0;
    }
    
    DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
    {
        for (int i = 0; i < 100000; i++)
        {
            WaitForSingleObject(Semaphore, INFINITE);
            g_n++;
            ReleaseSemaphore(Semaphore, 1, NULL);
        }
    
        return 0;
    }
    int main()
    {
        HANDLE hThread1 = 0, hThread2;
    
        hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
        hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    
        WaitForSingleObject(hThread1, -1);
        WaitForSingleObject(hThread2, -1);
    
        printf("%d", g_n);
        return 0;
    }
    

    总结:
    原子操作,只能保证对于基本算数操作是原子性的。
    临界区和互斥体从词语的含义上看,他们主要就是为了解决互斥问题。
    临界区的优点是快。互斥体的优点是能够跨进程访问,崩溃不死锁。
    事件 从词语的含义上看,更适合做通知(产生了一个事件)。比较适合解决有先后顺序的多线程问题。
    事件和互斥体的最大区别,就是线程所有权。互斥体谁上锁,谁开锁。事件没有这个要求。
    信号量,由于存在信号数的问题,比较适合解决多线程的协调问题。典型问题,就是生产者消费者问题。

    原子操作:

    函数 作用 备注
    InterlockedIncrement 自增 InterlockedIncrement(&g_count)
    InterlockedDecrement 自减 InterlockedDecrement(&g_count);
    InterlockedExchangeAdd 加法/减法 InterlockedExchangeAdd(&g_count, 256L);
    InterlockedExchange 赋值 InterlockedExchange(&g_count, 256L);

    临界区

    函数 作用 备注
    InitializeCriticalSection 初始化
    DeleteCriticalSection 销毁
    EnterCriticalSection 进入临界区
    LeaveCriticalSection 离开临界区

    互斥体

    函数 作用 备注
    CreateMutex 创建互斥体 可以给互斥体起名字
    OpenMutex 打开互斥体,得到句柄 根据名字才能打开互斥体
    ReleaseMutex 释放互斥体 会使得互斥体处于激发态
    CloseHandle 关闭句柄 使用完后关闭
    WaitForSignalObject 等待互斥体处于激发态 等到激发态后,会使得互斥体再次处于非激发态

    事件

    函数 作用 备注
    CreateEvent 创建事件 可以给事件起名字
    可以设置两种模式:手工 自动
    OpenEvent 打开事件,得到句柄 根据名字才能打开事件
    SetEvent 释放事件 会使得事件处于激发态
    ResetEvent 重置事件 会使得事件处于非激发态,对手工模式的事件有效
    WaitForSignalObject 等待事件处于激发态 等到激发态后,对于自动模式的事件会使其再次处于非激发态

    信号量

    函数 作用 备注
    CreateSemaphore 创建信号量 可以给信号量起名字
    可以指定最大信号数和当前信号数
    OpenSemaphore 打开信号量 根据名字才能打开信号量
    ReleaseSemaphore 释放信号量 会增加信号量的信号数,但是不会超过最大信号数
    WaitForSignalObject 等待信号量处于激发态 若处于激发态,则会减少1个信号数,信号数位0,将其置为非激发态