[C++] thread library

1. thread class

The thread library is proposed by the C++11 standard, so that C++ does not need to rely on third-party libraries when programming in parallel, and the concept of atomic classes is also introduced in atomic operations. To use threads from the standard library, the <thread> header must be included.
insert image description here

Common interface:

Function name Function
thread() Construct a thread object, not associated with any thread function, that is, no thread is started
get_id() get thread id
jion() After the function is called, the thread will be blocked. When the thread ends, the main thread will continue to execute
detach() Called immediately after the thread object is created, it is used to separate the created thread from the thread object, and the separated thread becomes a background thread, and the "life and death" of the created thread has nothing to do with the main thread

Note:
1️⃣ Thread is a concept in the operating system. A thread object can be associated with a thread to control the thread and obtain
the status of the thread.
2️⃣ When a thread object is created, no thread function is provided, and the object does not actually correspond to any thread.
3️⃣ The return value type of get_id() is the id type, and the id type is actually a class encapsulated under the std::thread namespace.
4️⃣ When a thread object is created and a thread function is associated with the thread, the thread will be started and run together with the main thread. Generally, thread functions can be provided in the following three ways:
function pointer
lambda expression
function object

use:

#include <iostream>
#include <thread>
#include <vector>
#include <windows.h>

using namespace std;

void fun()
{
    
    
	Sleep(1100);
	cout << this_thread::get_id() << endl;
}

int main()
{
    
    
	thread t1([]() {
    
    
		while (true)
		{
    
    
			Sleep(1000);
			cout << this_thread::get_id() << endl;
		}
		});
	thread t2(fun);

	t1.join();
	t2.join();
	return 0;
}

insert image description here

2. Thread safety issues

static int val = 0;

