使用C++11新特性构建一个线程池

目录: 

1.原子变量

2.异步操作

3.回调方法

4.可变模板参数

1.原子变量

概念:原子变量是一种特殊类型的变量,它可以保证在多线程并发访问时,对该变量的操作是原子性的,即不会被中断或干扰。在并发编程中,原子变量可以用来解决多线程竞争的问题,避免出现竞态条件(Race Condition)的情况。

在 Linux 中,原子变量通常使用 atomic_t 类型定义。atomic_t 类型是一个整型变量,支持原子操作,包括读取、赋值、加法、减法等。

#include <linux/atomic.h>

atomic_t count = ATOMIC_INIT(0);  // 初始化为 0

void add_count(int n)
{
    atomic_add(n, &count);  // 原子加法操作
}

在上述代码中,ATOMIC_INIT(0) 表示将 count 初始化为 0,atomic_add(n, &count) 表示对 count 进行原子加法操作,将其加上 n

原子变量的操作是原子性的,即在多线程并发访问时,每个线程对原子变量的操作都是不可分割的,不会被其他线程中断或干扰。因此,原子变量可以用来实现多线程并发访问的同步和互斥,避免出现竞态条件的情况。

优缺点:

原子变量的优点是它可以保证在多线程并发访问时数据的一致性和正确性。原子变量的操作是原子的,即在执行过程中不会被其他线程干扰,因此可以保证线程安全。

另外,原子变量的使用也可以提高程序的性能,因为它不需要使用锁来保证线程安全,避免了锁的开销和竞争

然而,原子变量的缺点是它只能保证单个变量的原子性操作,对于多个变量之间的操作仍然需要使用锁来保证线程安全。此外,在某些情况下,原子变量的使用会导致一些难以调试的问题,比如死锁等。因此,在使用原子变量时需要谨慎,考虑清楚是否真正需要使用它。

运行一下这段代码(几次,对比结果)就清楚了:

#include <iostream>       
#include <atomic>         
#include <thread>         

//std::atomic<int> count = 0;//错误初始化
std::atomic<int> count(0); // 准确初始化

void set_count(int x)
{
    std::cout << "set_count:" << x << std::endl;
    count.store(x, std::memory_order_relaxed);     //赋值store 
}

void print_count()
{
    int x;
    do {
        std::cout << "wait" << std::endl;
        x = count.load(std::memory_order_relaxed);  // 加载数据
    } while (x == 0);
    std::cout << "count: " << x << '\n';
}

int main()
{
    std::thread t1(print_count);
    std::thread t2(set_count, 10);
    t1.join();
   t2.join();
    std::cout << "main finish\n";
    return 0;
}
//这两个线程不会有竞态的结果

2.异步操作

(1)std::aysnc和std::future 类

使用特性:std::future期待一个返回,从一个异步调用的角度来说,future更像是执行函数的返回值,C++标准库 使用std::future为一次性事件建模,如果一个事件需要等待特定的一次性事件,那么这线程可以获取一个future对象来代表这个事件。 异步调用往往不知道何时返回,但是如果异步调用的过程需要同步,或者说后一个异步调用需要使用前一个异步调用的结果。这个时候就要用到future。线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务,一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。

future的类型: 在库的头文件中声明了两种future,唯一future(std::future)和共享future(std::shared_future),这两个是参照std::unique_ptr和std::shared_ptr设立的,前者的实例是仅有的一个指向其关联事件的实例,而后者可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪。

使用场景: std::future是一个模板,模板参数就是期待返回的类型。虽然future被用于线程间通 信,但其本身却并不提供同步访问,必须通过互斥元或其他同步机制来保护访问。 future使用的时机是当你不需要立刻得到一个结果的时候,你可以开启一个线程帮你去做一项任务,并期待这个任务的返回,但是std::thread并没有提供这样的机制,这就需要用到std::async和std::future (都在头文件中声明) std::async返回一个std::future对象,而不是给你一个确定的值(所以当你不需要立刻使用此值的时候才需要用到这个机制)。当你需要使用这个值的时候,对future使用get(),线程就会阻塞直到future就绪,然后返回该值。

看以下代码:

#include <iostream>
#include <future>

int main() {
    std::future<int> future_result = std::async([]() {
        std::cout << "子线程开始执行" << std::endl;
        return 42;
        });

    // 主线程继续执行其他操作

    int result = future_result.get();
    std::cout << "子线程返回值为:" << result << std::endl;

    return 0;
}

我们使用 std::async 函数创建了一个异步任务,并将其返回值封装在 std::future 对象中。异步任务的实现是一个 lambda 表达式,它会输出一条信息并返回整数值 42。

