c++11 multithreaded programming (II) -- in depth understanding of thread class constructor

Arguments to constructor

The constructor of std::thread class is implemented using a variable parameter template, that is, any parameter can be passed. The first parameter is the entry function of the thread, and the following parameters are the parameters of the function.

The type of the first parameter is not the function pointer in c language (function pointers are used in c language transfer functions). In c++11, the concept of callable objects is added. In general, callable objects can be in the following cases:

  • Function pointer
  • The class object that overloads the operator() operator, that is, the functor
  • lambda expression (anonymous function)
  • std::function

Function pointer example

// Ordinary functions have no parameters
void function_1() {
}

// Ordinary function 1 parameter
void function_2(int i) {
}

// Ordinary function 2 Parameters
void function_3(int i, std::string m) {
}

std::thread t1(function_1);
std::thread t2(function_2, 1);
std::thread t3(function_3, 1, "hello");

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

A problem was also found in the experiment. If the overloaded function is used as the entry function of the thread, a compilation error will occur! The compiler does not know which function it is, such as the following code:

// Ordinary functions have no parameters
void function_1() {
}

// Ordinary function 1 parameter
void function_1(int i) {
}
std::thread t1(function_1);
t1.join();
// Compilation error
/*
C:\Users\Administrator\Documents\untitled\main.cpp:39: 
error: no matching function for call to 'std::thread::thread(<unresolved overloaded function type>)'
     std::thread t1(function_1);
                              ^
*/

functor

// functor 
class Fctor {
public:
    // Has one parameter
    void operator() () {

    }
};
Fctor f;
std::thread t1(f);  
// std::thread t2(Fctor()); //  Compilation error 
std::thread t3((Fctor())); // ok
std::thread t4{Fctor()}; // ok

An object generated by an imitation function class is used like a function, such as the above object F. when f() is used, the operator() operator is called. Therefore, you can also make it the first parameter of the thread class. If this imitation function has parameters, it can also be written on the last few parameters of the thread class.

The compilation error of t2 is because the compiler does not interpret Fctor() as a temporary object, but as a function declaration. The compiler thinks that you have declared a function, which does not accept parameters and returns a Factor object. The solution is to wrap Factor() with a layer of parentheses (), or use {} when calling the constructor of std::thread, which is a new consent initialization syntax in c++11.

However, if the overloaded operator() has parameters, the above error will not occur.

Anonymous function

std::thread t1([](){
    std::cout << "hello" << std::endl;
});

std::thread t2([](std::string m){
    std::cout << "hello " << m << std::endl;
}, "world");

std::function

class A{
public:
    void func1(){
    }

    void func2(int i){
    }
    void func3(int i, int j){
    }
};

A a;
std::function<void(void)> f1 = std::bind(&A::func1, &a);
std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);

std::thread t1(f1);
std::thread t2(f2);
std::thread t3(f3, 1);
std::thread t4(f4, 1);
std::thread t5(f5, 1, 2);

Pass value or reference

First, ask a question: if the parameter of the thread entry function is a reference type, will the variable of the main thread change if the variable is modified inside the thread?

The code is as follows:

#include <iostream>
#include <thread>
#include <string>

// functor 
class Fctor {
public:
    // One parameter is a reference
    void operator() (std::string& msg) {
        msg = "wolrd";
    }
};



int main() {
    Fctor f;
    std::string m = "hello";
    std::thread t1(f, m);

    t1.join();
    std::cout << m << std::endl;
    return 0;
}

// vs next: finally: "hello"
// g + + compiler: compilation error

In fact, the code compiled with g + + will report an error, while using vs2015 will not report an error, but the sub thread did not successfully change the external variable m.

I think so: the std::thread class also has several variables inside. When using the constructor to create an object, the parameters are assigned to these variables first, so these variables are only copies. Then, when the thread starts and calls the thread entry function, the parameters passed are only copies, so the internal operation is to change the copies, Without affecting the external variables. g + + may be relatively strict. This writing method may lead to serious errors in the program, which is simply prohibited.

If you want to really pass the reference, you can wrap it with std::ref() when calling the thread class constructor. As shown in the modified code below:

std::thread t1(f, std::ref(m));

Then both vs and g + + can be compiled successfully, and the child thread can modify the value of external variables.

Of course, this is not good. If multiple threads modify the same variable at the same time, data competition will occur.

Similarly, the first parameter of the constructor is the callable object. By default, a copy is passed.

#include <iostream>
#include <thread>
#include <string>

class A {
public:
    void f(int x, char c) {}
    int g(double x) {return 0;}
    int operator()(int N) {return 0;}
};

void foo(int x) {}

int main() {
    A a;
    std::thread t1(a, 6); // 1. Copy is called_ of_ a()
    std::thread t2(std::ref(a), 6); // 2. a()
    std::thread t3(A(), 6); // 3. The temporary object temp is called_ a()
    std::thread t4(&A::f, a, 8, 'w'); // 4. Copy is called_ of_ a.f()
    std::thread t5(&A::f, &a, 8, 'w'); //5. a.f() is called
    std::thread t6(std::move(a), 6); // 6. Call a.f(), a can no longer be used
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    t6.join();
    return 0;
}

For thread t1, the internally invoked thread function is actually a copy, so if the class member is modified inside the function, the external object will not be affected. It is only modified when the reference is passed. So at this time, you must figure out whether to pass value or reference!

Thread objects can only be moved and cannot be copied

Thread objects cannot be copied and can only be moved. Moving means that the ownership of threads is transferred between std::thread instances.

void some_function();
void some_other_function();
std::thread t1(some_function);
// std::thread t2 = t1; //  Compilation error
std::thread t2 = std::move(t1); //You can only move t1. There are no threads inside
t1 = std::thread(some_other_function); // Temporary object assignment is a move operation by default
std::thread t3;
t3 = std::move(t2); // There are no threads inside t2
t1 = std::move(t3); // The program will terminate because there is already a thread running inside t1

reference resources

  1. C + + concurrent programming practice
  2. C++ Threading #8: Using Callable Objects

Keywords: C++

Added by awpti on Wed, 01 Dec 2021 13:26:43 +0200