2

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;
            }
ArthurChamz
  • 2,039
  • 14
  • 25

1 Answers1

1

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?

sehe
  • 374,641
  • 47
  • 450
  • 633
  • I can understand this interpretation, and it's the one that makes sense to me. What confuses me is the part where it says: "Alternatively, for a single-threaded or synchronous application you may write:" Particularly, the "or" – ArthurChamz Sep 01 '22 at 10:52
  • 1
    The wording could be made more clear, but I am tempted to say this seems to be also a case of "programmer reading" of English. For single-threaded application you always have the implicit strand. For sync application, you necessarily have sequential invocation. Quite obviously you can not usefully overlap that with synchronous operations from a different thread. A strand really doesn't change anything there because synchronous operations do not use executors in the first place. You can synchronize threaded access in any way you want, including but not limited to strands there. – sehe Sep 01 '22 at 12:33