在主线程中,我们可以继续执行其他操作,而不必等待异步任务完成。当需要获取异步任务的结果时,我们可以调用 std::future::get 函数,它会等待异步任务完成并返回其结果。

需要注意的是,在调用 get 函数之前,异步任务可能还没有完成,因此 get 函数会阻塞当前线程直到异步任务完成并返回结果。如果异步任务抛出了异常,get 函数也会抛出相应的异常。

除了上面的示例中演示的方式,还可以使用 std::async 函数的第一个参数指定异步任务的启动策略,例如 std::launch::async 表示在新线程中执行异步任务,而 std::launch::deferred 表示延迟执行异步任务直到调用 get 函数时。

另外需要注意的是,std::future 对象只能获取一次异步任务的结果。如果需要多次获取异步任务的结果,可以使用 std::shared_future 对象。

(2)std::packaged_task 类

如果说std::async和std::future还是分开看的关系的话,那么std::packaged_task就是将任务和future 绑定在一起的模板,是一种对任务的封装。

可以通过std::packaged_task对象获取任务相关联的future,调用get_future()方法可以获得 std::packaged_task对象绑定的函数的返回值类型的future。std::packaged_task的模板参数是函数签 名。 PS:例如int add(int a, intb)的函数签名就是int(int, int)

自己运行以下代码就会了。

#include <iostream>
#include <future>

using namespace std;

int add(int a, int b, int c)
{
	std::cout << "call add\n";
	return a + b + c;
}
void do_other_things()
{
	std::cout << "do_other_things" << std::endl;
}
int main()
{
	std::packaged_task<int(int, int, int)> task(add); // 封装任务
	do_other_things();
	std::future<int> result = task.get_future();//等待返回结果
	add(1, 1, 2); //必须要让任务执行,否则在get()获取future的值时会一直阻塞
	std::cout << "result:" << result.get() << std::endl;
	return 0;
}

(3) std::promise

std::promise 是 C++11 标准库中的一个类,它提供了一种通信机制,可以让一个线程向另一个线程传递一个值或一个异常。它通常与 std::future 一起使用,std::future 可以获取 std::promise 传递的值或异常。

在使用 std::promise 时,我们可以调用 std::promise 的 set_value 成员函数来设置一个值,或者调用 set_exception 成员函数来设置一个异常。当调用 set_value 或 set_exception 后,与 std::promise 相关联的 std::future 会被通知,并且可以获取到 set_value 或 set_exception 设置的值或异常。

下面是一个简单的例子,演示了如何使用 std::promise 和 std::future

#include <iostream>
#include <future>

void compute(std::promise<int>& result_promise, int arg) {
    // 计算结果
    int result = arg * 2;

    // 设置结果值
    result_promise.set_value(result);
}

int main() {
    // 创建一个 promise 和一个 future
    std::promise<int> result_promise;
    std::future<int> result_future = result_promise.get_future();

    // 启动一个新线程计算结果
    std::thread t(compute, std::ref(result_promise), 10);

    // 获取结果
    int result = result_future.get();

    // 输出结果
    std::cout << "Result: " << result << std::endl;

    // 等待线程结束
    t.join();

    return 0;
}

std::promise提供了一种设置值的方式,它可以在这之后通过相关联的std::future对象进行读取。换种说法,之前已经说过std::future可以读取一个异步函数的返回值了,那么这个std::promise就提供一种方式手动让future就绪。

线程在创建promise的同时会获得一个future,然后将promise传递给设置他的线程,当前线程则持有 future,以便随时检查是否可以取值。future的表现为期望,当前线程持有future时,期望future获取到想要的结果和返回,可以把future当做异步函数的返回值。而 promise是一个承诺,当线程创建了promise对象后,这个promise对象向线程承诺他必定会被人设置一 个值,和promise相关联的future就是获取其返回的手段。

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

using namespace std;

void print(std::promise<std::string>& p)
{
	p.set_value("There is the result which you want.");
}
void do_some_other_things()
{
	std::cout << "Hello World" << std::endl;
}
int main()
{
	std::promise<std::string> promise;
	std::future<std::string> result = promise.get_future();

	std::thread t(print, std::ref(promise));
	do_some_other_things();
	std::cout << result.get() << std::endl;
	t.join();
	return 0;
}

3.回调函数:function和bind用法

在设计回调函数的时候,无可避免地会接触到可回调对象。在C++11中,提供了std::function和 std::bind两个方法来对可回调对象进行统一和封装。 C++语言中有几种可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用 运算符的类。 和其他对象一样,可调用对象也有类型。例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定。

(1)function的用法

包含头文件#include<functional>

