Hacker News new | past | comments | ask | show | jobs | submit login
Latches in C++20 (modernescpp.com)
106 points by ibobev on Jan 26, 2021 | hide | past | favorite | 83 comments



I'm a little disappointed in the name because latch already has a specific meaning for computer engineers. It is one of the first memory circuits we study and forms the basis for registers.


Not meaning to disregard your point, but the class doing the same thing as `std::latch` in the JDK is called `CountDownLatch`[1] and is used in a similar way:

    CountDownLatch workDone = new CountDownLatch(6);
    workDone.countDown(); // mark an event
    workDone.await(); // wait until the counter reaches zero
Even if latches also refer to another concept, it's still positive to see some naming consistency across languages. What name would you have preferred?

[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.base...


That's a barrier.

A latch is a circuit that takes on a value (high or low, 1 or 0) when some some gating signal arrives (like a clock pulse) and then holds that value after the input is removed.

It is named after a door latch; a door latches when you close it and then holds that state: you can't pull it open again without using the handle/knob.

If something is called "latch" which does not change its state once in order to reflect an input event, and then hold that state until explicitly recent, then it's misnamed due to abusing the metaphor.

The barrier metaphor is the right one for an object that is hit some predetermined number of times and then fires an event (like releasing some waiting thread(s)). That predetermined number of events is its "barrier potential": the threshold that must be met to break through the barrier.


I see what you mean, but since the topic is concurrency "barrier" is also a term used for a different concept in this domain: memory barriers. N=1 of course but if I was asked what a barrier was in the context of concurrency I would immediately think of memory barriers, not the system we're talking about here.


POSIX has had barriers since around 2000 or so: pthread_barrier_wait and friends.

Memory barriers and POSIX-style barriers are semantically related; both mechanisms ensure that certain events are not re-ordered between adjacent phases; they stay on their side of the barrier.


Yes; the C++ multithreading support owes a lot (among many influences) to pthreads and one reason that low level memory barriers were called fences was to reserve the barrier name to a possible pthread-like construct.


Memory barriers also being called fences ("fence instruction") long precedes C++ multithreading.

(That conflicts with "fence register"; a concept in non-virtualized memory management whereby each running process is confined to accessing range of memory delimited by values held in fence registers.)


This doesn't change state after being signaled, hence why it's called a latch.

The reusable variant is called a std::barrier.


It seems to me the only difference is the number of input values. N inputs latches open (N can be 1).


I have limited knowledge of the Java and C++ libraries but I believe Go has something similar called a WaitGroup.


Had a mentor who said one of the biggest problems in computer science is that all the good metaphors are already taken.


But in this case the concept already had a name in CS and it was called a barrier (which is a pretty apt name too). For some rename they called it a latch and then they called a cyclic barrier a barrier...


As far as I can tell, a (countdown) latch is different from a barrier.

In a barrier signaling and waiting is a single atomic operation. Each thread (of a group of N) reaching a barrier will wait until all N threads have reached an waited (and implicitly signaled) it.

A countdown latch is an event that will release one (or more) waiter only after has been signaled N times. Signaling and waiting threads are not necessarily the same and often are distinct sets.

I guess you could build a barrier from a countdown latch, but I suspect that a trivial mapping of barrier::wait to latch::signal+wait is going to racey and you need an additional sinchronization primitive. For example pthread_barrier_wait requires an additional mutex (similar to condition variables).

edit: std::barrier has (optionally) separate singal+wait and uses an explicit arrival token instead of a mutex to tie the signaling with waiting.

In practice a barrier and a countdown latch are used for different purposes (the former to coordinate multiple symmetric threads across phases of a distributed computation, the latter to wait for completion of N events).


> In a barrier signaling and waiting is a single atomic operation. Each thread (of a group of N) reaching a barrier will wait until all N threads have reached an waited (and implicitly signaled) it.

Yeah that's exactly what std::latch::arrive_and_wait does.

I don't think atomicity is a thing here though... arrive_and_wait() is just count_down() followed by wait().

> A countdown latch is an event that will release one (or more) waiter only after has been signaled N times. Signaling and waiting threads are not necessarily the same and often are distinct sets.

C# still calls this a Barrier, except that it requires one of the waiters to be a participant (see AddParticipant() and SignalAndWait()): https://docs.microsoft.com/en-us/dotnet/api/system.threading...

They even allow removing participants!

This is a little bit like a mutex that has a try_lock. It's not strictly the vanilla Computer Science "mutex" with lock/unlock per se, but it's not a fundamentally different concept; it's just a handy yet pretty close generalization. I guess if you really want to give this a new name then maybe it's not a terrible idea given it's a slight generalization of a barrier, but latch is certainly not going to be any more accurate (or less confusing) than barrier.


Yes, if you can't reset the latch, I don't think there is actually any race. I didn't realize that latch had an arrive and wait in fact.


I don't know who said the following, but it sums up CS pretty good:

"There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors."


It's interesting how we've used all the various 'doing' words in English for different technical concepts, although with some overlap. Function, method, procedure, action, operation, routine, task, process.


Note the name isn't new for this concept, Java also uses the latch name to describe this: https://docs.oracle.com/javase/7/docs/api/java/util/concurre...


Apparently databases historically call shared-memory locks "latches" to distinguish them from locks used for transaction conflict detection.


Yeah this confused me too. Not sure what made them think this resembles a latch. A latch only has 2 states, not 2^n states.


The full name is typically CountDownLatch [0]. The latch still has 2 states, closed and open. It's just that it doesn't open until everyone is ready.

[0] https://docs.oracle.com/en/java/javase/15/docs/api/java.base...


That's like saying every integer has 2 states, zero and nonzero. I'm talking states in the computer science sense, not in the "what I prefer to look at" sense. These have 2^n states, whether you choose to care about them or not.

Just because Java calls it something that doesn't mean it is "typically" called that thing.


Representing it as a "state machine" it would still have only 2 states. Open and closed.

I personally "prefer to look at" integers as not having state.

> Just because Java calls it something that doesn't mean it is "typically" called that thing.

It's only been there since 2004 and is named the same in C#


> Representing it as a "state machine" it would still have only 2 states. Open and closed.

No. There are lots of "open" states with different counter values. That you choose to call them all "open" and not observe the differences doesn't change this. It sounds like you've never studied state machines, so I recommend reading up on them; here's a starting point: https://en.wikipedia.org/wiki/State_(computer_science)#Finit...

> It's only been there since 2004 and is named the same in C#

No it's not... where did you get this? C# has CountdownEvent and Barrier. It's not called "latch".

And, for that matter, OpenMP also has #pragma omp barrier since 1998... https://www.openmp.org/wp-content/uploads/cspec10.pdf#page=2...

There's no point continuing this argument so let's just leave it here.


Please avoid swipes in your comments like "It sounds like you've never studied state machines", "where did you get this?" and "There's no point continuing this argument so let's just leave it here". They invariably land with much more force on the other person than you intend, and this leads to much worse threads. It's easy to understand see how this post would turn into a provocation.

https://news.ycombinator.com/newsguidelines.html


> There's no point continuing this argument so let's just leave it here.

Yeah cause you're insufferable.


Ouch, please don't cross into personal attack, regardless of how annoying another comment is or you feel it is! We ban accounts that do that. Fortunately your comment history looks like a good one otherwise. If you wouldn't mind reviewing https://news.ycombinator.com/newsguidelines.html and sticking to the rules when posting here, we'd be grateful.


It makes sense when compared against flip-flop circuit https://www.geeksforgeeks.org/difference-between-flip-flop-a...

Most importantly, latch is level-triggered, just like the synchronization primitive.


latches can be edge or level triggered. The important concept is that once they are triggered, they latch the input value, and maintain it after the input has disappeared.


That's what std::latch does... Once triggered it's always triggered. Same as Java's CountDownLatch


The thing is, pretty much every object you work with in your programming language latches the most recently stored value in it. The machine is built on latching as a fundamental block.

A simple mutex latches open when you unlock it, and then latches locked when something locks it again.


No, I mean, a std::latch cannot be reset. It's a one-direction one-use toggle. There's no unlocking or relocking. There's no "again" to it.


Latches can be reset; so bad metaphor on that count also.

Something that triggers, flips state and cannot be reset might be called a "fuse".

That could work here in another way: since there is a count-down, that's like a dynamite fuse burning.


> It makes sense when compared against flip-flop circuit

It doesn't to me... I'm not following unfortunately.

Barriers are level-triggered too. Like, physical ones. Push with enough force against a barrier and it'll break down.


The name makes a bit of sense, in the sense that once it's signaled, it stays that way. So the signaled state is latched.

This contrasts to std::barrier which auto-resets once signaled[1].

As such I think it would be better if they had called it std::latched_barrier or similar.

[1]: https://en.cppreference.com/w/cpp/thread/barrier


And barrier instructions/semantics are used by many architectures to flush/partition pipelines. AFAICT this one doesn't do that.

Too bad they couldn't qualify it somewhere further down below std::. Something like std::thread::barrier or similar might be easier to understand.


Unfortunately, nested namespaces can have some surprising and unintended consequences. You might find https://abseil.io/tips/130 interesting. Given the author was until very recently the chair of the C++ LEWG, I imagine he's had some influence on how what namespaces new types ended up in.


However, the barrier metaphor is correct. The counter reaches a certain level (the barrier potential, get it?) upon which the object triggers.


It will also be annoying for HW designers ... latches and flip-flops have a very specific meaning in their world.


You repeat OP 1:1.

Computer engineers are HW designers, may not go into that field but it's pretty engrained in their courses.

source: am computer engineer.


The name has been overloaded ;)


