Search code examples
c++multithreadingboostboost-asioboost-beast

boost beast sync app - Is explicit concurrency handling needed?


Consider the official boost beast websocket server sync example

Specifically, this part:

for(;;)
{
    // This buffer will hold the incoming message
    beast::flat_buffer buffer;

    // Read a message
    ws.read(buffer);

    // Echo the message back
    ws.text(ws.got_text());
    ws.write(buffer.data());
}

To simplify the scenario, let's assume it only ever writes, and the data being written is different each time.

for(;;)
{
    // assume some data has been prepared elsewhere, in str
    mywrite(str);
}
...
void mywrite(char* str)
{
   net::const_buffer b(str, strlen(str));
   ws.write(b);
}

This should be fine, as all calls to mywrite happen sequentially.

What if we had multiple threads and the same for loop? i.e. what if we had concurrent calls to mywrite, and to ws.write by extension? Would something like a strand or a mutex be needed?

In other words, do we need to explicitly handle concurrency when calling ws.write from multiple threads?

I've not yet understood the docs, as they mention:

Thread Safety

Distinctobjects:Safe.

Sharedobjects:Unsafe. The application must also ensure that all asynchronous operations are performed within the same implicit or explicit strand.

And then

 Alternatively, for a single-threaded or synchronous application you may write:

websocket::stream<tcp_stream> ws(ioc);

This seems to imply ws object is not thread-safe, but also for the specific case of a sync app, there's no explicit strand being constructed, implying it's OK?

I was not able to work it out by reading the example, or the websocket implementation. I've never worked with asio before.

I tried to test it as follows, it didn't seem to fail on my laptop but I don't have the guarantee this will work for other cases. I'm not sure it's a valid test for the case I've described either.

auto lt = [&](unsigned long long i)
{
    char s[1000] = {0};
    for(;;++i)
    {
        sprintf(s, "Hello from thread:%llu", i);
        mywrite(s,30);
    }
};

std::thread(lt, 10000000u).detach();
std::thread(lt, 20000000u).detach();
std::thread(lt, 30000000u).detach();

// ws client init, as the official example

            for (int i = 0; i < 100; ++i)
            {
                beast::flat_buffer buffer;
                // Read a message into our buffer
                ws.read(buffer);
                
                // The make_printable() function helps print a ConstBufferSequence
                std::cout << beast::make_printable(buffer.data()) << std::endl;
            }

Solution

  • Yes, you need synchronization because you access the object from multiple threads.

    The documentation you quoted is very clear on that:

    Sharedobjects:Unsafe [...]

    On your rationale for being confused:

    This seems to imply ws object is not thread-safe, but also for the specific case of a sync app, there's no explicit strand being constructed, implying it's OK?

    It's okay because it's single-threaded, not because it's synchronous. In fact, even if single-threading you still need a strand to prevent overlapped asynchronous write operations. That's what the second part hints at:

    [...] The application must also ensure that all asynchronous operations are performed within the same implicit or explicit strand.

    Now, the missing piece that might solve the puzzle for you is the example has an implicit logical strand (a sequential chain of non-overlapping asynchronous operations). See also Why do I need strand per connection when using boost::asio?