详谈C++11新特性之future及开源项目ananas(folly,std c++11和ananas的future各自的区别是?)(而)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 详谈C++11新特性之future及开源项目ananas(folly,std c++11和ananas的future各自的区别是?)

下面就几种场景展示一下使用ananas future的解决方案。

3.使用场景


3.1 按顺序向多个服务器发起请求:链式调用


服务器需要向redis1拉取玩家基础信息,获得基础信息后,又根据其内容,再向redis2请求获取详细信息。在老式C代码中,使用callback我们一般需要保存上下文,而C++11可以利用shared_ptr和lambda模拟闭包以捕获上下文:


//1. 异步获取基础信息
redis_conn1->Get<BasicProfile>("basic_profile_key")
.Then([redis_conn2](const BasicProfile& data) {
    //2. 处理返回的基础信息,异步获取详细信息                           
    return redis_conn2->Get<DetailProfile>("detail_profile_key"); 
    // it return another future
})
.Then([client_conn](const DetailProfile& data) {
    //3. SUCC 处理返回的详细信息,返回给客户端
    client_conn->SendPacket(data);
})
.OnTimeout(std::chrono::seconds(3), [client_conn]() {
    std::cout << "请求超时了\n";
    //3. FAIL 返回给客户端
    client_conn->SendPacket("server timeout error");
}, &this_event_loop);

第一个Get发起请求,并立即返回,使用Then注册callback处理结果,第一个请求返回后,发起第二个Get请求,当第二个请求返回后,再发送给客户端。其中OnTimeout是处理请求超时的情况,如果3s内任意redis没有返回响应,thiseventloop超时回调,向客户端通知。


3.2 同时向多个服务器发起请求,当所有请求返回后,开始处理


仍然沿用上面的例子,条件改为基础信息和详细信息没有关联,可以同时请求,并都发送给客户端:


//1. 异步获取基础信息和详细信息
auto fut1 = redis_conn1->Get<BasicProfile>("basic_profile_key");
auto fut2 = redis_conn2->Get<DetailProfile>("detail_profile_key");
ananas::WhenAll(fut1, fut2)
.Then([client_conn](std::tuple<BasicProfile, DetailProfile>& results) {
    //2. SUCC 返回给客户端
    client_conn->SendPacket(std::get<0>(results));
    client_conn->SendPacket(std::get<1>(results));
})
.OnTimeout(std::chrono::seconds(3), [client_conn]() {
    std::cout << "请求超时了\n";
    //3. FAIL 返回给客户端
    client_conn->SendPacket("server timeout error");
}, &this_event_loop);

WhenAll将所有future的结果收集起来,只有收集完毕,才会执行回调。


3.3 同时向多个服务器发起请求,当某一个请求返回后,开始处理


假如有3个同样的服务器S1,S2,S3,我们想发起100次请求测试,看哪个服务器响应最快。这是使用WhenAny的场景:


 

struct Statics
    {
        std::atomic<int> completes{0};
        std::vector<int>  firsts;
        explicit Statics(int n) : 
            firsts(n)
        { } 
    };  
    auto stat = std::make_shared<Statics>(3); // 统计每个服务器获得第一的次数 (响应最快)
    const int kTests = 100;
    for (int i = 0; i < kTests; ++ i)
    {   
        std::vector<Future<std::string> > futures;
        for (int i = 0; i < 3; ++ i)
        {   
            auto fut = conn[i]->Get<std::string>("ping");
            futures.emplace_back(std::move(fut));
        }   
        auto anyFut = futures.WhenAny(std::begin(futures), std::end(futures));
        anyFut.Then([stat](std::pair<size_t/* fut index*/, std::string>& result) {
            size_t index = result.first;
            // 本次,index这个服务器的响应最快
            stat->firsts[index] ++; 
            if (stat->completes.fetch_add(1) == kTests - 1) {
                // 100次测试完成 
                int quickest = 0;
                for (int i = 1; i < 3; ++ i)
                {   
                    if (stat->firsts[i] > stat->firsts[quickest])
                        quickest = i;
                }   
                printf("The fast server index is %d\n", quickest);
            }   
        });
    }

当3个请求中有任意一个返回(亦即最快的那个服务器),回调函数执行,统计次数。


最终,次数最多的那个服务器基本就是响应最快的。


3.4.同时向多个服务器发起请求,当其中过半请求返回后,开始处理


典型场景是paxos。在第一阶段,proposer尝试发起预提案prepare;当得到多数派acceptors的承诺回包,才可以发起第二阶段,请求提议一个值给acceptors:


// paxos phase1: Proposer发送prepare给Acceptors
const paxos::Prepare prepare;
std::vector<Future<paxos::Promise> > futures;
for (const auto& acceptor : acceptors_)
{
    auto fut = acceptor.SendPrepare(prepare);
    futures.emplace_back(std::move(fut));
}
const int kMajority = static_cast<int>(futures.size() / 2) + 1;
// 这里用匿名future即可
WhenN(kMajority, std::begin(futures), std::end(futures))
.Then([](std::vector<paxos::Promise>& results) {
    printf("提议成功,收到了多数派acceptors的承诺,现在发起第二阶段propose!\n");
    // paxos phase2: 选择一个值:SelectValue
    const auto value = SelectValue(hint_value);
    // 向acceptors发起提案:
    // foreach (a in acceptors)
    //   a->SendAccept(ctx_id, value); // 使用ctx-id,保证两阶段使用的是同一个提议id号码
})
.OnTimeout(std::chrono::seconds(3), []() {
    printf("prepare超时,也许是失败,请增大提议号重试发起!\n");
    //increase prepareId and  continue send prepare
},
&this_eventloop);