It sounds like "latch" is basically the same thing as an "event" (as in CreateEvent() in Windows, eventfd in Linux), but maybe optimized for single-process usage?

Edit: Ok so a (Windows) event is one bit, a C++ latch is a one-shot counter, and a C++ barrier is a cyclic counter. But I thought a barrier in general computer science terms is just a one-shot counter, which is what they're calling a latch in C++?


Does someone have any idea why latches do not have also count up operation alongside count down? In one of my previous jobs, I implemented a latch with a mutex, condition variable, and a counter and I supported also count up. It seems to work OK and count up was an essential operation for the use case I had needed it.

    class ThreadLatch
    {
    public:
        ThreadLatch(std::size_t count = 0)
            : _count(count) {}

        void inc()
        {
            std::lock_guard<std::mutex> lock(_mutex);
            ++_count;
        }

        void dec()
        {
            std::lock_guard<std::mutex> lock(_mutex);
            assert(0 != _count);
            --_count;
            if(0 == _count)
            {
                _condition.notify_all();
            }
        }

        void wait()
        {
            std::unique_lock<std::mutex> lock(_mutex);
            while(_count > 0)
            {
                _condition.wait(lock);
            }
        }

    private:
        std::mutex _mutex;
        std::condition_variable _condition;
        std::size_t _count;
    };


