Strange keywords encountered in c + + thread pool and some functions of new features of C++11

Recently, I learned thread pool by myself. After reading other people's codes, I found that some functions and some strange keywords in the new features of C++11 are used in many codes. In addition, I don't know what this function is. As a result, I don't know how to understand it. After some reference, I understand something. I hereby record it.

1, Basic usage of variable parameter template function

  • Variable parameter templates can create template functions and template classes with any number of parameters

1) Variable parameter template function declaration and definition

template<typename... Args>  //Args is a template parameter package
void Show(Args... args)     //args is a function parameter package
{
    //Function function
    return;
}
  • Variable parameter template function is generally used for variable parameter output. The call of variable parameter args cannot be called in args[2], and recursive expansion and non recursive expansion can be adopted.

2) Variable parameter template function call example

  • Recursive expansion
void showList(){
    cout<<"empty~"<<endl;
}
template<typename T,typename...Args>
void showList(T value,Args...args){
    cout<<value<<endl;
    showList(args...);
}
	call	showList(5,'L',1.1);
	
	output		5
				'L'
				1.1
				empty~
  • Non recursive expansion
template<typename T>
void Print(T arg)
{
    cout<<arg<<endl;
}
template<typename ... Args>
void showList2(Args ... args)
{
    int a[]={(Print(args),0)...};
}
	call	showList2(5,'L',1.1);
	
	output		5
				'L'
				1.1

Generally speaking, the recursive call method is safe and controllable, but the call times of non recursive call method are difficult to control.

2, inline, explicit, implicit keywords

1)inline

  • In c/c + +, in order to solve the problem that some frequently called small functions consume a lot of stack space (stack memory), the inline modifier is specially introduced, which is expressed as an inline function. The stack space refers to the memory space where the local data of the program (that is, the data in the function) is placed. In the system, the stack space is limited. If it is used frequently, it will cause the problem of program error due to insufficient stack space. For example, the final result of function dead loop recursive call is to lead to the depletion of stack memory space.
// The function is defined as inline, that is, inline function
inline char* inline_test(int num) 
{
    return (num % 2 > 0) ? "odd" : "even";
}
 
 
int main()
{
   int i = 0;
   for (i = 1; i < 10; i++) 
   {
       printf("inline_test:   i:%d   Parity:%s\n", i, inline_test(i));   
   }
   
   return 0;
}

The above example is the use of standard inline functions. We can't see the benefits of using inline modification. In fact, the internal work is to call inline inside each for loop_ Test (I) is replaced by (i%2 > 0) " Odd ":" even ", which avoids the consumption caused by repeated development of stack memory by frequent function calls.

  • The use of inline is limited. Inline is only suitable for simple culvert numbers in culvert numbers.
    (1) It cannot contain complex structural control statements, such as while and switch, and the inline function itself cannot be a direct recursive function (that is, it also calls its own function internally).
    (2) All virtual functions (except the most ordinary ones, which do almost nothing) try to prevent inlining. This should not cause too much surprise, because virtual means "wait until the execution time to determine which function should be called", while inline means "in the compilation stage, the calling action will be replaced by the body of the called function ". if the compiler doesn't know which function to call when it makes a decision, it's hard to ask them to make an inline function.
  • inline is a "keyword for implementation", not a "keyword for declaration"
inline void Foo(int x, int y); // inline is only placed with the function declaration
 
void Foo(int x, int y){}

The Foo function in the above code cannot be an inline function, while the following code can

void Foo(int x, int y);
 
inline void Foo(int x, int y) {} // inline is placed with the function definition body
  • Inline can improve the execution efficiency of functions. Why not define all functions as inline functions? If all functions are inline functions, do you still need the keyword "inline"?

  • Inlining is code expansion (replication) At the cost of, it only saves the cost of function call, so as to improve the execution efficiency of the function. If the time of executing the code in the function body is greater than the cost of function call, the efficiency gain will be less. On the other hand, each call of inline function needs to copy the code, which will increase the total code of the program and consume more memory space.

  • Inline should not be used in the following cases:

    • (1) If the code in the function body is long, the use of inlining will lead to high memory consumption.
    • (2) If there is a loop in the function body, the time to execute the code in the function body is more expensive than the function call.
    • (3) Class constructors and destructors are easily misunderstood as being more effective using inlining. Be careful that constructors and destructors may hide some behaviors, such as "secretly" executing the constructors and destructors of base classes or member objects. Therefore, do not casually put the definitions of constructors and destructors in class declarations.

