C++ multithreading learning 04 multithreading state and mutex

1. Thread state description:

Initialization (Init): The thread is being created: first apply for a blank TCB, and fill in some control and management process information in the TCB; then the system allocates the resources necessary for the process to run; finally transfer the process to into the ready state.
Ready (Ready): The thread is in the ready list, waiting for CPU scheduling.
Running: The thread is running.
Blocked (Blocked): The thread is blocked and suspended. The Blocked state includes: pend (blocked by locks, events, semaphores, etc.), suspend (active pend), delay (delayed blocking), pendtime (waiting for timeouts due to locks, events, semaphores, etc.).
Exit (Exit): The thread finishes running and waits for the parent thread to reclaim its control block resources.
insert image description here
Initialization→Ready state: The operating system has allocated resources for the thread and mounted it on the ready queue of the CPU.
Ready→Run: The thread is ranked at the head of the ready queue, and the CPU has scheduled the thread (such as time slice rotation algorithm, high response ratio priority algorithm, short job priority, etc.), and it is handed over to the CPU to run Run→Ready
: The thread running on the CPU is used for CPU scheduling and interrupts the operation, mounts the tail of the ready queue Ready
→ Blocking: X
Running → Blocking: When the process requests the use and allocation of a certain resource (such as a peripheral) or waits for a certain event When an event occurs (such as the completion of an IO operation), it transitions from the running state to the blocking state. The process requests the operating system to provide services in the form of system calls. This is a special form of calling the operating system kernel process by running user-mode programs.
Blocking state → ready state: when the event that the process is waiting for arrives, such as I/O operation When the interrupt or interrupt ends, the interrupt handler
must change the state of the corresponding process from the blocked state to the ready state

It is an active behavior for a process to change from the running state to the blocked state, while changing from the blocked state to the ready state is a passive behavior that requires the assistance of other related processes. Therefore, there is a bit of an error in the figure. The blocking state cannot be switched to the ready state, because the blocking state occurs actively when the process is running:
when the program executes system calls such as read and recv in blocking I/O, the process will remain blocked until Data arrives or reaches the set timeout period.
A process can explicitly enter blocking by executing the sleep system call.
A process in the ready state cannot execute any code that causes it to block (that is, it cannot execute blocking system calls such as read/recv/sleep), so it cannot be converted to a blocked state.

2. Race condition and critical section

Race Condition: Multiple threads read and write shared data at the same time.
Critical Section: Code fragments for reading and writing shared data
Avoid race condition strategy, protect the critical section, and only one thread can enter the critical section at the same time

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

The code printed on the screen is the critical section. The 10 threads created by for use this code at a very fast interval, so there will be competition. One thread prints half of the line before it is scheduled to run another one. thread, so that the display is messed up

insert image description here
So use the lock mutex:
If you want to access the critical area, you must first perform the "lock" operation, if the lock is successful, then read and write the critical area, and release the lock after the read and write operations are completed; if the "lock" is unsuccessful, then The thread is blocked and does not occupy CPU resources until the locking is successful.

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

insert image description here

std::mutex also has another operation: mtx.try_lock(), which literally means: "try to lock". The difference from mtx.lock() lies in two points: 01
try_lock () If the lock is unsuccessful, the current thread It does not block, but takes up CPU resources and continues to wait to enter the critical section; while lock() will block, and other threads can use CPU resources at this time. Reason: The lock
function
is blocked because it is passed when calling the WaitForSingleObject function The second parameter is INFINITE, which means waiting indefinitely, so it is blocked.
The tryLock function is non-blocking and returns immediately after calling. Because the second parameter passed when it calls the WaitForSingleObject function is 0, which means that it does not wait and returns immediately.

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 : The return value types of try_lock() and lock() are also different:
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()));
    }

Therefore, try_lock() can be used to invigilate whether the lock is successful:

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() will print a dot every time the lock request fails, for example, it is requested twice before it comes in successfully
insert image description here

The above difference 1 is mentioned: if the lock() is unsuccessful, the thread will block, and then release the occupied CPU resources, and other threads cannot use the CPU resources; if try_lock() fails to lock, the current thread will not block, Instead, it occupies CPU resources and continues to wait to enter the critical section. At this time, other threads cannot use the CPU, so try_lock() requires sleep_for (100ms), otherwise the CPU resources will be used up, and the threads occupying the critical section will not be able to release resources. It may cause deadlock:
remove this_thread::sleep_for(100ms);
insert image description here
because we are equipped with a high-end processor, a small number of threads will not cause deadlock, but because try_lock() occupies the CPU for a long time, it leads to occupation It is difficult for threads in the critical section to release resources, so try_lock() can successfully occupy CPU resources many times

3. The pit of mutual exclusion lock

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

Theoretically, after thread No. 1 comes in, 23 is blocked and queued in the blocking queue. After No. 1 unlocks, 23 goes out of the queue and enters the running state one by one. However, in reality, after No. 1 unlocks, it will go to the lock immediately, and then the system makes a kernel judgment: judge the lock Whether the resources are occupied by other threads, but the unlock to lock is very fast, the thread may still occupy the thread resources, and then the thread continues to print, blocking 23 threads:
insert image description here

So add sleep_for between unlock and lock to let thread 1 fully release resources:
insert image description here

Guess you like

Origin blog.csdn.net/qq_42567607/article/details/125467883