void fun1(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

void fun2(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

int main()
{
    
    
	thread t1(fun1, 100000);
	thread t2(fun2, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

insert image description here
It can be seen that it should have been added to 300000, but it has not been added now.
Because the val++ operation is not atomic.

2.1 Locking

In order to ensure thread safety we need to lock:

static int val = 0;
mutex mtx;

void fun1(int n)
{
    
    
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
	mtx.unlock();
}

void fun2(int n)
{
    
    
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
	mtx.unlock();
}

int main()
{
    
    
	thread t1(fun1, 100000);
	thread t2(fun2, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

insert image description here
Note here that it is better to put locking and unlocking outside the for loop, because the process of locking and unlocking also consumes resources.

Here it is also possible if two threads call a function at the same time.
insert image description here
The reason here is that each thread will have an independent stack structure to store private data.

2.2 CAS operation

The full name of CAS is compare and swap, a non-blocking atomic operation provided by JDK, which guarantees the atomicity of update operations through hardware. It allows multiple threads to modify shared resources non-blockingly, but only one thread can modify at a time, and other threads will not block but try again.

insert image description here

2.3 Atomic operation library (atomic)

The atomic operation library provides the relevant interface of CAS.
If you want to use first-served import header files:

#include <atomic>
atomic<int> val = 0;

void fun1(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

void fun2(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

int main()
{
    
    
	thread t1(fun1, 100000);
	thread t2(fun1, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

insert image description here

Here val++ will be abandoned to ensure thread safety

In practice, try to avoid using global variables.

int main()
{
    
    
	atomic<int> val = 0;
	auto func = [&](int n) {
    
    
		for (int i = 0; i < n; i++)
		{
    
    
			val++;
		}
	};
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

Three, lock

3.1 The difference between lock and try_lock

The locking process of the lock: if there is no lock, apply for the lock, if other threads hold the lock, they will block and wait until other threads are unlocked.
And try_lock can prevent the thread from blocking, and if you can't apply, you can do other things. Returns true on success and false on failure.

if (try_lock())
{
    
    
	// ...
}
else
{
    
    
	// 干其他的事情
}

3.2 recursive_mutex recursive lock

If we want to use lock to lock normally in the recursive function, it is likely to cause deadlock. Because it recurses to the next layer after locking, the lock has not been unlocked, which is equivalent to applying for a lock after locking it.

void fun()
{
    
    
	lock();
	fun();// 递归
	unlock();
}

This can be avoided by using recursive_mutex.
principle:

After recursing to the next layer and encountering a lock, first judge the ID value of the thread. If it is the same, there is no need to lock and go directly to the next process.

3.3 lock_guard RAII lock

What is RAII?

It is an idiom in the C++ language to manage resources and avoid leaks. What is used is the principle that objects constructed by C++ will eventually be destroyed. The approach of RAII is to use an object, obtain the corresponding resources during its construction, control the access to the resources during the life of the object, so that it is always valid, and finally release the resources obtained during the construction when the object is destructed.

We know that it is possible to throw an exception after locking, which may cause the lock to not be released. In order to avoid this situation, we can encapsulate the lock, and unlock it in the destructor, so that it can be automatically destroyed when it goes out of scope.
The specific implementation is in [linux] thread mutual exclusion and synchronization 2.5 lock encapsulation .

The thread library provides us with such a lock .
insert image description here

int main()
{
    
    
	int val = 0;
	mutex mtx;
	auto func = [&](int n) {
    
    
		lock_guard<mutex> lock(mtx);
		for (int i = 0; i < n; i++)
		{
    
    
			val++;
		}
	};
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

insert image description here

3.4 unique_lock active unlock

insert image description here
The difference between it and lock_guard is that lock_guard can only implement RAII, while unique_lock can actively unlock its own lock without waiting for its destruction.

4. Two threads alternately print 1~100

Now we want two threads to alternately print from 1 to 100, one thread printing odd numbers and one thread printing even numbers.

int main()
{
    
    
	int val = 1;
	thread t1([&]() {
    
    
		while (val < 100)
		{
    
    
			if (val % 2 != 0)
			{
    
    
				cout << "thread 1" << "->" << val << endl;
				val++;
			}
		}
		});
	thread t2([&]() {
    
    
		while (val <= 100)
		{
    
    
			if (val % 2 == 0)
			{
    
    
				cout << "thread 2" << "->" << val << endl;
				val++;
			}
		}
		});
	t1.join();
	t2.join();
	return 0;
}

Although this can meet the requirements, it may cause a waste of resources.

There is a scenario where t2 meets the conditions and is running, but when the time slice is up, switch to t1. At this time, t1 does not meet the conditions, and it has been in an endless loop of while. It does not switch until the time slice is up.
This will result in a waste of CPU resources.

So we hope that two threads can notify each other, which requires condition variable control.

4.1 Condition variables

The concept of condition variables is introduced in detail in [linux] Thread Mutual Exclusion and Synchronization 3.1 Condition Variables .

C++11 also encapsulates condition variables.
Header file: #include <condition_variable>
related interface:
insert image description here

insert image description here
And we know that condition variables are not thread-safe, so we need to add a lock first.
Note here that when using wait, the lock must be passed in, and it must be unique_lock.
Wait passes the lock in to unlock it, and it will be locked again when it returns.

int main()
{
    
    
	int val = 1;
	mutex mtx;
	condition_variable cv;
	thread t1([&]() {
    
    
		while (val < 100)
		{
    
    
			unique_lock<mutex> lock(mtx);
			while (val % 2 == 0)
			{
    
    
				cv.wait(lock);// 阻塞
			}
			cout << "thread 1" << "->" << val << endl;
			val++;
			cv.notify_one();
		}
		});
	thread t2([&]() {
    
    
		while (val <= 100)
		{
    
    
			unique_lock<mutex> lock(mtx);
			while (val % 2 != 0)
			{
    
    
				cv.wait(lock);
			}
			cout << "thread 2" << "->" << val << endl;
			val++;
			cv.notify_one();
		}
		});
	t1.join();
	t2.join();
	return 0;
}

analyze:

At the beginning, t1 applies for the lock, then t2 will block and wait at the place where the lock is applied. But t1 does not meet the conditions, so wait, enter the wait function and it will be automatically unlocked, then t2 can run. Note here that notify_one() may wake up a running thread, but no problem, at this time notify_one() will do nothing by default.



Guess you like

Origin blog.csdn.net/qq_66314292/article/details/130173588