【问题标题】:How do you implement Coroutines in C++你如何在 C++ 中实现协程
【发布时间】:2010-09-12 09:56:09
【问题描述】:

我怀疑它是否可以便携,但有什么解决方案吗?我认为可以通过创建一个备用堆栈并在函数入口处重置 SP、BP 和 IP,并让产量保存 IP 并恢复 SP+BP 来完成。析构函数和异常安全似乎很棘手,但可以解决。

完成了吗?不可能吗?

【问题讨论】:

  • 只是想指出协程在 C++ 中是可能的。升压是一种可能性。另一个是被批准为 C++17 技术规范的协程。已经有两个支持的编译器(VC14 和 Clang),TS 很可能会在 C++17 之后成为语言。详情见我的回答。
  • 对于 C 程序员,这里有一篇 Simon Tatham 撰写的文章,C 中的协程,其中包含多种方法。 chiark.greenend.org.uk/~sgtatham/coroutines.html 有些比其他的更复杂。

标签: c++ coroutine c++17


【解决方案1】:

是的,它可以做到没有问题。您只需要一点汇编代码即可将调用堆栈移动到堆上新分配的堆栈。

我会查看boost::coroutine

您应该注意的一件事是堆栈溢出。在大多数操作系统上,堆栈溢出将导致段错误,因为未映射虚拟内存页面。但是,如果您在堆上分配堆栈,您将得不到任何保证。 请记住这一点。

【讨论】:

  • 我认为应该有一个徽章,以便能够在 SO 的有效技术上下文中提及工作“stackoverflow”!
  • 这是一个不错的标准 C++ 解决方案,不需要涉及 Boost:akira.ruc.dk/~keld/research/COROUTINE
  • 如果你在堆上分配栈,你可以和真正的栈做同样的事情,并在最后(或开始,因为它通常向后增长)放置一个保护页,这也会导致小溢出的段错误。
  • 请注意,尽管 boost::coroutine 是一个很棒的库,但 c++ 协程有望成为 C++17 之后的核心 c++ 功能。目前在技术规范中定义,参考实现在 Visual Studio 2015 和 Clang:wg21.link/p0057r2
  • c++20 中的协程不是 OP 想要的协程,因为它们是无堆栈的。
【解决方案2】:

在 POSIX 上,您可以使用 makecontext()/swapcontext() 例程来可移植地切换执行上下文。在 Windows 上,您可以使用光纤 API。否则,您所需要的只是一些切换机器上下文的胶水汇编代码。我已经使用 ASM(对于 AMD64)和 swapcontext() 实现了协程;两者都不是很难。

【讨论】:

【解决方案3】:

为了后代,

Dmitry Vyukov 的 wondeful web site 有一个巧妙的技巧,使用 ucontext 和 setjump 来模拟 c++ 中的协程。

此外,Oliver Kowalke 的上下文库是 recently accepted 进入 Boost,所以希望我们很快就会看到在 x86_64 上运行的 boost.coroutine 的更新版本。