2)explicit,implicit

  • Let's first review what explicit and implicit transformations are

  •  - Explicit conversion:  
     		person p1; 		
     		person p2 = person(10); 		
     		person p3 = person(p2);
     - Implicit conversion:  
     		person p4 = 10;(amount to person  p4 = person(10))
     		person p5 = p4; 
    
    • The explicit keyword in C + + can only be used to modify a class constructor with only one parameter. Its function is to indicate that the constructor is explicit rather than implicit. Another keyword corresponding to it is implicit, which means hidden. The class constructor is declared implicit by default
class Test{
	public:
		Test(int age){
			```
			}
	private:
		int age;
};

This class is implicit by default and can be converted explicitly and implicitly

class Test{
	public:
		explicit Test(int age){
			```
			}
	private:
		int age;
};

This class is explicit by default and can only be explicitly converted

  • The explicit keyword is used to prevent implicit automatic conversion of class constructors. It is only valid for class constructors with one parameter. If the class constructor parameters are greater than or equal to two, implicit conversion will not occur, so the explicit keyword is invalid.
class Test{
	public:
		explicit Test(int age,string name){
			```
			}
	private:
		int age;
		string name;
};

In this way, even if the constructor defines explicit, it is invalid. However, there is an exception, that is, when all parameters except the first parameter have default values, the explicit keyword is still valid. At this time, when calling the constructor, only one parameter is passed in, which is equivalent to a class constructor with only one parameter

class Test{
	public:
		explicit Test(int age,string name="ccc"){
			```
			}
		/*Or that's OK
		explicit Test(int ag):age(ag),name("ccc"){
			```
			}		
		*/
	private:
		int age;
		string name;
};

3, std::function and std::bind

1)std::bind

The header file of std::bind is a function adapter that accepts a callable object and generates a new callable object to "adapt" the parameter list of the original object.
std::bind binds the callable object with its parameters. The bound results can be saved using std::function.

  • std::bind has two main functions:

    • Bind the callable object and its parameters into an anti function;
    • Bind only some parameters to reduce the parameters passed in by callable objects.
  • 1. std::bind binding ordinary function

double my_divide (double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2);  
std::cout << fn_half(10) << '\n';                        // 5
  • The first parameter of bind is the function name, which will be implicitly converted into a function pointer when ordinary functions are used as arguments. Therefore, std::bind(my_divide,_1,2) is equivalent to STD:: bind (& my_divide, _1,2);

  • _1 represents a placeholder, located in, std::placeholders::_1;

  • 2. Member function of std::bind binding class

class Foo {
public:
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};
int main() 
{
    Foo foo;
    auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
    f(5); // 100
}
  • When bind binds a class member function, the first parameter represents the pointer of the member function of the object, and the second parameter represents the address of the object.
  • The specified & Foo::print_sum must be displayed. Because the compiler will not implicitly convert the member function of the object into a function pointer, you must add &; before Foo::print_sum;
  • When using the pointer of an object member function, you must know which object the pointer belongs to, so the second parameter is the address of the object & foo;
  • 3. Parameter binding

bind is called as follows:

auto newCallable=bind(callable,arg_list);
  • The first parameter of bind is a callable object, which refers to the object on which the calling operator () can be used.
  • Commonly used callable objects include functions, function pointers, classes overloaded with function call operators and lambda expressions

arg_list is the parameter list of the calling object, which can contain_ 1, _ Placeholders such as 2 are used to occupy the parameter position of the calling object. The number represents the undetermined placeholder parameter. The placeholder is defined in the namespace placeholders. You can also include parameters of the bound object. arg_list should have as many parameters as the bound object.

int sum(int a, int b, int c) {
    if (a > b)return a + c;
    return b + c;
}
auto add = bind(sum, _1, _2, 10);

In this way, sum is bound to a new object generated by bind that calls sum;
_ 1 represents the first parameter in the new object and is a placeholder. In other words, in fact, this bind will add (1, 2) and be mapped to sum (1, 2, 10). At this time, the add parameter will replace the original placeholder as the parameter calling sum. Of course, the premise is that the two types should match.
For example, add(20,10) actually calls sum(20,10,10), and the result is 30;

The parameter sequence can be changed

#include<iostream>
using namespace std;
using namespace placeholders;
int sum(int a, int b, int c)
{
    if (a > b)return a + c;
    return b + c;
}
int main(void)
{
    auto add  = bind(sum, _1, _2, 10);
    auto add2 = bind(sum, _2, _1, 10);
    int t = add(20, 10), t1 = add2(10, 20);
    cout << t << " " << t1 << endl;
    return 0;
}

