[C++ 11] thread class of multithreaded programming

Before C++11, C + + language did not provide language level support for concurrent programming, which makes us have a lot of inconvenience when writing portable concurrent programs. Now C++11 adds threads and thread related classes, which conveniently supports concurrent programming, and greatly improves the portability of multithreaded programs.

The thread class provided in C++11 is called std::thread. It is very simple to create a new thread based on this class. You only need to provide a thread function or function object, and you can specify the parameters of the thread function at the same time. Let's first learn about some common API s provided by this class:

1. Thread class constructor

// ①
thread() noexcept;

// ②
thread( thread&& other ) noexcept;

// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );

// ④
thread( const thread& ) = delete;

Constructor ①: the default constructor is to construct a thread object in which no processing action is performed

Constructor ②: move the constructor to transfer the thread ownership of other to the new thread object. After that, other no longer represents the execution thread.

Constructor ③: create a thread object and execute the business logic in function f in this thread. args is the parameter to be passed to function F

    Task function f There are many optional types of, as follows:
        Ordinary function, class member function, anonymous function, imitation function (these are callable object types)
        It can be a callable object wrapper type, or it can be a type obtained after binding with a binder (imitation function)

Constructor ④: use = delete to display and delete the copy structure. Copying between thread objects is not allowed

2. Thread class public member function

2.1 get_id()

After the application is started, there is only one thread by default. This thread is generally called the main thread or parent thread. Threads created through thread classes are generally called child threads. Each created thread instance corresponds to a thread ID, which is unique. Each existing thread instance can be distinguished and identified through this ID, The function to get the thread ID is called get_id(), the function prototype is as follows:

std::thread::id get_id() const noexcept;

The sample program is as follows:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func(int num, string str)
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "Child thread: i = " << i << "num: " 
             << num << ", str: " << str << endl;
    }
}

void func1()
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "Child thread: i = " << i << endl;
    }
}

int main()
{
    cout << "Thread of main thread ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "thread  t Thread ID: " << t.get_id() << endl;
    cout << "thread  t1 Thread ID: " << t1.get_id() << endl;
}

There is a bug in the above example program. Two sub threads are created in the main thread in turn, and the thread ID s of the two sub threads are printed. Finally, the main thread exits after execution (the main thread is the thread that executes the main () function). By default, when the main thread is destroyed, the two child threads associated with it will also be destroyed. However, at this time, it is possible that the tasks in the child threads have not been completed, and we will not get the desired results in the end.
When a thread is started (a thread object is created) and the thread ends (std::terminate()), how do we recycle the resources used by the thread? The thread library gives us two options:

Add in: join()
Split type: detach()

We must choose between the two before the thread object is destroyed, otherwise there will be bug s during the program running.

2.2 join()

If you want to block the execution of the main thread, you only need to call this method through the sub thread object in the main thread. After the task function in the sub thread object calling this method is executed, the blocking of the main thread will be relieved. The function prototype of this function is as follows:

void join();

The above code is modified as follows:

int main()
{
    cout << "Thread of main thread ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "thread  t Thread ID: " << t.get_id() << endl;
    cout << "thread  t1 Thread ID: " << t1.get_id() << endl;
    t.join();
    t1.join();
}

The compilation output is as follows:

When the main thread runs to the eighth line, t.join();, According to the execution of the task function func() of the sub thread object T, the main thread will do the following:

  • If the task function func() is not completed, the main thread will block until the task is completed. The main thread will unblock and continue to run downward
  • If the task function func() has been executed, the main thread will not block and continue to run downward

The same is true for the code on line 9.

2.3 detach()

The detach() function is used to separate threads, separating the main thread and the created sub thread. After the thread separation, the main thread will also destroy all the created sub threads when it exits. Before the main thread exits, it can continue to run independently from the main thread. After the task is executed, the sub thread will automatically release the system resources it occupies. (in fact, the child's wings are hard, he breaks off relations with his family and wanders around. If the family is killed, the nine tribes will still be involved). The prototype of this function is as follows:

void detach();

The thread separation function has no parameters and no return value. Just call the function through the thread object after the thread succeeds. Continue to modify the above test program:

int main()
{
    cout << "Thread of main thread ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "thread  t Thread ID: " << t.get_id() << endl;
    cout << "thread  t1 Thread ID: " << t1.get_id() << endl;
    t.detach();
    t1.detach();
    // Let the main thread sleep and wait for the child thread to finish executing
    this_thread::sleep_for(chrono::seconds(5));
}

Compile output:
Note:
Note: the thread separation function detach () will not block the thread. After the sub thread is separated from the main thread, you can no longer control the sub thread in the main thread. For example, block the main thread through join() and wait for the task in the sub thread to complete, or call get_id() gets the thread ID of the child thread. Advantages have disadvantages. You can't have both fish and bear's paw. It is recommended to use join ().

2.4 joinable()

It is used to judge whether join() or detach() can be called. It can return true but not false
The example code is as follows:

// C++ program to demonstrate the use of 
// std::thread::joinable() 
  
#include <chrono> 
#include <iostream> 
#include <thread> 
  
using namespace std; 
  
// function to put thread to sleep 
void threadFunc() 
{ 
    std::this_thread::sleep_for( 
        std::chrono::seconds(1)); 
} 
  
int main() 
{ 
    std::thread t1; // declaring the thread 
  
    cout << "t1 joinable when default created? \n"; 
    // checking if it is joinable 
    if (t1.joinable()) 
        cout << "YES\n"; 
    else
        cout << "NO\n"; 
  
    // calling the function threadFunc 
    // to put thread to sleep 
    t1 = std::thread(threadFunc); 
  
    cout << "t1 joinable when put to sleep? \n"; 
  
    // checking if t1 is joinable 
    if (t1.joinable()) 
        cout << "YES\n"; 
    else
        cout << "NO\n"; 
  
    // joining t1 
    t1.join(); 
  
    // checking joinablity of t1 after calling join() 
    cout << "t1 joinable after join is called? \n"; 
    if (t1.joinable()) 
        cout << "YES\n"; 
    else
        cout << "NO\n"; 
  
    return 0; 
}

Compile output:
Threads cannot be joined when:

  • It is constructed by default
  • If any of its members join or detach has been called
  • It has been moved elsewhere

2.5 operator=

The resources in the thread cannot be copied, so the assignment operation through the = operator will not get two identical objects in the end.

// move (1)	
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)	
thread& operator= (const other&) = delete;

You can know from the overload declaration of the = operator above:

  • If other is an R-value, resource ownership will be transferred
  • If other is not an R-value, copying is prohibited, and the function is displayed for deletion (= delete), which is not available

3. Static function

The thread thread class also provides a static method to obtain the number of CPU cores of the current computer. According to this result, an equal number of threads are created in the program. Each thread occupies a CPU core alone. These threads do not need to reuse CPU time slices in time-sharing. At this time, the concurrency efficiency of the program is the highest.

static unsigned hardware_concurrency() noexcept;

The example code is as follows:

#include <iostream>
#include <thread>
using namespace std;

int main()
{
    int num = thread::hardware_concurrency();
    cout << "CPU number: " << num << endl;
}

Tags: C++ Back-end C++11

Posted on Mon, 25 Oct 2021 08:13:34 -0400 by bhoward3