C++11 – Multithreading

Table of Contents

1. A brief introduction to the thread class

2. Thread function parameters

3. Atomic operation library (atomic)

4.lock_guard and unique_lock

1.lock_guard

2.unique_lock

5. Condition variables


1. A brief introduction to the thread class

Before C++11, multi-threading issues were all related to the platform. For example, windows and linux each had their own interfaces.
port, which makes the code less portable. The most important feature in C++11 is support for threads, making C++
There is no need to rely on third-party librariesin parallel programming, and the concept of atomic classes is also introduced in atomic operations. To use the standard library
Threads must include the header file.

Function Name Function
thread() Construct a thread object without any associated thread function, that is, no thread is started
thread(fn,
args1, args2,
…)
Construct a thread object and associate the thread function fn, args1, args2,… as parameters of the thread function
get_id() Get thread id
jionable() Whether the thread is still executing, joinable represents an executing thread in .
jion() After this function is called Will block the thread. When the thread ends, the main thread continues to execute
detach() Called immediately after creating the thread object, it is used to separate the created thread from the thread object. The separated thread becomes a background thread. The “life and death” of the created thread is the same as that of the main thread. Not relevant

Notice:

1. Thread is a concept in the operating system.The thread object can be associated with a thread and is used to control the thread and obtain the thread’s information.
state.

2. When a thread object is created, no thread function is provided, and the object does not actually correspond to any thread.

The return value type of get_id() is the id type. The id type is actually a class encapsulated under the std::thread namespace. In this class
Contains a structure:

// View under vs
typedef struct
{ /* thread identifier for Win32 */
void* _Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;

3. When a thread object is created and a thread function is associated with the thread, the thread is started and runs together with the main thread.
Thread functions can generally be provided in the following three ways:

  1. function pointer
  2. lambda expression
  3. function object
#include<iostream>
#include<thread>
#include<string>
using namespace std;

void func1(string str)
{
cout << str << endl;
}
struct func2
{
void operator()(string str)
{
cout << str << endl;
}
};

int main()
{
std::thread t1(func1, "function pointer");
std::thread t2(func2(), "functor");
std::thread t3([](string str){cout << str << endl; }, "lambda");
t1.join();
t2.join();
t3.join();
return 0;
}

4. The thread class is copy-proof and does not allow copy construction and assignment, but move construction and move assignment are allowed, that is, a
The state of the thread object associated with the thread is transferred to other thread objects, and the execution of the thread is not intended during the transfer.
5. You can use the jionable() function to determine whether the thread is valid. If it is any of the following situations, the thread is invalid.

  1. Thread object constructed using no-argument constructor
  2. The state of the thread object has been transferred to other thread objects
  3. The thread has ended by calling jion or detach.

2. Thread function parameters

The parameters of the thread function are copied to the thread stack space in the form of value copy. Therefore: Even if the thread parameters are reference types, in
External parameters cannot be modified after modification in the thread, because they actually refer to the copy in the thread stack, not the external parameters.

If you want to change external actual parameters through formal parameters, you must use the std::ref() function or use a pointer:

#include <thread>
void ThreadFunc1(int & x)
{
x + = 10;
}
void ThreadFunc2(int* x)
{
*x + = 10;
}
int main()
{
int a = 10;
int b = 10;
// If you want to change the external actual parameters through formal parameters, you must use the std::ref() function
thread t2(ThreadFunc1, std::ref(a));
t2.join();
cout << a << endl;
//copy of address
thread t3(ThreadFunc2, & amp;b);
t3.join();
cout << b << endl;
return 0;
}

Note: If a class member function is used as a thread parameter, this must be used as a thread function parameter.

3. Atomic operation library (atomic)

The main problem with multithreading is the problem caused by shared data (ie, thread safety). If the shared data is all read-only, then no need to ask
question, because the read-only operation will not affect the data, let alone modify the data, so all threads will obtain the same data.
according to. However, when one or more threads want to modify shared data, a lot of potential trouble arises. for example:

Two threads perform 100,000 + + operations respectively:

#include <iostream>
#include <thread>
using namespace std;
unsigned long sum = 0;

void fun(size_t num)
{
for (size_t i = 0; i < num; + + i)
sum + + ;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}

The first solution we think of is to lock, but there is a flaw in locking: as long as one thread is dealing with sum++, other threads will be blocked, which will affect the efficiency of program operation, and if the lock is controlled Not good, it can easily cause deadlock.

Hence the introduction of atomic operations in C++11. The so-called atomic operation:That is, one or a series of operations that cannot be interrupted, introduced in C++11
The atomic operation type makes data synchronization between threads very efficient, atomic operation header file .

For example:

#include <iostream>
#include <thread>
#include<atomic>
using namespace std;
//unsigned long sum = 0;
atomic_int sum = 0;

void fun(size_t num)
{
for (size_t i = 0; i < num; + + i)
sum + + ;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}

In C++11,programmers do not need to lock and unlock atomic type variables. Threads can mutually exclude atomic type variables.
access.

More generally,programmers can use the atomic class template to define any atomic type they need.

atmoic<T> t; // Declare an atomic type variable t of type T
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // Compilation failed
atomic<int> a2(0);
//a2 = a1; // Compilation failed
return 0;
}

Note: Atomic types usually belong to “resource” data. Multiple threads can only access a copy of a single atomic type, so in C++11
, atomic types can only be constructed from their template parameters, and atomic types are not allowed to undergo copy construction, move construction, and
operator=, etc., in order to prevent accidents, the standard library has changed the copy construction, move construction, and assignment operations in the atmoic template class
Symbol overloading is removed by default.