//保存普通函数
void func1(int a)
{
cout << a << endl;
}
//1. 保存普通函数
std::function<void(int a)> func;
func = func1;
func(2); //2
//保存lamba表达式
std::function<void()> func_1 = [](){cout << "hello world" << endl;};
func_1(); //hello world
//保存成员函数
class A{
public:
A(string name) : name_(name){}
void func3(int i) const {cout <<name_ << ", " << i << endl;}
private:
string name_;
};
//3 保存成员函数
std::function<void(const A&,int)> func3_ = &A::func3;
A a("name");
func3_(a, 1);

运行这段代码就清楚了

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

//保存普通函数
void func1(int a)
{
	cout << a << endl;
}
//保存成员函数
class A {
public:

	A(string name) : name_(name) {}
	void func3(int i) const { cout << name_ << ", " << i << endl; }
private:
	string name_;
};
int main()
{
	cout << "main1 -----------------" << endl;
	//1. 保存普通函数
	std::function<void(int a)> func1_;
	func1_ = func1;
	func1_(2); //2

	cout << "\n\nmain2 -----------------" << endl;
	//2. 保存lambda表达式
	std::function<void()> func2_ = []() {cout << "hello lambda" << endl; };
	func2_(); //hello world
	cout << "\n\nmain3 -----------------" << endl;
	
	//3 保存成员函数
	std::function<void(const A&, int)> func3_ = &A::func3;
	A a("name");
	func3_(a, 1);
	return 0;
}

(2) bind用法

可将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适 应”原对象的参数列表。 调用bind的一般形式:auto newCallable = bind(callable, arg_list); 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的 参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。 arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参 数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:1为 newCallable的第一个参数,_2为第二个参数,以此类推。

4.可变模板参数

C++11的新特性--可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。

(1). 可变参数模板语法

template <class... T>
void f(T... args);

上面的参数args前面有省略号,所以它就是一个可变模版参数。我们把带省略号的参数称为“参数包”, 它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。 可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变模版参数函数和可变模版参数类。然而,模版函数不支持偏特化,所以可变模版参数函数和可变模版参数类展开可变模版参数的方法还不尽相同。

上面的可变模版参数的定义当中,省略号的作用有两个: 1. 声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数; 2. 在模板定义的右边,可以将参数包展开成一个一个独立的参数。

这样说,比较抽象,但是这是可变参数模板的概念,具体的看以下的代码就可以明白上面的概念

//一个简单的可变模版参数函数
#include <iostream>
using namespace std;
template <class... T>
void f(T... args)
{
	cout << sizeof...(args) << endl; //打印变参的个数
}
int main()
{
	f(); //0
	f(1, 2); //2
	f(1, 2.5, ""); //3
	f(1, 2, 3, 4);//4
	return 0;
}

上面的例子中,f()没有传入参数,所以参数包为空,输出的size为0,后面三次调用分别传入两个,三个和4个参数,故输出的size分别为2,3和4。由于可变模版参数的类型和个数是不固定的,所以我们可以传任意类型和个数的参数给函数f。这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。

展开可变模版参数函数的方法一般有两种: 1. 通过递归函数来展开参数包, 2. 是通过逗号表达式来展开参数包。下面来看看如何用这两种方法来展开参数包:

(1)递归函数方式展开参数包

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归的。参考以下的代码例子:

//递归函数方式展开参数包
#include <iostream>
using namespace std;

//递归终止函数
void print()
{
	cout << "empty" << endl;
}

//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
	cout << "parameter " << head << endl;
	print(rest...);
}
int main(void)
{
	print(1, 2, 3, 4);
	return 0;
}

上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一 个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一 个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。

上面的递归终止函数还可以写成这样:

template <class T>
void print(T t)
{
cout << t << endl;
}

(2)逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归,这样可能会感觉稍有不便。有没有一种更简单的方 式呢?其实还有一种方法可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

// 逗号表达式展开参数包
#include <iostream>
using namespace std;
template <class T>
void printarg(T t)
{
	cout << t << endl;
}
template <class ...Args>
void expand(Args... args)
{
	int arr[] = { (printarg(args), 0)... };
}
int main()
{
	expand(1, 2, 3, 4);
	return 0;
}

这个例子将分别打印出1,2,3,4四个数字。这种展开参数包的方式,不需要通过递归终止函数,是直接在 expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。 expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。

同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最 终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。我们可以把上面的例子再进一步改进一 下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:

#include <iostream>
using namespace std;
template<class F, class... Args>void expand(const F& f, Args&&...args)
{
	//这里用到了完美转发
	initializer_list<int>{(f(std::forward< Args>(args)), 0)...};
}
int main()
{
	expand([](int i) {cout << i << endl; }, 1, 2, 3);
	return 0;
}

以上的这篇文章及上篇文章中讲解的C++11特性,是写一个线程池的所有技术点。当你学完了这两篇文章中的技术点,那么,你就可以看下一篇的线程池实现了!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值