【讨论】:

    【解决方案4】:

    没有简单的方法来实现协程。因为协程本身就像线程一样脱离了 C/C++ 的堆栈抽象。因此,如果不更改支持的语言级别,就无法支持它。

    目前(C++11),所有现有的 C++ 协程实现都是基于汇编级别的黑客攻击,很难安全可靠地跨平台。为了可靠,它需要是标准的,并且由编译器而不是黑客来处理。

    为此有一个standard proposal - N3708。如果您有兴趣,请查看它。

    【讨论】:

    【解决方案5】:

    如果可能的话,使用迭代器可能比使用协程更好。这样您就可以继续调用next() 来获取下一个值,但您可以将状态保留为成员变量而不是局部变量。

    这可能会使事情更易于维护。其他 C++ 开发人员可能不会立即理解协程,但他们可能更熟悉迭代器。

    【讨论】:

      【解决方案6】:

      对于那些想知道如何在 C++ 中以可移植方式利用协程的人 y̶o̶u̶̶w̶i̶l̶l̶̶h̶a̶v̶e̶̶t̶o̶̶w̶a̶i̶t̶̶f̶o̶r̶̶C̶+̶+̶1̶7̶ 等待结束(见下文)!标准委员会正在研究该功能,请参阅N3722 paper。总结一下论文的当前草稿,关键字将是可恢复和等待,而不是 Async 和 Await。

      看一下 Visual Studio 2015 中的实验性实现,与微软的实验性实现一起玩。看起来 clang 还没有实现。

      来自 Cppcon Coroutines a negative overhead abstraction 的精彩演讲概述了在 C++ 中使用协程的好处以及它如何影响代码的简单性和性能。

      目前我们仍然必须使用库实现,但在不久的将来,我们将把协程作为核心 C++ 特性。

      更新: 看起来协程实现是为 C++20 设计的,但作为 C++17 的技术规范发布(p0057r2)。 Visual C++、clang 和 gcc 允许您选择使用编译时标志。

      【讨论】:

        【解决方案7】:

        COROUTINE a portable C++ library for coroutine sequencing 是否为您指明了正确的方向?这似乎是一个经过时间考验的优雅解决方案......它已经 9 岁了!

        在 DOC 文件夹中是 Keld Helsgaun 的论文 A Portable C++ Library for Coroutine Sequencing 的 pdf,其中描述了该库并提供了使用它的简短示例。

        [update] 实际上我自己也成功地使用了它。好奇心使我变得更好,所以我研究了这个解决方案,发现它非常适合我已经研究了一段时间的问题!

        【讨论】:

          【解决方案8】:

          我认为 C++ 中没有很多成熟、干净的实现。我喜欢的一种尝试是Adam Dunkels' protothread library

          另请参阅 ACM 数字图书馆中的 Protothreads: simplifying event-driven programming of memory-constrained embedded systems 和维基百科主题 Protothread 中的讨论,

          【讨论】:

            【解决方案9】:

            它基于(畏缩)宏,但以下站点提供了易于使用的生成器实现:http://www.codeproject.com/KB/cpp/cpp_generators.aspx

            【讨论】:

              【解决方案10】:

              今天发布了一个新库 Boost.Context,它具有用于实现协程的可移植特性。

              【讨论】:

                【解决方案11】:

                这是一个旧线程,但我想建议使用不依赖于操作系统的 Duff 设备进行破解(据我所知):

                C coroutines using Duff's device

                作为一个例子,这是一个我修改为使用协程而不是 fork/threads 的 telnet 库: Telnet cli library using coroutines

                由于 C99 之前的标准 C 本质上是 C++ 的真正子集,因此这在 C++ 中也很有效。

                【讨论】:

                  【解决方案12】:

                  我想出了一个没有asm代码的实现。思路是使用系统的线程创建函数初始化栈和上下文,使用setjmp/longjmp切换上下文。但它不是便携式的,如果您有兴趣,请参阅tricky pthread version

                  【讨论】:

                    【解决方案13】:

                    https://github.com/tonbit/coroutine 是支持 resume/yield/await 原语和 Channel 模型的 C++11 单 .h 非对称协程实现。它通过 ucontext/fiber 实现,不依赖于 boost,在 linux/windows/macOS 上运行。这是学习在 C++ 中实现协程的一个很好的起点。

                    【讨论】:

                      【解决方案14】:

                      查看我的实现,它说明了 asm hacking 点并且很简单:

                      https://github.com/user1095108/generic/blob/master/coroutine.hpp

                      【讨论】:

                      • 我在 armv5、armv6、armv7 和 arm64 上进行了测试。如果它不起作用,请修复它,我将接受补丁。请注意,您确实需要 STL。
                      【解决方案15】:

                      同样基于宏(Duff 的设备,完全便携,请参阅 http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html ) 并受到 Mark 发布的链接的启发,下面模拟了使用事件作为同步机制进行协作的协同进程(与传统的协同例程/生成器样式略有不同的模型)

                      // Coprocess.h
                      #pragma once
                      #include <vector>
                      
                      class Coprocess {
                        public:
                          Coprocess() : line_(0) {}
                          void start() { line_ =  0; run(); }
                          void end()   { line_ = -1; on_end(); }
                          virtual void run() = 0;
                          virtual void on_end() {}; 
                        protected:
                          int line_;
                      };
                      
                      class Event {
                        public:
                          Event() : curr_(0) {}
                      
                          void wait(Coprocess* p) { waiters_[curr_].push_back(p); }
                      
                          void notify() {
                              Waiters& old = waiters_[curr_];
                              curr_ = 1 - curr_; // move to next ping/pong set of waiters
                              waiters_[curr_].clear();
                              for (Waiters::const_iterator I=old.begin(), E=old.end(); I != E; ++I)
                                  (*I)->run();
                          }   
                        private:
                          typedef std::vector<Coprocess*> Waiters;
                          int curr_;
                          Waiters waiters_[2];
                      };
                      
                      #define corun()   run() { switch(line_) { case 0:
                      #define cowait(e) line_=__LINE__; e.wait(this); return; case __LINE__:
                      #define coend     default:; }} void on_end()
                      

                      使用示例:

                      // main.cpp
                      #include "Coprocess.h"
                      #include <iostream>
                      
                      Event e;
                      long sum=0;
                      
                      struct Fa : public Coprocess {
                          int n, i;
                          Fa(int x=1) : n(x) {}
                          void corun() {
                              std::cout << i << " starts\n";
                              for (i=0; ; i+=n) {
                                  cowait(e);
                                  sum += i;
                              }
                          } coend {
                              std::cout << n << " ended " << i << std::endl;
                          }   
                      };
                      
                      int main() {
                          // create 2 collaborating processes
                          Fa f1(5);
                          Fa f2(10);
                      
                          // start them
                          f1.start();
                          f2.start();
                          for (int k=0; k<=100; k++) { 
                              e.notify();
                          }   
                          // optional (only if need to restart them)
                          f1.end();
                          f2.end();
                      
                          f1.start(); // coprocesses can be restarted
                          std::cout << "sum " << sum << "\n";
                          return 0;
                      }
                      

                      【讨论】:

                        【解决方案16】:

                        WvContWvStreams 的一部分,它实现了所谓的半协程。这些比完整的协程更容易处理:你调用它,它会返回给调用它的人。

                        它是使用更灵活的 WvTask 实现的,它支持完整的协程;您可以在同一个库中找到它。

                        至少可以在 win32 和 Linux 上工作,并且可能在任何其他 Unix 系统上工作。

                        【讨论】:

                          【解决方案17】:

                          你需要一些像 boost context 这样的 asm 代码来切换上下文。

                          Here 是 C++ 的 golang-style 协程实现。它适用于 Windows、Linux 和 Mac。并且很容易启动协程如下:

                          go(f);                      // void f();
                          go(f, 3);                   // void f(int);
                          go(f, p);                   // void f(void*);   void* p;
                          go(&T::f, p);               // void T::f();     T* p;
                          go(std::bind(&T::f, p, i);  // void T::f(int);  T* p;  int i;
                          

                          此外,无需担心堆栈溢出,因为同一线程中的协程共享一个足够大的堆栈(默认为 1MB)。

                          【讨论】:

                            【解决方案18】:

                            您应该始终考虑使用线程;尤其是在现代硬件中。如果您的工作可以在协程中进行逻辑分离,那么使用线程意味着这些工作实际上可能是由单独的执行单元(处理器内核)同时完成的。

                            但是,也许您确实想要使用协程,可能是因为您有一个已经以这种方式编写和测试的经过良好测试的算法,或者因为您正在移植以这种方式编写的代码。

                            如果您在 Windows 中工作,您应该查看fibers。 Fibers 将在操作系统的支持下为您提供类似协程的框架。

                            我不熟悉其他操作系统,无法推荐那里的替代品。

                            【讨论】:

                            • 我不同意盲目地偏爱线程而不是纤维。在最佳系统中,尝试运行的线程数等于内核数(如果超线程则更多)。在具有大量线程(100 个)的解决方案中,运行纤程的线程池可以更加高效。
                            • 感谢您的评论。但是,我并没有说“一味宠爱”;我说“总是考虑”。我同意在某些情况下,纤维或其他协程方法可能更合适,但它们可能比线程更难“正确”——这说明了很多。基本上,我建议在大多数情况下,您应该默认使用线程,除非您可以说服自己有充分的理由去做其他事情。
                            • 线程意味着锁定,而协程自然是按顺序执行的。繁荣你的工作已经为你完成了一半。如果您想并行计算多个重型算法,线程很好,我真的想不出任何其他使用它们的理由。我猜是否有一些没有非阻塞模式的阻塞 API?
                            • @Longpoke,我不同意你一半的工作是用协程为你完成的。您将考虑锁换成必须拆分算法。不同的东西。有时协程更好,有时则不然。这就是我所说的考虑线程
                            【解决方案19】:

                            我尝试使用 C++11 和线程自己实现协程:

                            #include <iostream>
                            #include <thread>
                            
                            class InterruptedException : public std::exception {
                            };
                            
                            class AsyncThread {
                            public:
                                AsyncThread() {
                                    std::unique_lock<std::mutex> lock(mutex);
                                    thread.reset(new std::thread(std::bind(&AsyncThread::run, this)));
                                    conditionVar.wait(lock); // wait for the thread to start
                                }
                                ~AsyncThread() {
                                    {
                                        std::lock_guard<std::mutex> _(mutex);
                                        quit = true;
                                    }
                                    conditionVar.notify_all();
                                    thread->join();
                                }
                                void run() {
                                    try {
                                        yield();
                                        for (int i = 0; i < 7; ++i) {
                                            std::cout << i << std::endl;
                                            yield();
                                        }
                                    } catch (InterruptedException& e) {
                                        return;
                                    }
                                    std::lock_guard<std::mutex> lock(mutex);
                                    quit = true;
                                    conditionVar.notify_all();
                                }
                                void yield() {
                                    std::unique_lock<std::mutex> lock(mutex);
                                    conditionVar.notify_all();
                                    conditionVar.wait(lock);
                                    if (quit) {
                                        throw InterruptedException();
                                    }
                                }
                                void step() {
                                    std::unique_lock<std::mutex> lock(mutex);
                                    if (!quit) {
                                        conditionVar.notify_all();
                                        conditionVar.wait(lock);
                                    }
                                }
                            private:
                                std::unique_ptr<std::thread> thread;
                                std::condition_variable conditionVar;
                                std::mutex mutex;
                                bool quit = false;
                            };
                            
                            int main() {
                                AsyncThread asyncThread;
                                for (int i = 0; i < 3; ++i) {
                                    std::cout << "main: " << i << std::endl;
                                    asyncThread.step();
                                }
                            }
                            

                            【讨论】:

                            • 这不只是生产者-消费者的实现吗?
                            • 当你说线程时你把我弄丢了。协程不需要线程。
                            • 是的,这是很久以前的事了,那时我还没有真正理解协程是什么;)
                            猜你喜欢
                            • 1970-01-01
                            • 1970-01-01
                            • 2016-01-05
                            • 2012-10-29
                            • 2011-03-27
                            • 1970-01-01
                            • 1970-01-01
                            • 2010-11-27
                            • 1970-01-01
                            相关资源
                            最近更新 更多