bind can also change the order of the original parameters, because when calling a new object, the parameters we pass to the new object are actually the parameters at the position occupied by the placeholder. Therefore, the above call is as follows:

  • When add(20,10), parameter 20 corresponds to placeholder 1 and parameter 10 corresponds to placeholder 2, so the actual call is sum(20,10,10);
  • When add2(10,20), parameter 10 corresponds to placeholder 1 and parameter 20 corresponds to placeholder 2. Therefore, the actual call is sum(20,10,10), which rearranges the parameter order.

Can contain_ 1, _ Placeholders such as 2 are used to occupy the parameter position of the calling object. The number represents the number of undetermined placeholders

void printValue(int value1, int value2, int value3)
{
    cout << value1 << endl;
    cout << value2 << endl;
    cout << value3 << endl;
}
 
 
void testFunc(std::function<void(int, int)> printFunc, int value1, int value2)
{
    printFunc(value1, value2);
}
 
int main()
{
    auto newFunc1 = std::bind(printValue, placeholders::_1, placeholders::_2, 123);
    testFunc(newFunc1, 111, 222); //111 222 123
 
    auto newFunc2 = std::bind(printValue, placeholders::_1, 123, placeholders::_2);
    testFunc(newFunc2, 111, 222);//111 123 222
    system("pause");
}
  • 4. Bind a reference parameter

By default, the parameters of bind that are not placeholders are copied to the callable object returned by bind. However, similar to lambda, sometimes some bound parameters need to be passed by reference, or the type of parameter to be bound cannot be copied.

#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
#include <sstream>
using namespace std::placeholders;
using namespace std;
 
ostream & print(ostream &os, const string& s, char c)
{
    os << s << c;
    return os;
}
 
int main()
{
    vector<string> words{"helo", "world", "this", "is", "C++11"};
    ostringstream os;
    char c = ' ';
    for_each(words.begin(), words.end(), 
                   [&os, c](const string & s){os << s << c;} );
    cout << os.str() << endl;
 
    ostringstream os1;
    // ostream cannot be copied. If you want to pass an object to bind,
    // Instead of copying it, you must use the ref function provided by the standard library
    for_each(words.begin(), words.end(),
                   bind(print, ref(os1), _1, c));
    cout << os1.str() << endl;
}
  • 5. Pointer to member function

Through the following example, get familiar with the definition of pointers to member functions.

#include <iostream>
struct Foo {
    int value;
    void f() { std::cout << "f(" << this->value << ")\n"; }
    void g() { std::cout << "g(" << this->value << ")\n"; }
};
void apply(Foo* foo1, Foo* foo2, void (Foo::*fun)()) {
    (foo1->*fun)();  // call fun on the object foo1
    (foo2->*fun)();  // call fun on the object foo2
}
int main() {
    Foo foo1{1};
    Foo foo2{2};
    apply(&foo1, &foo2, &Foo::f);
    apply(&foo1, &foo2, &Foo::g);
}

Definition of member function pointer: void (Foo::fun)(). The call is the passed argument: & foo:: F;
Fun is a class member function pointer, so the call is to obtain the member function fun by dereference, that is, (foo1 - > * fun) ();

2)std::function

  • Class template std::function is a universal polymorphic function wrapper.
  • The instance of std::function can store, copy and call any callable object - function, lambda expression and bind
    Expressions or other function objects, as well as pointers to member functions and data members.
  • It is also a type safe package for existing callable entities in C + + (relatively speaking, the call of function pointers is not type safe)
  • Function < T > F is an empty function used to store callable objects. The call form of these callable objects should be the same as the function type T (that is, t is retType(args))
  • std::function can replace the function pointer because it can delay the execution of the function. It is especially suitable for use as a callback function. It is more flexible and convenient than ordinary function pointers.
#include <functional>
#include <iostream>
 
struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 
struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};
 
int main()
{
    // Storage free function
    std::function<void(int)> f_display = print_num;
    f_display(-9);
 
    // Storage lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();
 
    // The result stored in the std::bind call
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();
 
    // Calls stored to member functions
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);
 
    // Calls stored to data member accessors
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';
 
    // Calls stored in member functions and objects
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
    f_add_display2(2);
 
    // Calls stored to member functions and object pointers
    std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
    f_add_display3(3);
 
    // Calls stored in function objects
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
}
//output
-9
42
31337
314160
314160
num_: 314159
314161
314162
18


3) Application between std::function and std::bind

#include <iostream>
#include <functional>
 
void test1(){std::cout<<"function"<<std::endl;}
 
int test2(int i){ return i; }
 
int test3(int i, int j){ return i+j; }
 
struct A{
    void foo(int i){ std::cout<<i<<std::endl; }
};
 