3.5 指定Then回调在特定线程执行


在Herb Sutter的提案中,提到了关于指派Then回调函数在特定线程执行的能力。对此,我捏造了这样的一个例子:


假如服务器需要读一个很大的文件,文件是没有非阻塞读的(先不考虑io_sumbit ),read可能需要数百毫秒的时间。如果采取同步读取,势必造成服务器阻塞。我们希望另外开一个IO线程读取,当IO线程读取完成通知我们。 使用future编写代码如下:

// In this_loop thread.
// 在另外一个线程读取very_big_file
Future<Buffer> ft(ReadFileInSeparateThread(very_big_file));
ft.Then([conn](const Buffer& file_contents) {
    // SUCCESS : process file_content; 
    conn->SendPacket(file_content);
})
.OnTimeout(std::chrono::seconds(3), [=very_big_file]() {
    // FAILED OR TIMEOUT: 
    printf("Read file %s failed\n", very_big_file); 
},
&this_loop);

这样的代码是否存在问题?请注意,对于一个tcp连接,send一般来说都不允许多线程调用。callback中的这行语句


conn->SendPacket(file_content);

是在读文件线程中执行的,因此有多线程调用send的危险。


所以我们需要指定该callback在原来的线程执行,很简单,只需要改动一行,调用另外一个Then的重载:


ft.Then(&this_loop, [conn](const Buffer& file_contents) { ...

注意第一个参数this_loop,这样,SendPacket就将在本线程运行,不存在并发错误了。


4.示例:基于future的redis客户端


前面简单介绍了future使用的各种场景,现在以一个完整的例子结束本文:redis客户端。之所以选择实现redis客户端,一是因为redis应用广泛,大家对它很熟悉;二是redis协议简单,且能保证协议应答的有序性,实现起来难度不大,不至于使大家分散注意力。


4.1协议的发送


对于协议打包,我选择了采用inline协议。利用C++11的变长模板参数可以非常容易做到:

// Build redis request from multiple strings, use inline protocol 
template <typename... Args>
std::string BuildRedisRequest(Args&& ...);
template <typename STR>
std::string BuildRedisRequest(STR&& s)
{
    return std::string(std::forward<STR>(s)) + "\r\n";
}
template <typename HEAD, typename... TAIL>
std::string BuildRedisRequest(HEAD&& head, TAIL&&... tails)
{
    std::string h(std::forward<HEAD>(head));
    return h + " " + BuildRedisRequest(std::forward<TAIL>(tails)...);
}


4.2 协议的发送与上下文维护


redis支持pipeline请求,也就是不必要一应一答。因此我们需要为发送出去的请求保存一个上下文。由于请求和应答是严格有序对应的,一定程度上简化了我们的实现。当发出一个请求,需要为此构造一个Promise,这里简单说一下Promise:promise和future是一一对应的,可以理解为生产者操作promise,为其填充value,而消费者操作future,为其注册回调函数,在获得value时这些回调被执行)。这样api可以返回其对应的future,使用者就可以享用fluent的future接口:

// set name first, then get name.
    ctx->Set("name", "bertyoung").Then(
            [ctx](const ResponseInfo& rsp) {
                RedisContext::PrintResponse(rsp);
                return ctx->Get("name"); // get name, return another future
            }).Then(
                RedisContext::PrintResponse
            );

 

现在定义挂起的请求上下文:

enum ResponseType
{
    None,
    Fine, // redis返回OK
    Error, // 返回错误
    String, // redis返回字符串
};
using ResponseInfo = std::pair<ResponseType, std::string>;
struct Request
{
       std::vector<std::string> request;
       ananas::Promise<ResponseInfo> promise;
}
std::queue<Request> pending_;


每次请求,创建一个Request对象,并加入到pending_队列,queue的先进先出特性和redis协议的有序性配合非常完美:


ananas::Future<ResponseInfo>
RedisContext::Get(const std::string& key)
{
    // Redis inline protocol request
    std::string req_buf = BuildRedisRequest("get", key);
    hostConn_->SendPacket(req_buf.data(), req_buf.size());
    RedisContext::Request req;
    req.request.push_back("get");
    req.request.push_back(key);
    auto fut = req.promise.GetFuture();
    pending_.push(std::move(req));
    return fut;
}

4.3 处理响应


当解析到完整的redis服务器回包,从pending队列中取出头部的promise,设置值即可:


auto& req = pending_.front();

// 设置promise      

req.promise.SetValue(ResponseInfo(type_, content_));

// 弹出已收到响应的请求

pending_.pop();

4.4调用示例


发起两个请求,当请求都返回后,打印:


void WaitMultiRequests(const std::shared_ptr<RedisContext>& ctx)
{
    // issue 2 requests, when they all return, callback
    auto fut1 = ctx->Set("city", "shenzhen");
    auto fut2 = ctx->Set("company", "tencent");
    ananas::WhenAll(fut1, fut2).Then(
                    [](std::tuple<ananas::Try<ResponseInfo>,
                                  ananas::Try<ResponseInfo> >& results) {
                        std::cout << "All requests returned:\n";
                        RedisContext::PrintResponse(std::get<0>(results));
                        RedisContext::PrintResponse(std::get<1>(results));
            }); 
}

5.结语


关于ananas future的使用篇就到这里,后面会带来future的源码分析以及其它模块的使用和实现。



 


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
135 59
|
1月前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
26 2
|
13天前
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
35 0
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
45 0
|
1月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
52 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
109 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
102 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
129 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
33 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
33 4