What's the difference with a semaphore?


A semaphore can be used to control thread access to a resource.

A latch allows one or more threads to wait until a set of operations being performed in other threads completes [0].

[0] https://docs.oracle.com/javase/8/docs/api/java/util/concurre...


A semaphore allows threads to pass until the counter reaches zero.

A barrier (what this thing is) blocks threads until the counter reaches zero.


Why use the bank account example to introduce a problem and then never resolve it with latches?


How are latches implemented? Is it more efficient than just a condition variable?


Check out futexes [1]. I did a quick scan of the llvm libcxx sources and latch and other threading primitives are based on this syscall.

[1] https://en.wikipedia.org/wiki/Futex


Right, and futexes also back condition variables in glibc.


And mutexes too!


I prefer the modern nutex construct


Probably atomic increment / decrement + condition_variable. Just check if you hit 0 on decrement to notify waiters (or return if you are the waiter).


Well now that sounds like a semaphore!


Almost certainly. Condition variables are very complicated internally. On Linux, a latch could be backed by a futex with very little code.


What is the source of the complexity inside condition variables? Reading the glibc sources, the main problem seems to be ensuring that signals are delivered properly?


Yes, atomic unlock + wait is the complex bit. Prior to Vista, Windows did not have a CV primitive. Existing emulations used multiple kernel objects and their correctness was difficult to verify. Performance also suffered.


