blocking peek for blocking queues
Jaikiran Pai
jai.forums2013 at gmail.com
Thu Apr 27 05:22:55 UTC 2023
Hello Mark,
core-libs-dev is a better place for this discussion
https://mail.openjdk.org/mailman/listinfo/core-libs-dev
-Jaikiran
On 27/04/23 8:21 am, mark.yagnatinsky at barclays.com wrote:
>
> I suspect this is the wrong list; if so I hope someone will redirect me.
>
> I’d like to make a case for adding a blocking variant of peek() to
> BlockingQueue.
>
> This has apparently been suggested before:
>
> https://bugs.openjdk.org/browse/JDK-6653412
>
> At the time, the verdict was that the proposed use case for a blocking
> peek was not very compelling.
>
> For one thing, that use case was inherently racy, and ideally the
> standard library should not be encouraging races.
>
> And for another thing, this was before the invention of default
> methods, so a new interface would be needed.
>
> I’d like to present what might be a more sympathetic use case, where a
> blocking peek actually makes it easier to avoid races.
>
> (This is not hypothetical; I’ve spent all day trying to cope with the
> lack of a blocking peek. I think I succeeded, but it was tough.)
>
> Let’s start at the beginning. The game known as “multithreaded
> programming” can be played on 3 difficulty levels: easy, medium, and hard.
>
> Easy mode is when the JDK happens to have a high-level tool (e.g., a
> Collection or an Executor or something) such as DelayQueue that does
> exactly what you want.
>
> Sadly, the JDK is finite, and even Maven Central is finite, so this
> not guaranteed to happen, though it’s nice when it does.
>
> At the other extreme is hard mode, when every nanosecond counts and
> you must resort to the sorts of mental gymnastics that were involved
> when String.hashCode was re-worked to avoid re-computing hash codes
> equal to zero.
>
> This email is about the medium difficulty level. In this mode, you
> must glue together two or three collections to get what you want.
>
> To be concrete, suppose we want to glue a map together with a blocking
> queue, since that’s a simplified version of what I was doing. (In
> particular I was using a DelayQueue and a HashMap.)
>
> Since we’re not playing on hard mode, we’re going to allow ourselves
> to guard the entire data structure by one giant lock whenever it seems
> convenient, without worrying about performance.
>
> In fact, let’s give a name to this lock. Since I’m not feeling very
> creative right now, let’s call it LOCK. Similarly, let’s call our
> compound data structure STRUCT.
>
> My usual heuristic to “win” (that is, to make sure my code is correct,
> or at least non-racy) on medium difficulty goes something like this.
>
> First, figure out whether there is any way for STRUCT to be “invalid”.
>
> For instance, maybe “it’s valid if and only if every entry in the
> queue has a corresponding entry in the map”.
>
> One could then write a brief comment in the code explaining what
> “valid” means, or perhaps even write an “isValid()” method for testing
> purposes.
>
> Once we’ve decided what it means for STRUCT to be “valid” (aka,
> non-corrupt), we can try to maintain the following invariant:
>
> EITHER some thread holds the LOCK, and no other thread is using STRUCT
> right now,
>
> OR ELSE the LOCK is not held by any thread, and thus STRUCT is
> currently in a valid state.
>
> Sometimes the invariant above is a bit too strong, and it’s convenient
> to weaken it slightly. So we can instead consider the following rule:
>
> RULE: we must never mutate STRUCT unless we hold the LOCK.
>
> If we uphold the invariant, then we must be following the rule, but
> not vice versa.
>
> (The rule doesn’t forbid queries without the lock; it only forbids
> writes.)
>
> Finally, we come to the point. The lack of a blocking peek in
> BlockingQueue has forced me to break the above “weakened” rule.
>
> I had to invent a new much weaker rule, something like “please
> carefully think through all possible interleaved executions; if they
> all work, great!”.
>
> What went wrong? Well, one of the methods I wanted on STRUCT was
> basically “call take() on the queue, and then do things with the result”.
>
> The trouble is that you can’t usually afford to hold a lock while
> calling take(), since it can block forever, and in the meantime you’ve
> “locked out” anyone who could have added something and thus unblocked you.
>
> Thus, I split my method into two pieces: In PART_ONE we call take()
> without the LOCK, and once take() returns I grab the lock right away
> and run PART_TWO.
>
> But notice that this breaks the RULE from the previous paragraph: I
> mutated the STRUCT without holding the LOCK.
>
> This means my method is not atomic: it’s possible for some other
> thread to observe that PART_ONE is done but PART_TWO is not started.
>
> Typically this means that other threads can now observe the STRUCT in
> an invalid (aka corrupt/broken) state.
>
> Now, suppose we had a blocking peek. How does that help? In that
> case, we would still break our method into two pieces.
>
> To avoid confusion, let’s call them PART_A and PART_B. In PART_A we
> call blockingPeek() without holding the LOCK.
>
> Note that we completely ignore the return value from peek; for all we
> care, the return type is “void” instead of “T”.
>
> This breaks the “invariant” from a few paragraphs ago since we’re
> technically “using” the STRUCT but it respects the “rule” since we’re
> not mutating it.
>
> Once peek returns, PART_B begins. Here, we grab the lock, and do a
> non-blocking poll() on the queue.
>
> It’s might return “null” if someone raced us to get the lock. But
> from our perspective that’s okay: it’s just like a spurious wakeup
> from a raw wait() call.
>
> The reason it’s okay is that we haven’t done anything yet, so we can
> just call blockingPeek() again and hope for better luck next time.
>
> Similarly we don’t care if poll() returns a different item than
> blockingPeek() originally did, since we never looked at what peek()
> returned.
>
> The rest of PART_B is basically PART_TWO from the “peek-less” world.
>
> Note that this way, our method appears atomic to other threads, since
> PART_A doesn’t actually do anything; all the work happens in PART_B,
> which is guarded by the LOCK.
>
> Thoughts?
>
> This message is for information purposes only. It is not a
> recommendation, advice, offer or solicitation to buy or sell a product
> or service, nor an official confirmation of any transaction. It is
> directed at persons who are professionals and is intended for the
> recipient(s) only. It is not directed at retail customers. This
> message is subject to the terms at:
> https://www.cib.barclays/disclosures/web-and-email-disclaimer.html.
>
> For important disclosures, please see:
> https://www.cib.barclays/disclosures/sales-and-trading-disclaimer.html
> regarding marketing commentary from Barclays Sales and/or Trading
> desks, who are active market participants;
> https://www.cib.barclays/disclosures/barclays-global-markets-disclosures.html
> regarding our standard terms for Barclays Corporate and Investment
> Bank where we trade with you in principal-to-principal wholesale
> markets transactions; and in respect to Barclays Research, including
> disclosures relating to specific issuers, see:
> http://publicresearch.barclays.com.
> __________________________________________________________________________________
>
> If you are incorporated or operating in Australia, read these
> important disclosures:
> https://www.cib.barclays/disclosures/important-disclosures-asia-pacific.html.
> __________________________________________________________________________________
> For more details about how we use personal information, see our
> privacy notice:
> https://www.cib.barclays/disclosures/personal-information-use.html.
> __________________________________________________________________________________
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/jdk-dev/attachments/20230427/537d0ce4/attachment.htm>
More information about the jdk-dev
mailing list