four.lock_guard and unique_lock

In a multi-threaded environment, if you want to ensure the safety of a certain variable, just set it to the corresponding atomic type, that is, high
It is efficient and less prone to deadlock problems. But in some cases, we may need to ensure the security of a piece of code, so we can only
Controlled by locks.

For example: one thread increases the variable number by one 100 times, and another thread decreases the variable number by one 100 times. Each operation increases or decreases one by one.
Finally, output the result of number, requiring: the final value of number is 1.

#include <thread>
#include <mutex>
int number = 0;
mutex g_lock;
int ThreadProc1()
{
for (int i = 0; i < 100; i + + )
{
g_lock.lock();
+ + number;
cout << "thread 1 :" << number << endl;
g_lock.unlock();
}
return 0;
}
int ThreadProc2()
{
for (int i = 0; i < 100; i + + )
{
g_lock.lock();
--number;
cout << "thread 2:" << number << endl;
g_lock.unlock();
}
return 0;
}

int main()
{
thread t1(ThreadProc1);
thread t2(ThreadProc2);
t1.join();
t2.join();
cout << "number:" << number << endl;
system("pause");
return 0;
}

Defects of the above code: When the lock control is not good, it may cause deadlock. The most common ones are code return in the middle of the lock, or deadlock in the middle of the lock.
throws an exception within the scope.
So: C++11 uses RAII to encapsulate locks, namely lock_guard and unique_lock.

lock_guard is defined as follows:

template<class _Mutex>
class lock_guard
{
public:
// When constructing lock_gard, _Mtx has not been locked yet
explicit lock_guard(_Mutex & _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// When constructing lock_gard, _Mtx has already been locked, so there is no need to lock it again.
lock_guard(_Mutex & _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard & amp;) = delete;
lock_guard & amp; operator=(const lock_guard & amp;) = delete;
private:
_Mutex & _MyMutex;
};

1.lock_guard

As can be seen from the above code, the lock_guard class template mainly seals the mutex it manages through RAII.
Installation,Where locking is required, just instantiate a lock_guard using any mutex introduced above and call the constructor
After successful locking, the lock_guard object will be destroyed before going out of scope, and the destructor will be called to automatically unlock it, which can effectively avoid deadlock.
question.

Modify the above code:

#include <thread>
#include <mutex>
int number = 0;
mutex g_lock;
int ThreadProc1()
{
for (int i = 0; i < 100; i + + )
{
lock_guard<mutex> lock(g_lock);//Call the constructor to lock and automatically unlock when out of scope
+ + number;
cout << "thread 1 :" << number << endl;
\t\t
}
return 0;
}
int ThreadProc2()
{
for (int i = 0; i < 100; i + + )
{
lock_guard<mutex> lock(g_lock);//Call the constructor to lock and automatically unlock when out of scope
--number;
cout << "thread 2:" << number << endl;
}
return 0;
}

int main()
{
thread t1(ThreadProc1);
thread t2(ThreadProc2);
t1.join();
t2.join();
cout << "number:" << number << endl;
system("pause");
return 0;
}

Defects of lock_guard:It is too single and users have no way to control the lock, so C++11 provides unique_lock.

2.unique_lock

Similar to lock_gard, the unique_lock class template also uses RAII to encapsulate locks, and also manages the locking and unlocking operations of mutex objects in an exclusive ownership manner, that is, no copying can occur between its objects. During construction (or move assignment), the unique_lock object needs to pass a Mutex object as its parameter, and the newly created unique_lock object is responsible for the locking and unlocking operations of the incoming Mutex object. When using the above type of mutex to instantiate a unique_lock object, the constructor is automatically called to lock it. When the unique_lock object is destroyed, the destructor is automatically called to unlock it, which can easily prevent deadlock problems.

Different from lock_guard, unique_lock is more flexible and provides more member functions:

  1. Lock/unlock operations: lock, try_lock, try_lock_for, try_lock_until and unlock.
  2. Modification operations: move assignment, exchange (swap: exchange ownership of the mutex managed by another unique_lock object), release (release: return a pointer to the mutex object it manages, and release ownership).
  3. Obtain attributes: owns_lock (returns whether the current object is locked), operator bool() (same function as owns_lock()), mutex (returns the pointer of the mutex managed by the current unique_lock).

5. Condition variables

We have already talked about the familiarity of condition variables in Linux mutual exclusion and synchronization. They are used to notify each other between threads. There is no big difference between condition_variable and Linux posix condition variables. They are mainly implemented in an object-oriented manner.

For example: using two threads, the two threads alternately print odd and even numbers:

#include<mutex>
#include<condition_variable>
mutex g_lock;//lock
condition_variable cond;//Condition variable

int num = 1;

//Print odd numbers
void Func1(int n)
{
for (int i = 1; i <= n; i + + )
{
unique_lock<mutex> mutex(g_lock);
if (num % 2 == 0)
{
cond.wait(mutex);
}
cout << "thread1: " << num + + << endl;
cond.notify_one();
}
}

//Print even numbers
void Func2(int n)
{
for (int i = 1; i <= n; i + + )
{
unique_lock<mutex> mutex(g_lock);
if (num % 2 == 1)
{
cond.wait(mutex);
}
cout << "thread2: " << num + + << endl;
cond.notify_one();
}
}
int main()
{
thread t1(Func1, 100);
thread t2(Func2, 100);

t1.join();
t2.join();

return 0;
}