I think Vista implemented keyed synchronization objects (i.e. a flavour of futexes) which have similar edge triggered semantics as CVs.


It's possible to implement very simple condition variables. The tricky part is preventing correct but inefficient behaviours like stampeding herds.


When implementing posix semantics there are a lot of nasty corner cases, especially with dealing with fairnes, graceful destruction, correct handling of wait generations and probably more that I'm missing.

I would never attempt to implement a full condvar.


Typical implementations use std::atomic or a variation of it (further stripped version of it).


"When you think about this workflow, you may notice that it can be performed without a boss." hehe


Is that just the same as

    ws.Add(1)
    [...]
    wg.Done() 
in Go?


In most cases I think they're identical. WaitGroup has an ability that latch doesn't: WaitGroup can Add() and subtract (Add() with negative) whereas latch can only subtract (latch has to be initialized with its maximum value). The fact that latch is initialized with its value could be a little more convenient than WaitGroup in some situations (save 1 line of code). latch has an ability that WaitGroup doesn't: .try_wait(). Also latch's .arrive_and_wait() can save a line of code compared to WaitGroup.


Go's approach seems a little more prone to user error. I know I've been tripped up by not explicitly adding a worker.


Saving lines of code in C++ compared to Go is like fighting windmills.


Not quite. It is not about reducing the program line count, but rather about reducing cognitive load for writing correct code. If you have to specify the initial count, then you cannot introduce an error where setting the initial count is forgotten.


Small typo above but it seems like a different approach to mutex. What's similar is basically a count of operations that's locked between threads/routines.


Haha yes sorry, a Freudian slip of me working with web sockets at the moment.


Does the Java ecosystem offer something similar?


Isn't CountDownLatch [0] the same thing in Java?

[0]: https://docs.oracle.com/javase/7/docs/api/java/util/concurre...


It is exactly that.


I love multithreading. So much fun.


And that's why I love Java. Somehow it's been there for almost 20 years.


Worth noting Doug Lee's early work on adding concurrency to Java. That effort resulted in the java.util.concurrency package but work started in the late nineties and it was available as a separate library. And of course it included a Latch class; and the related CountDownLatch.

http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/co...

Before this stuff landed, Java's concurrency model was a lot less nice to deal with. Good high level abstractions are important. Nice to see the same kind of primitives are being added to C++.


Glib::Cond has existed for longer, and is just a wrapper around pthread condition variables that go back to the 1980s.

Several other C++ libraries also provided condition variables, which latches are a variant of, along with barriers and flex_latches.


std::condition_variable existed since c++11, so std::latch must provide some benefit beyond it.


The semantics of latch are a little simpler, which leaves room for a lighter weight implementation than is typically associated with condition variables. Barriers are closer, but also have slightly simpler semantics, again allowing for a potentially lighter weight implementation.

But the semantics are not sufficiently different as to allow for any notably new use cases/code flow, certainly not from the examples I've seen. Every one of them could have been written using condition variables at any point in the last 30 years and would look almost indistinguishable.


Perhaps differentiation is justified as program semantics can be captured to allow optimizations in runtimes. For example, condition variable to wait for events which can be delayed vs latched to wait for arrival of running threads to a point of execution. Runtime should handle each uniquely.


Why I like Java, by Mark Dominus https://blog.plover.com/prog/Java.html

"I enjoyed programming in Java, and being relieved of the responsibility for producing a quality product."


This is something I always read from people who produce a puddle of noodle soup that keeps crunching through corner cases instead of producing something structured and actually readable by human beings.


(2014)

Yeah java 6/7 was verbose, but it's nothing representative of the language now. You might as well point out the problems of C++03. Streams, lambdas, method references, records, and local variable inference make it pretty fluent to write and at the very least it has a sane standard library (I can't say as much for Scala).

Also as for their intro problem what type of Java programmer worth their salt can't do:

    while (true)
        System.out.print(System.in.read());


Very poorly and confusingly named.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: