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;
}

在屏幕上打印的代码就是临界区,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);
    }
}

在这里插入图片描述

std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于两处:
01try_lock()如果上锁不成功,当前线程不阻塞,而是占着CPU资源继续等待可以进入临界区;而lock()是会阻塞的,此时其他线程可以来使用CPU资源
原因:
lock函数是阻塞的,因为它调用WaitForSingleObject函数时传递的第二个参数是INFINITE,表示无限等待下去,所以是阻塞的。
tryLock函数时非阻塞的,调用后立即返回。因为它调用WaitForSingleObject函数时传递的第二个参数是0,表示不等待,立即返回。

status_t Mutex::lock()  
{
    
      
    DWORD dwWaitResult;  
    dwWaitResult = WaitForSingleObject((HANDLE) mState, INFINITE);  
    return dwWaitResult != WAIT_OBJECT_0 ? -1 : NO_ERROR;  
}  

void Mutex::unlock()  
{
    
      
    if (!ReleaseMutex((HANDLE) mState))  
        LOG(LOG_WARN, "thread", "WARNING: bad result from unlocking mutex\n");  
}  

status_t Mutex::tryLock()  
{
    
      
    DWORD dwWaitResult;  

    dwWaitResult = WaitForSingleObject((HANDLE) mState, 0);  
    if (dwWaitResult != WAIT_OBJECT_0 && dwWaitResult != WAIT_TIMEOUT)  
        LOG(LOG_WARN, "thread", "WARNING: bad result from try-locking mutex\n");  
    return (dwWaitResult == WAIT_OBJECT_0) ? 0 : -1;  
}  

02: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);
        }
    }

lock():

 void lock() {
    
    
        _Check_C_return(_Mtx_lock(_Mymtx()));
    }

因此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();
    }
}

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

上面的区别1说到:而lock()上锁不成功的话线程会阻塞,然后释放占着的CPU的资源,其他线程无法使用CPU资源;try_lock()如果上锁不成功,当前线程不阻塞,而是占着CPU资源继续等待可以进入临界区,此时其他线程不能使用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号线程进来之后,23被阻塞,排在阻塞队列中,当1号unlock之后23依次出队进去运行态,然而实际1号unlock之后立马会到lock,然后系统做内核判断:判断锁的资源是否被其他线程占掉,然而unlock到lock非常快,该线程可能仍然占着线程资源,然后该线程继续去打印,阻塞23线程:
在这里插入图片描述

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

猜你喜欢

转载自blog.csdn.net/qq_42567607/article/details/125467883
今日推荐