【问题标题】:Can C++20 coroutines be copied?C++20协程可以复制吗?
【发布时间】:2020-02-26 12:17:02
【问题描述】:

我一直在使用 C++20 协程,并尝试将我的一些代码库转移到使用它们。不过,我遇到了一个问题,因为似乎无法复制新的协程。 generator 对象已删除复制构造函数和复制赋值运算符,而我所研究的一切似乎都没有办法。

这个可以吗?

作为参考,我编写了一个小测试程序,尝试复制 C++20 协程失败,并成功尝试使用 boost::asio::coroutine 做同样的事情。这是使用 Visual Studio 2019 版本 16.3.7

#include <sdkddkver.h>
#include <string>
#include <algorithm>
#include <iterator>
#include <experimental\resumable>
#include <experimental\generator>
#include <cassert>

#include <boost\asio\yield.hpp>

namespace std_coroutines {
    auto letters() {
        for (auto c = 'a'; ; ++c)
            co_yield c;
    }

    void run() {
        auto gen = letters();
        std::string s1, s2;
        std::copy_n(gen.begin(), 3, std::back_inserter(s1)); // append "abc" to s1
        //auto gen_copy = gen; // doesn't compile

        std::copy_n(gen.begin(), 3, std::back_inserter(s1)); // append "def" to s1
        //std::copy_n(gen_copy.begin(), 3, std::back_inserter(s2)); // append "def" to s2

        assert(s1 == "abcdef");
        assert(s2 == "def"); // fails
    }
};// namespace std_coroutines

namespace boost_asio_coroutines {
    struct letters : boost::asio::coroutine {
        char c = 'a';
        char operator()() {
            reenter(this) for (;; ++c)
            {
                yield return c;
            }
        }
    };

    void run() {
        auto gen = letters();
        std::string s1, s2;
        std::generate_n(std::back_inserter(s1), 3, std::ref(gen)); // append "abc" to s1
        auto gen_copy = gen;
        std::generate_n(std::back_inserter(s1), 3, std::ref(gen)); // append "def" to s1
        std::generate_n(std::back_inserter(s2), 3, std::ref(gen_copy)); // append "def" to s2

        assert(s1 == "abcdef");
        assert(s2 == "def");
    }
} // namespace boost_asio_coroutines

int main() {
    boost_asio_coroutines::run();
    std_coroutines::run();
}

【问题讨论】:

  • 我不知道 boost 协程,但是std::generator 拥有底层协程的所有权;因此它只能被移动(因为无法复制底层协程)。
  • 所以..不是吗?没有办法复制c++20协程?
  • @KaenbyouRin 你肯定可以复制boost协程,我写的示例代码演示了它
  • 请不要在#include 路径中使用反斜杠。正斜杠在任何地方都很好用,所以就默认使用那些。

标签: c++ coroutine c++20


【解决方案1】:

当我谈到“复制协程”时,我的意思本质上是对协程函数返回的“未来”对象执行复制操作,这会导致用户可能认为是该未来的“副本” .

协同程序的浅拷贝(拷贝的未来引用相同的coroutine_handle 和promise 对象)在某些情况下可能有一些小用途。对于非生成器场景,您实际上是在创建 std::shared_future 的等效项:可以从中提取承诺值的多个位置。

我不确定这对生成器场景有多大用处。推断生成器正在做什么以及它在执行过程中的位置将变得更加困难。它也基本上会扼杀分配省略的任何希望,因为您将多次引用承诺/句柄。

协同程序的深度复制几乎是不可能的。要深度复制协程,您需要复制此类协程的堆栈。即使我们假设协程保证不会在复制操作期间执行,这也不是真正可行的。为什么?

因为堆栈可能包含不可复制的对象。并且协程不需要内联,因此必须编译复制操作的编译器不一定可以访问协程本身的源代码。因此,它无法确定协程堆栈是否可复制。

现在,至少假设可以进行某种运行时测试,以查看复制是否是该协程的可能操作。但那是它自己的蠕虫罐头。

但是任何类型的深度复制都需要更改协程机制;浅拷贝是你可以假设在你自己的未来类型中实现的东西。

您的代码具有深拷贝语义的原因是它并没有真正使用协程承诺/未来机制。承诺和执行的协程都没有值;你的协程对象是。因此,复制您的协程对象会创建该值的副本。实际的 C++20 协程不是这样工作的。

【讨论】:

  • 但如果不可能,那boost::asio::coroutine怎么办呢?啊,看来他们只允许空堆栈的协程。但是,理论上,C++20 协程不能提供一种声明“堆栈可复制”协程以保证堆栈可以复制的方法吗?它将被保存到memcpy 他们的堆栈...
【解决方案2】:

TS 没有明确禁止复制。正如您所提到的,std::experimental::generator promise 对象已删除复制操作。我认为最初的实现对副本比较保守,因为需要考虑很多。

协程管理协程激活上下文的句柄,N4775 正式称为 协程状态。此状态是在堆上还是在堆栈上(出于优化原因),是由实现定义的。 coroutine-storage 本身的格式也是实现定义的。

只要按照shared_ptrweak_ptr 的行建立协程句柄的所有权语义,就可以通过实现实现浅拷贝(类比有点分崩离析,因为只有一个协程是国家,而所有其他人都是观察者)。

如果您询问的是深层副本,最终会得到两个不会相互影响的独立生成器,我想这也是可能的,包括所有含义。

我能想到的一些含义:

  • 深度复制函数的局部变量非常昂贵,而且这种额外的分配开销可能会让用户感到意外。
  • 复制的协程句柄总是*需要存在于堆上,这意味着您最终可能会遇到“原始”协程具有出色性能但复制的协程性能较差的情况。意外副本可能会默默地扼杀优化。

*协程的存储是通过调用全局非数组new函数获得的。理论上你可以重载这个函数来匹配你的协程,但是由于存储是实现定义的,你必须知道你的平台。尽管如此,这仍然可以让您在理论上利用一个全局竞技场分配器,该分配器保留了堆栈的一部分,例如

【讨论】:

  • "浅拷贝" 协程的浅拷贝没有意义。尤其是发电机;您可能会从两个不同的源位置获取值,但它们都来自同一个流。复制协程与复制文件流一样有意义,但我们也不允许这样做。
猜你喜欢
  • 2021-09-03
  • 2020-12-22
  • 2019-12-01
  • 1970-01-01
  • 2021-08-07
  • 2019-07-31
  • 2021-04-30
  • 2020-01-22
相关资源
最近更新 更多