admin管理员组

文章数量:1123049

I have a project that uses a custom class, Router, that has an asynchronous call to Start a connection. How this works is that the consumer will call into Start(), which will return immediately. At a later time, then, the Router will signal that the connection has started.

I am trying to put a wrapper around this class to make the Start call synchronous, i.e. will block until the Router signals back that the connection has started. So to the consumer it looks like a single, synchronous call.

To accomplish this, I create a promise for every call to Start and then block the call until the promise gets fulfilled. I keep these promises, wrapped in std::shared_ptr<>, in a std::vector. When the Router signals that the connection has started, the vector will be iterated over and all promises are set.

Here is some lightly edited code from my project:

class Wrapper {
private:
    Router* m_router; // this is a custom class... details do not matter
    std::vector<std::shared_ptr<std::promise<void>>> m_pendingPromises;
    std::recursive_mutex m_startStopMutex;

public:
    // this gets called by the consumer to start the connection
    void Start(){
        if(m_router->IsRunning() == false) {
            std::unique_lock lock(m_startStopMutex);
            if(m_router->IsRunning() == false) {
                // create the promise and put it in the pending list
                auto promise = std::make_shared<std::promise<void>>();
                m_pendingPromises.emplace_back(promise);

                // call on the router to start the connection
                m_router->Start();

                // unlock the mutex to allow the OnStarted handler to run
                lock.unlock();

                // wait for the promise to be fulfilled
                promise->get_future().get();
            }
        }
    }

    // this will get called by the Router once the connection has started
    void OnStarted() {
    {
        // lock the mutex to ensure that no more pending promises are created
        std::lock_guard lock(m_startStopMutex);

        // fulfill all the pending promises  
        for(int i = 0; i < m_pendingPromises.size(); i++) {
            m_pendingPromises[i]->set_value();
        }

        // clear the list of pending promises
        m_pendingPromises.clear();
    }
}

The issue that I'm running into is at the m_pendingPromises[i]->set_value(); in the OnStarted() method. When I debug and I step up to the point where the vector starts to get iterated, but before any promise is set, I see the expected state of m_pendingPromises as size = 1 and capacity = 1:

However, the instant that I set the promise, the vector's size goes to zero and the capacity goes to some crazy high number!

Does anyone know why this is happening? Alternatively, is this a real error or just a red herring caused by the CLion debugger?

FYI I am developing in C++ 17 using CLion 2024.2.2:

Update: I modified my unit test to put two promises into the vector. The same behavior occurs but only after the last promise is set. When setting the first promise, I do not see the size or capacity of the vector change.

Update 2: I originally omitted m_pendingPromises from the constructor since, as I understand it, the default constructor will be called when it gets declared. However, just to try, I did add a constructor for the Wrapper and initialized the vector to 0:

void Wrapper() : m_pendingPromises(0) {}

When I did this, I got a similar behavior but with different values for size and capacity:

Update 3: Posting the unit test that exhibits this behavior:

TEST_F(WrapperTestFixture, Start_RouterStartCalledOnce) {
    // arrange
    // set up router.IsRunning to return the running flag
    EXPECT_CALL(this->router, IsRunning())
    .Times(::testing::AnyNumber()) // EXPECT_CALL.Times(AnyNumber()) is preferable over ON_CALL because it doesn't raise "Uninteresting call" warnings
    .WillRepeatedly([this]() -> bool { return this->connectionRunning; });

    auto wrapper = Wrapper(this->router);

    // set up router.Start to start a thread that will invoke the callback unless the running flag is toggled
    EXPECT_CALL(this->router, Start())
    .Times(::testing::AnyNumber()) // EXPECT_CALL.Times(AnyNumber()) is preferable over ON_CALL because it doesn't raise "Uninteresting call" warnings
    .WillRepeatedly(::testing::Invoke([this, wrapper]() {
        this->m_numTimesStartCalled++;
        auto future = std::async(std::launch::async, [this, wrapper]() {
            // toggle the running flag and raise the event that signals this connection is now running
            this->connectionRunning = true;
            wrapper.OnStarted();
        });

        // grab the future from the async so that the test fixture can make sure to wait until this thread is completed
        this->m_futureFromRouterStart = std::make_shared<std::future<void>>(std::move(future));
    }));

    // act
    wrapper.Start();

    // assert
    ASSERT_EQ(this->m_numTimesStartCalled, 1);

    // make sure the Start thread is finished
    this->m_futureFromRouterStart->get();
}

本文标签: