原创 lightcity 光城 2020-04-29
C++像Go一样的并发与闭包
1.并发与并行的区分
- 并发的关键是你有处理多个任务的能力,不一定要同时。
- 并行的关键是你有同时处理多个任务的能力。
举例:并发就是一个厕所坑很多人排队交替用,并行就是多个厕所坑多个人用,可以同时。
并发性是程序的一种属性,其中两个或多个任务可以同时进行。并行性是一个运行时属性,其中两个或多个任务同时执行。通过并发性,为程序定义一个适当的结构。并发可以使用并行来完成它的工作,但并行不是并发的最终目标。
2.Go的优雅写法
并发主要由切换时间片来实现“同时”运行,在并行则是直接利用多核实现多线程的运行,但 Go 可以设置使用核数,以发挥多核计算机的能力。
Goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
下面是Go的一个简单例子:
import (
"fmt"
"sync"
"time"
)
func main() {
page_req, page_rsp := 1, 1
var wg sync.WaitGroup
wg.Add(2)
go func() {
page_req++
page_rsp++
time.Sleep(time.Millisecond * 2000)
fmt.Println(page_req, page_rsp)
wg.Done()
}()
go func() {
page_req++
page_rsp++
time.Sleep(time.Millisecond * 3000)
fmt.Println(page_req, page_rsp)
wg.Done()
}()
wg.Wait()
}
3.C++写法
我们预期:
TaskList task_list;
task_list += [&]() {return Foo(req,rsp);};
task_list += [&]() {return Bar(req,rsp);};
task_list.ExeAllTask();
或者:
TaskList task_list;
task_list<<[&]() { return Foo(req,rsp); }<<[&]() { return Bar(req,rsp); };
task_list.ExeAllTask();
给它一个任务池,把每个任务放进去,并发的执行。
首先业务逻辑很简答 ,就是只需要实现Foo,Bar函数就行了。
int Foo(const int&req, int&rsp)
{
rsp++;
cout << "doing foo task req: " << req <<" rsp: " <<rsp<< endl;
return 0;
}
int Bar(const int&req, int&rsp)
{
rsp++;
cout << "doing bar task req: " << req<<" rsp: " <<rsp<< endl;
return 0;
}
紧接着我们看到+=
与<<
操作在c++中就是重载操作符,每次塞进去的是个任务,所以我们命名每个任务为Task。还需要一个池子处理这些任务,命名为TaskList,下面就是实现这两个类。
假设每个业务对应的业务函数时个返回值为int,入参为void类型的,那么我们先做一次这样操作:
using TaskFunc = std::function<int(void)>
Task实现:
class Task
{
protected:
const TaskFunc task_func_;
public:
Task(const TaskFunc &func) : task_func_(func)
{
}
int Process()
{
task_func_();
return 0;
}
};
紧接着就是需要一个池子,类似中央调度器,去调度任务,也就是执行每个任务的Process方法。
TaskList实现:
class TaskList
{
public:
std::vector<Task *> task_lists_;
TaskList()
{
}
~TaskList()
{
for (auto &task : task_lists_)
{
delete task;
}
task_lists_.clear();
}
void operator+=(const TaskFunc &task_func)
{
AddTask(task_func);
}
TaskList& operator<<(const TaskFunc& task_func) {
AddTask(task_func);
return *this;
}
void AddTask(const TaskFunc & task_func) {
Task *task = new Task(task_func);
task_lists_.push_back(task);
}
int ExeAllTask()
{
for (auto &task : task_lists_)
{
task->Process();
}
return 0;
}
};
实现起来,重点三个地方。
- 第一个:执行任务ExecAllTask,调用Task的Proccess方法即可
- 第二个:操作符重载,支持连续累加
最后就是前面的调用了:
int main()
{
int req=1,rsp=0;
TaskList task_list;
task_list += [&]() {return Foo(req,rsp);};
task_list += [&]() {return Bar(req,rsp);};
task_list<<[&]() { return Foo(req,rsp); }<<[&]() { return Bar(req,rsp); };
task_list.ExeAllTask();
}
实现起来不算复杂,对比不同语言的实现方式,有利于不断提升。
4.Go闭包
a closure is a record storing a function together with an environment.
闭包是由函数和与其相关的引用环境组合而成的实体 。
闭包究竟包了什么?
- 函数
- 指的是在闭包实际实现的时候,往往通过调用一个外部函数返回其内部函数来实现的。内部函数可能是内部实名函数、匿名函数或者一段lambda表达式。用户得到一个闭包,也等同于得到了这个内部函数,每次执行这个闭包就等同于执行内部函数。
- 环境
- 与其(函数)相关的引用环境
验证一下传递引用与非引用的区别,对上述环境的影响。
func foo_1(x *int) func() {
return func() {
*x = *x + 1
fmt.Printf("foo_1 val = %d\n", *x)
}
}
func foo_2(x int) func() {
return func() {
x = x + 1
fmt.Printf("foo_1 val = %d\n", x)
}
}
调用:
x := 10
f_1 := foo_1(&x)
f_2 := foo_2(x)
f_1() // 11
f_1() // 12
f_2() // 11
f_2() // 12
x = 100
f_1() // 101
f_1() // 102
f_2() // 13
f_2() // 14
可以看到前面输出11,12是因为每次调用的时候,因为闭包f_1
与f_2
都保存了x=10
时的环境,每次调用闭包f_1
与f_2
都会执行一次自增+打印的内部匿名函数。所以输出是11与12。而当修改值为100的时候,由于f_1传递的是引用,在f_1中的x
环境不限制在f_1中,因此会被修改,输出101,102,而f_2则还是之前的值累加。
5.C++像Go一样的闭包
闭包,我们想到了lambda。传入闭包中的元素,必须为其在堆上分配内存,如果以=
值传递,那么在外面得分配好,如果以&
传递,就不需要再外面提前分配了。
#include <functional>
#include <iostream>
#include <memory>
typedef std::function<int(void)> Fun;
Fun Test1() {
// 保证内存在堆上分配及自动回收
std::shared_ptr<int> t = std::make_shared<int>( 0 );
return [=]{
(*t) += 1;
return (*t);
};
}
int main()
{
auto f1 = Test1();
std::cout << f1() << std::endl ;
std::cout << f1() << std::endl;
std::cout << f1() << std::endl;
auto f2 = Test1();
std::cout << f2() << std::endl;
std::cout << f2() << std::endl;
std::cout << f2() << std::endl;
return 0;
}
同Go一样的输出。
有疑问加站长微信联系(非本文作者)