• C++ 多线程学习04 多线程状态与互斥锁


    一、线程状态说明:

    初始化(Init):该线程正在被创建:首先申请一个空白的TCB,并向TCB中填写一些控制和管理进程的信息;然后由系统为该进程分配运行时所必需的资源;最后把该进程转入到就绪状态。
    就绪(Ready):该线程在就绪列表中,等待 CPU 调度。
    运行(Running):该线程正在运行。
    阻塞(Blocked):该线程被阻塞挂起。Blocked 状态包括:pend(锁、 事件、信号量等阻塞)、suspend(主动 pend)、delay(延时阻塞)、 pendtime(因为锁、事件、信号量时间等超时等待)。
    退出(Exit):该线程运行结束,等待父线程回收其控制块资源。
    在这里插入图片描述
    初始化→就绪态:操作系统为线程分配好了资源,将其挂载CPU的就绪队列上。
    就绪→运行:该线程排在就绪队列队首元素,且CPU调度到了该线程(如时间片轮转算法、高响应比优先算法、短作业优先等),将其交给CPU去运行
    运行→就绪:正在CPU上运行的线程用于CPU的调度中断了运行,挂载就绪队列队尾
    就绪→阻塞:X
    运行→阻塞:当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如IO操作的完成)时,它就从运行状态转换为阻塞状态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式
    阻塞状态→就绪状态:当进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理
    程序必须把相应进程的状态由阻塞状态转换为就绪状态

    一个进程从运行状态变成阻塞状态是一个主动的行为,而从阻塞状态变到就就绪状态是一个被动的行为,需要其他相关进程的协助。因此图中有点误差,阻塞态不能切换到就绪态,因为阻塞态是进程在运行时主动发生的:
    在程序执行阻塞I/O中的read、recv等系统调用时,进程将会一直处于阻塞直到数据到来或者到达设定的超时时间。
    进程可以执行sleep系统调用来显式进入阻塞。
    处于就绪态的进程无法执行任何造成其阻塞的代码(也就是无法执行read/recv/sleep等阻塞系统调用),故无法转换为阻塞态。

    二、竞争状态和临界区

    竞争状态(Race Condition):多线程同时读写共享数据
    临界区(Critical Section):读写共享数据的代码片段
    避免竞争状态策略, 对临界区进行保护,同时只能有一个线程进入临界区

    void TestThread()
    {
            cout << "==============================" << endl;
            cout << "test 001" << endl;
            cout << "test 002" << endl;
            cout << "test 003" << endl;
            cout << "==============================" << endl;
            this_thread::sleep_for(1000ms);
    }
    int main(int argc, char* argv[])
    {
        for (int i = 0; i < 10; i++)
        {
            thread th(TestThread);
            th.detach();
        }
        getchar();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在屏幕上打印的代码就是临界区,for创建的10个线程以很快的间隔去进行使用这块代码,于是会出现竞争,一个线程打印一半还没来得及打印换行符就被调度去运行另外一个线程,以至于显示乱了

    在这里插入图片描述
    于是使用锁mutex:
    如果想访问临界区,首先要进行"加锁"操作,如果加锁成功,则进行临界区的读写,读写操作完成后释放锁; 如果“加锁”不成功,则线程阻塞,不占用CPU资源,直到加锁成功。

    void TestThread()
    {
        for(;;){
            //获取锁资源,如果没有则阻塞等待
            mux.lock(); //
            cout << "==============================" << endl;
            cout << "test 001" << endl;
            cout << "test 002" << endl;
            cout << "test 003" << endl;
            cout << "==============================" << endl;
            mux.unlock();
            //this_thread::sleep_for(1000ms);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞,而是占着CPU资源继续等待可以进入临界区;try_lock()与lock()的返回值类型也不一样:
    try_lock():

    _NODISCARD bool try_lock() {
            const auto _Res = _Mtx_trylock(_Mymtx());
            switch (_Res) {
            case _Thrd_success:
                return true;
            case _Thrd_busy:
                return false;
            default:
                _Throw_C_error(_Res);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    lock():

     void lock() {
            _Check_C_return(_Mtx_lock(_Mymtx()));
        }
    
    • 1
    • 2
    • 3

    因此try_lock()可以用于监考上锁是否成功的情况:

    void TestThread()
    {
        for(;;){
            if (!mux.try_lock())
            {
                cout << "." << flush;
                this_thread::sleep_for(100ms);
                continue;
            }
            cout << "==============================" << endl;
            cout << "test 001" << endl;
            cout << "test 002" << endl;
            cout << "test 003" << endl;
            cout << "==============================" << endl;
            mux.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    try_lock()每次请求上锁失败后会打印一个点,比如这里就是请求了两次才成功进来
    在这里插入图片描述

    try_lock()如果上锁不成功,当前线程不阻塞,而是占着CPU资源继续等待可以进入临界区,而lock()上锁不成功的话线程会阻塞,然后释放占着的CPU的资源,因此try_lock()要sleep_for(100ms),不然把CPU资源占完了,并且占着临界区的线程也无法释放资源,可能会造成死锁:
    把this_thread::sleep_for(100ms);去掉:
    在这里插入图片描述
    用于我们搭载的是高端处理器,少量的线程还不至于死锁,但是由于try_lock()长时间占着CPU,导致占着临界区的线程很难释放资源,于是try_lock()非常多次才能成功占有CPU资源

    三、互斥锁的坑

    void ThreadMainMux(int i)
    {
        for (;;)
        {
            mux.lock();
            cout << i << "[in]" << endl;
            this_thread::sleep_for(1000ms);
            mux.unlock();
            //this_thread::sleep_for(1ms);
        }
    }
    int main(int argc, char* argv[])
    {
        for (int i = 0; i < 3; i++)
        {
            thread th(ThreadMainMux, i + 1);
            th.detach();
        }
    
    
    
        getchar();
        for (int i = 0; i < 10; i++)
        {
            thread th(TestThread);
            th.detach();
        }
        getchar();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    理论上1号线程进来之后,23被阻塞,排在阻塞队列中,当1号unlock之后23依次出队进去运行态,然而实际1号unlock之后立马会到lock,然后系统做内核判断:判断锁的资源是否被其他线程占掉,然而unlock到lock非常快,该线程可能仍然占着线程资源,然后该线程继续去打印,阻塞23线程:
    在这里插入图片描述

    于是在unlock到lock之间加上sleep_for让1号线程充分释放掉资源:
    在这里插入图片描述

  • 相关阅读:
    gitlab git lfs的替代软件整理汇总及分析
    将多个EXCEL 合并一个EXCEL多个sheet
    开源游戏服务器框架NoahGameFrame(NF)客户端的Log日志系统(五)
    365天深度学习训练营-第P2周:彩色图片识别
    帆软FineReports使用超级链接导出excel出现错误代码:11300004没有找到模板文件
    云计算 - 阿里云最佳云上实践介绍 卓越架构
    Taro小程序富文本解析4种方法
    高速串行总线——SATA
    react可视化编辑器 第一章 拖拽
    SBT40100VFCT-ASEMI塑封肖特基二极管SBT40100VFCT
  • 原文地址:https://blog.csdn.net/qq_42567607/article/details/125467883