Hi Stuart,

I mostly agree with your assessment about the suitability of the ByteBuffer API for nice multithreaded use. What would such API look like? I think pretty much like ByteBuffer but without things that mutate mark/position/limit/ByteOrder. A stripped-down ByteBuffer API therefore. That would be in my opinion the most low-level API possible. If you add things to such API that coordinate multithreaded access to the underlying memory, you are already creating a concurrent data structure for a particular set of use cases, which might not cover all possible use cases or be sub-optimal at some of them. So I think this is better layered on top of such API not built into it. Low-level multithreaded access to memory is, in my opinion, always going to be "unsafe" from the standpoint of coordination. It's not only the mark/position/limit/ByteOrder that is not multithreaded-friendly about ByteBuffer API, but the underlying memory too. It would be nice if mark/position/limit/ByteOrder weren't in the way though.

Regards, Peter

On 09/28/2018 07:51 AM, Stuart Marks wrote:
Hi Andrew,

Let me first stay that this issue of "ByteBuffer might not be the right answer" is something of a digression from the JEP discussion. I think the JEP should proceed forward using MBB with the API that you and Alan had discussed previously. At most, the discussion of the "right thing" issue might affect a side note in the JEP text about possible limitations and future directions of this effort. However, it's not a blocker to the JEP making progress as far as I'm concerned.

With that in mind, I'll discuss the issue of multithreaded access to ByteBuffers and how this bears on whether buffers are or aren't the "right answer." There are actually several issues that figure into the "right answer" analysis. In this message, though, I'll just focus on the issue of multithreaded access.

To recap (possibly for the benefit of other readers) the Buffer class doc has the following statement:

    Buffers are not safe for use by multiple concurrent threads. If a buffer     is to be used by more than one thread then access to the buffer should be
    controlled by appropriate synchronization.

Buffers are primarily designed for sequential operations such as I/O or codeset conversion. Typical buffer operations set the mark, position, and limit before initiating the operation. If the operation completes partially -- not uncommon with I/O or codeset conversion -- the position is updated so that the operation can be resumed easily from where it left off.

The fact that buffers not only contain the data being operated upon but also mutable state information such as mark/position/limit makes it difficult to have multiple threads operate on different parts of the same buffer. Each thread would have to lock around setting the position and limit and performing the operation, preventing any parallelism. The typical way to deal with this is to create multiple buffer slices, one per thread. Each slice has its own mark/position/limit values but shares the same backing data.

We can avoid the need for this by adding absolute bulk operations, right?

Let's suppose we were to add something like this (considering ByteBuffer only, setting the buffer views aside):

    get(int srcOff, byte[] dst, int dstOff, int length)
    put(int dstOff, byte[] src, int srcOff, int length)

Each thread can perform its operations on a different part of the buffer, in parallel, without interference from the others. Presumably these operations don't read or write the mark and position. Oh, wait. The existing absolute put and get overloads *do* respect the buffer's limit, so the absolute bulk operations ought to as well. This means they do depend on shared state. (I guess we could make the absolute bulk ops not respect the limit, but that seems inconsistent.)

OK, let's adopt an approach similar to what was described by Peter Levart a couple messages upthread, where a) there is an initialization step where various things including the limit are set properly; b) the buffer is published to the worker threads properly, e.g., using a lock or other suitable memory operation; and c) all worker threads agree only to use absolute operations and to avoid relative operations.

Now suppose the threads have completed their work and you want to, say, write the buffer's contents to a channel. You have to carefully make sure the threads are all finished and properly publish their results back to some central thread, have that central thread receive the results, set the position and limit, after which the central thread can initiate the I/O operation.

This can certainly be made to work.

But note what we just did. We now have an API where:

 - there are different "phases", where in one phase all the methods work, but in another phase only certain methods work (otherwise it breaks silently);

 - you have to carefully control all the code to ensure that the wrong methods aren't called when the buffer is in the wrong phase (otherwise it breaks silently); and

 - you can't hand off the buffer to a library (3rd party or JDK) without carefully orchestrating a transition into the right phase (otherwise it breaks silently).

Frankly, this is pretty crappy. It's certainly possible to work around it. People do, and it is painful, and they complain about it up and down all day long (and rightfully so).

Note that this discussion is based primarily on looking at the ByteBuffer API. I have not done extensive investigation of the impact of the various buffer views (IntBuffer, LongBuffer, etc.), nor have I looked thoroughly at the implementations. I have no doubt that we will run into additional issues when we do those investigations.

If we were designing an API to support multi-threaded access to memory regions, it would almost certainly look nothing like the buffer API. This is what Alan means by "buffers might not be the right answer." As things stand, it appears quite difficult to me to fix the multi-threaded access problem without turning buffers into something they aren't, or fragmenting the API in some complex and uncomfortable way.

Finally, note that this is not an argument against adding bulk absolute operations! I think we should probably go ahead and do that anyway. But let's not fool ourselves into thinking that bulk absolute operations solve the multi-threaded buffer access problem.

s'marks


Reply via email to