Proper cleanup with a suspended coroutine

后端 未结 1 1504
无人及你
无人及你 2021-01-20 18:47

I\'m wondering what the best (cleanest, hardest to mess up) method for cleanup is in this situation.

void MyClass::do_stuff(boost::asio::yield_context contex         


        
1条回答
  •  挽巷
    挽巷 (楼主)
    2021-01-20 19:14

    A more typical approach would be to use boost::enable_shared_from_this with MyClass, and run the methods as bound to the shared pointer.

    Boost Bind supports binding to boost::shared_ptr transparently.

    This way, you can automatically have the destructor run only when the last user disappears.


    If you create a SSCCE, I'm happy to change it around, to show what I mean.


    UPDATE

    To the SSCCEE: Some remarks:

    • I imagined a pool of threads running the IO service
    • The way in which MyClass calls into AsyncBuffer member functions directly is not threadsafe. There is actually no thread safe way to cancel the event outside the producer thread[1], since the producer already access the buffer for Writeing. This could be mitigated using a strand (in the current setup I don't see how MyClass would likely be threadsafe). Alternatively, look at the active object pattern (for which Tanner has an excellent answer[2] on SO).

      I chose the strand approach here, for simplicity, so we do:

      void MyClass::Write(uint32_t data) {
          strand_.post(boost::bind(&AsyncBuffer::Write, &buffer_, data));
      }
      
    • You ask

      Also, for the event (what I have is basically the same as Tanner's answer here) I need to cancel it in a way that I'd have to keep some extra state (a true cancel vs. the normal cancel used to fire the event)

      The most natural place for this state is the usual for the deadline_timer: it's deadline. Stopping the buffer is done by resetting the timer:

      void AsyncBuffer::Stop() { // not threadsafe!
          write_event_.expires_from_now(boost::posix_time::seconds(-1));
      }
      

      This at once cancels the timer, but is detectable because the deadline is in the past.

    Here's a simple demo with a a group of IO service threads, one "producer coroutine" that produces random numbers and a "sniper thread" that snipes the MyClass::Run coroutine after 2 seconds. The main thread is the sniper thread.

    See it Live On Coliru

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // for refcounting:
    #include 
    #include 
    
    namespace asio = boost::asio;
    
    class AsyncBuffer {
        friend class MyClass;
    protected:
        AsyncBuffer(boost::asio::io_service &io_service) : write_event_(io_service) {
            write_event_.expires_at(boost::posix_time::pos_infin);
        }
    
        void Write(uint32_t data) {
            buffer_.push_back(data);
            write_event_.cancel();
        }
    
        uint32_t Read(boost::asio::yield_context context) {
            if (buffer_.empty()) {
                boost::system::error_code ec;
                write_event_.async_wait(context[ec]);
    
                if (ec != boost::asio::error::operation_aborted || write_event_.expires_from_now().is_negative())
                {
                    if (context.ec_)
                        *context.ec_ = boost::asio::error::operation_aborted;
                    return 0;
                }
            }
    
            uint32_t data = buffer_.front();
            buffer_.pop_front();
            return data;
        }
    
        void Stop() {
            write_event_.expires_from_now(boost::posix_time::seconds(-1));
        }
    
    private:
        boost::asio::deadline_timer write_event_;
        std::list buffer_;
    };
    
    class MyClass : public boost::enable_shared_from_this {
        boost::atomic_bool stopped_;
    public:
        MyClass(boost::asio::io_service &io_service) : stopped_(false), buffer_(io_service), strand_(io_service) {}
    
        void Run(boost::asio::yield_context context) {
            while (!stopped_) {
                boost::system::error_code ec;
    
                uint32_t data = buffer_.Read(context[ec]);
    
                if (ec == boost::asio::error::operation_aborted)
                    break;
    
                // do something with data
                std::cout << data << " " << std::flush;
            }
            std::cout << "EOF\n";
        }
    
        bool Write(uint32_t data) { 
            if (!stopped_) {
                strand_.post(boost::bind(&AsyncBuffer::Write, &buffer_, data));
            }
            return !stopped_;
        }
    
        void Start() {
            if (!stopped_) {
                stopped_ = false;
                boost::asio::spawn(strand_, boost::bind(&MyClass::Run, shared_from_this(), _1));
            }
        }
    
        void Stop() {
            stopped_ = true;
            strand_.post(boost::bind(&AsyncBuffer::Stop, &buffer_));
        }
    
        ~MyClass() { 
            std::cout << "MyClass destructed because no coroutines hold a reference to it anymore\n";
        }
    
    protected:
        AsyncBuffer buffer_;
        boost::asio::strand strand_;
    };
    
    int main()
    {
        boost::thread_group tg;
        asio::io_service svc;
    
        {
            // Start the consumer:
            auto instance = boost::make_shared(svc); 
            instance->Start();
    
            // Sniper in 2 seconds :)
            boost::thread([instance]{ 
                    boost::this_thread::sleep_for(boost::chrono::seconds(2));
                    instance->Stop();
                    }).detach();
    
            // Start the producer:
            auto producer_coro = [instance, &svc](asio::yield_context c) { // a bound function/function object in C++03
                asio::deadline_timer tim(svc);
    
                while (instance->Write(rand())) {
                    tim.expires_from_now(boost::posix_time::milliseconds(200));
                    tim.async_wait(c);
                }
            };
    
            asio::spawn(svc, producer_coro);
    
            // Start the service threads:
            for(size_t i=0; i < boost::thread::hardware_concurrency(); ++i)
                tg.create_thread(boost::bind(&asio::io_service::run, &svc));
        }
    
        // now `instance` is out of scope, it will selfdestruct after the snipe
        // completed
        boost::this_thread::sleep_for(boost::chrono::seconds(3)); // wait longer than the snipe
        std::cout << "This is the main thread _after_ MyClass self-destructed correctly\n";
    
        // cleanup service threads
        tg.join_all();
    }
    

    [1] logical thread, this could be a coroutine that gets resumed on different threads

    [2] boost::asio and Active Object

    0 讨论(0)
提交回复
热议问题