int main() {
    std::function<void()> fn1 = std::bind(test1);
    std::function<int(int)> fn2 = std::bind(test2, std::placeholders::_1);
    std::function<int(int, int)> fn3 = std::bind(test3, std::placeholders::_1, std::placeholders::_2);
    std::function<int(int)> fn4 = std::bind(test3, 3, std::placeholders::_1);
    std::function<int()> fn5 = std::bind(test3, 3, 4);
 
    A a;
    std::function<void(int)> fn6 = std::bind(&A::foo, &a, std::placeholders::_1);
 
    fn1();
    std::cout<<fn2(1)<<std::endl;
    std::cout<<fn3(2, 3)<<std::endl;
    std::cout<<fn4(3)<<std::endl;
    std::cout<<fn5()<<std::endl;
    fn6(8);
}
//output
function
1
5
6
7
8

4, Implement thread pool

  • Thread pool is mainly required for these points:
    • 1) Thread pool manager: initialize and create threads, start and stop threads, allocate tasks, and manage thread pools
    • 2) Worker thread: wait in the thread pool and execute the assigned task
    • 3) Task interface: an interface for adding tasks. It provides a worker thread to schedule the execution of tasks
    • 4) Task queue: used to store unprocessed tasks and provide a buffer mechanism
    • 5) Task scheduling requires locking and mutual exclusion
    • 6) Wake up a thread every time you add a new task
    • 7) When the thread pool is destructed, wake up all threads, and then terminate the recycling of resources

1) Relatively easy to understand version

This version of thread pool does not implement thread priority scheduling

#ifndef _THREAD_POOL_2__
#define _THREAD_POOL_2__
#include<thread>
#include<mutex>
#include<condition_variable>
#include<atomic>
#include<vector>
#include<queue>
#include<functional>

class ThreadPool{
public:
    using Task=std::function<void()>;
    //Number of initialized thread pools
    explicit ThreadPool(int num):_threadNum(num),_threadPoolStatus(false){}
    //Start the thread pool and put all threads into the thread container
    void start(){
        _threadPoolStatus=true;
        for(int i=0;i<_threadNum;i++){
            _threads.emplace_back(std::thread(&ThreadPool::work,this));
        }
    }
    //Task interface: add a task interface to provide work threads to schedule the execution of tasks
    void appendTask(const Task&task){
        if(_threadPoolStatus){
            std::unique_lock<std::mutex>jk(_mutex);
            _tasks.push(task);
            _cond.notify_one();
        }
    }
    //Close thread pool
    void stop(){
        {
            std::unique_lock<std::mutex>jk(_mutex);
            _threadPoolStatus=false;
            _cond.notify_all();
        }
        for(auto&s:_threads){
            if(s.joinable())    s.join();
        }
    }
    //Destroy thread pool
    ~ThreadPool(){
        if(_threadPoolStatus) stop();
    }
private:
    //Worker thread: wait in the thread pool and execute the assigned task
    void work(){
        printf("begin work thread:%d\n",std::this_thread::get_id());
        while(_threadPoolStatus){
            Task task;
            {
                std::unique_lock<std::mutex>jk(_mutex);
                if(_threadPoolStatus&&!_tasks.empty()){
                    task=_tasks.front();
                    _tasks.pop(); 
                }else if(_threadPoolStatus&&_tasks.empty()){
                    _cond.wait(jk);
                }  
            }
           if(task) task();
        }
         printf("end work thread:%d\n",std::this_thread::get_id());
    }

    ThreadPool(const ThreadPool&)=delete;//Copy copy prohibited
    ThreadPool&operator=(const ThreadPool&)=delete;

    std::atomic_bool _threadPoolStatus;//Thread state
    std::mutex _mutex;
    std::condition_variable _cond;
    std::vector<std::thread>_threads;//Thread container: stores the number of threads
    std::queue<Task> _tasks;//Task queue: used to store unprocessed tasks and provide a buffer mechanism
    int _threadNum;//Number of threads
};
#endif
#include<iostream>
#include<chrono>
#include"threadPool2.h"

void printfx(int x){
    std::cout<<"Task:"<<x<<" work in thread"<<std::this_thread::get_id()<<std::endl;
}
int main(int argc,char*argv[]){
    ThreadPool tpool(3);
    tpool.start();
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    for(int i=0;i<6;i++){
        tpool.appendTask(std::bind(printfx,i));
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    tpool.stop();
    return 0;
}

2) A version that is relatively difficult to understand

Because I haven't fully understood it, I won't put it up first. I'll put it up in a day or two~

Continuous updating~~~

Tags: C++ thread pool C++11

Posted on Sun, 07 Nov 2021 12:20:09 -0500 by cdpapoulias