Speaker 1: Welcome to the deep dive. We've got some really interesting
Speaker 1: material you sent over on concurrency in modern C plus
Speaker 1: plus POM. Looks like it's mostly drawing from the book
Speaker 1: Concurrency with Modern C plus plus POM.
Speaker 2: That's right, And our goal today is well, to unpack
Speaker 2: the core ideas, maybe find some of those aha moments
Speaker 2: for you.
Speaker 1: Yeah, make this whole complex topic a bit more accessible
Speaker 1: without drowning in the jargon exactly.
Speaker 2: And you know, the book itself kind of hints at
Speaker 2: the challenge. It mentions how the C plus plus memory
Speaker 2: model often runs counter to our intuition.
Speaker 1: Oh. Interesting, So that's our mission then, to navigate that
Speaker 1: complexity and pull out the essentials you need for writing
Speaker 1: you know, solid concurrency.
Speaker 2: Plus plus code, precisely efficient, dependable code. That's the end.
Speaker 1: Okay, let's get started then, right at the foundation the
Speaker 1: memory model. Yeah, in simple terms, what is it we're
Speaker 1: trying to wrap our heads around here.
Speaker 2: Well, think of the memory model as like the official
Speaker 2: rule book. It dictates how different threads in your program
Speaker 2: see and interact with the computer's memory.
Speaker 1: Okay, rules for memory interaction.
Speaker 2: And from a concurrency angle, two basic questions pop up. First,
Speaker 2: what counts as a single place in memory a memory location.
Speaker 1: Right, is it a bite an integer?
Speaker 2: According to the source, Yeah, it's either a basic scaler
Speaker 2: type like your ince floats, pointers, enoms, or if you
Speaker 2: have bitfields, it's the largest sort of continuous sequence of
Speaker 2: those bits.
Speaker 1: Got it. Scaler types are contiguous bitfields. What was the
Speaker 1: second question?
Speaker 2: Ah, the big one. What happens when multiple threads try
Speaker 2: to access that same memory location around.
Speaker 1: The same time. Okay, and I sense danger here?
Speaker 2: You got it? That leads us straight to data races. Okay,
Speaker 2: imagine two threads hitting the same shared variable. It's mutable,
Speaker 2: and at least one of those threads is trying to
Speaker 2: write to it.
Speaker 1: That's data race.
Speaker 2: That's the data race, and the result undefined behavior. Ah,
Speaker 2: the dreaded ub exactly the wild West. Your program might crash,
Speaker 2: spit out garbage, or maybe even seem to work fine
Speaker 2: for a while, then just fail later completely out of
Speaker 2: the blue.
Speaker 1: So that's why we need things like mute texts and locks. Yeah,
Speaker 1: to coordinate who gets access when precisely.
Speaker 2: There are the traffic cops for shared data access essential tools.
Speaker 1: The book uses thread safe singleton initialization as a classic example.
Speaker 1: Why is that such a good illustration. It seems simple, right,
Speaker 1: just one instance.
Speaker 2: Well, it seems simple in a single thread, but imagine
Speaker 2: multiple threads all deciding, hey, I need the singleton at
Speaker 2: the exact same time.
Speaker 1: Ah. So if they all check and see it doesn't
Speaker 1: exist yet, they.
Speaker 2: Might all try to create it, and suddenly you've got
Speaker 2: multiple singletons, which completely breaks the whole idea.
Speaker 1: Right, So, thread safe techniques make sure only one thread
Speaker 1: actually does the creation, even if many try.
Speaker 2: Exactly ensure as it's created exactly once.
Speaker 1: Now for digging deeper, the book mentions a tool called creepmem.
Speaker 1: What's that about?
Speaker 2: Oh, CRIPPYMM is fantastic for this. It's like a sandbox
Speaker 2: or a simulator for the C plus plus memory model.
Speaker 1: Okay.
Speaker 2: You feed it small snippets of concurrent code, and it
Speaker 2: shows you all the possible ways the operations from different
Speaker 2: threads could interleave. It visualizes the impact of different memory orderings.
Speaker 1: So you can actually see how things might go wrong
Speaker 1: or why a certain ordering works precisely.
Speaker 2: It helps build that intuition for how the memory model behaves, which,
Speaker 2: as we said, isn't always obvious.
Speaker 1: Really valuable tool, okay, memory model basics covered, let's talk
Speaker 1: about the threads themselves. We've had std dot thread for
Speaker 1: a while clus plus twenty added std dot j thread.
Speaker 1: What's the leap forward there?
Speaker 2: The big difference really is resource management safety.
Speaker 1: Also with sdd.
Speaker 2: Dot thread, you the programmer must remember to either join
Speaker 2: the thread, wait for it to finish, or detach it
Speaker 2: to run independently.
Speaker 1: And if you forget, if the std.
Speaker 2: Dot thread object gets destroyed before you do either, your
Speaker 2: program terminates. It's a common mistake.
Speaker 1: Ouch. Okay, So how does std dot j thread fix that.
Speaker 2: It's our Aii based resource acquisition is initialization. When an
Speaker 2: std dot j thread object goes out of scope, its
Speaker 2: destructor automatically calls joint.
Speaker 1: No more forgetting Nice. That sounds much safer it is.
Speaker 2: Plus, std dot j thread has built in support for
Speaker 2: cooperative interruption, a clean way to ask a thread to stop.
Speaker 1: Okay, cooperative interruption. We'll probably circle back to that now.
Speaker 1: The book mentioned something tricky with std dot shared ptr.
Speaker 1: I thought they were threads safe.
Speaker 2: They help with memory management in threads. Yes, they prevent
Speaker 2: leaks by managing the object's lifetime automatically, but the shared
Speaker 2: pointer itself isn't fully thread safe for all operations.
Speaker 1: What's the catch?
Speaker 2: It's the internal reference counter. If you have multiple threads
Speaker 2: all trying to say, assign a new shared pointer to
Speaker 2: the same shared pointer variable, especially if it was passed
Speaker 2: by reference, we could corrupt the count exactly. You can
Speaker 2: get a data RaSE on that internal counter. The book
Speaker 2: shows an example where this happens when threads modify a
Speaker 2: shared shared ptr passed by reference. The object being pointed
Speaker 2: to might be fine, but the pointer's bookkeeping gets messed up.
Speaker 1: So if I need multiple threads to safely update which
Speaker 1: object to shared pointer points to, what's the solution.
Speaker 2: The book suggests using std dot atomic store for that
Speaker 2: specific case to make the update atomic, but it also
Speaker 2: points out that this is kind.
Speaker 1: Of a workaround. What's the real fix? Then?
Speaker 2: Ideally we'd use atomic smart pointers like std dot atomas
Speaker 2: esdd dot shared ptr, which C plus plus twenty introduced
Speaker 2: that handles the atomicity of the pointer operations themselves.
Speaker 1: Okay, that makes sense. The book also mentioned std dot
Speaker 1: atomic cref.
Speaker 2: What's that for, ah, atomic cref. That's pretty neat. It
Speaker 2: lets you perform atomic operations on an existing object that
Speaker 2: wasn't originally declared. Std dot atomic, so you.
Speaker 1: Can temporarily treat a regular variable as atomic sort of.
Speaker 2: Yeah, you created an automic craft to it, and then
Speaker 2: you can use atomic operations like fetchad or compare exchange
Speaker 2: strong directly on that underlying variable through the reference. The
Speaker 2: example showed incrementing a counter inside some big object without
Speaker 2: needing locks or making the whole object atomic.
Speaker 1: Interesting, so careful management is key. This leads us nicely
Speaker 1: into memory ordering, sequential consistency, acquire release, relaxed. These sound
Speaker 1: like different levels of rules.
Speaker 2: They are. They're different contracts, different guarantees about how memory
Speaker 2: operations become visible across threads.
Speaker 1: Let's start with the strictest sequential consistency memory order.
Speaker 2: Seconds, right, That's the default for atomics, and it's the
Speaker 2: easiest to reason about it. Basically, guarantees two things. One,
Speaker 2: all threads agree on a single global order of all
Speaker 2: sequentially consistent operations, and two, the operations within any single
Speaker 2: thread happen in the order you wrote them in your code.
Speaker 1: Like one single timeline for everything.
Speaker 2: Exactly simple model, but it can sometimes have performance costs
Speaker 2: because the hardware has to work harder to maintain that
Speaker 2: global order.
Speaker 1: Okay, what about acchore release semantics. Then sounds like it
Speaker 1: loosens things up a bit.
Speaker 2: It does acquoire, release memory order, require memory order, release
Speaker 2: memory order, roll, and also consume, though that's trickier. Focuses
Speaker 2: on synchronization between operations on the same atomic.
Speaker 1: Variable, same variable, okay.
Speaker 2: Release operation. Typically a right ensures that all memory rights
Speaker 2: that happen before it in the same thread become visible
Speaker 2: to other threads that later perform an acquire operation usually
Speaker 2: read on that same atomic variable.
Speaker 1: So the release makes prior rights visible and the acquire
Speaker 1: sees them precisely.
Speaker 2: This creates what's called a synchronizes with relationship. It's fundamental.
Speaker 2: The book points out this is how mutexes, thread joins,
Speaker 2: condition variables, all the higher level stuff actually works under
Speaker 2: the hood. A lock release synchronizes with a subsequent.
Speaker 1: Lock acquire that synchronizes with it. Sounds important. It establishes
Speaker 1: order across threads.
Speaker 2: Yes, it establishes A happens before relationship. If action A
Speaker 2: synchronizes with action B, then A happens before B. This
Speaker 2: guarantees visibility of memory changes.
Speaker 1: Got it? Now? What about the most lenient one memory
Speaker 1: order relaxed? What guarantees do we lose there.
Speaker 2: With relaxed ordering, you only get the bare minimum the
Speaker 2: operation itself as atomic. It happens indivisibly, and there's a
Speaker 2: single modification order for that specific atomic variable. All threads
Speaker 2: will agree on the sequence of values written to that one.
Speaker 1: Variable, but no guarantees about other memory operations exactly.
Speaker 2: Relaxed operations don't create synchronizers with relationships. They don't guarantee
Speaker 2: anything about the visibility or ordering of other reads and writes,
Speaker 2: even to the same variable by different threads or to
Speaker 2: different variables.
Speaker 1: So potentially faster, but much harder to reason about, much harder.
Speaker 2: The book shows using fetchad with relaxed ordering for a
Speaker 2: simple counter, which is a common use case, but it
Speaker 2: also warns that you can still get data rass on
Speaker 2: non atomic variables even if you're reading related atomics with
Speaker 2: relaxed order, because there's no happens before relationship established. It's
Speaker 2: subtle stuff.
Speaker 1: And where do memory fences? Atomic thread fens.
Speaker 2: Fit in fences acts like barriers. They enforce ordering constraints
Speaker 2: between operations before the fence and operations after the fence,
Speaker 2: even across different variables or relaxed atomics. A release fence
Speaker 2: makes prior rights visible to threads that later cute and
Speaker 2: acquire fence. It's another way to establish that synchronizes with relationship,
Speaker 2: but without needing a specific atomic variable to mediate.
Speaker 1: Okay, that's a lot to digest on ordering. Let's shift
Speaker 1: to actually using threads. How do we launch them? What
Speaker 1: are the options?
Speaker 2: The main way is std dot thread. Its constructor can
Speaker 2: take basically any callable thing hollible thing. Yeah, like a
Speaker 2: regular function pointer or a function object you know, an
Speaker 2: object where you've overloaded the parentheses operator, or very commonly
Speaker 2: a lambda function.
Speaker 1: Right. Lambas are handy there.
Speaker 2: Super handy. You just passed the function or lambda you
Speaker 2: want to run in the new thread, followed by any
Speaker 2: arguments it needs. The book shows a simple Hello from
Speaker 2: thread using a lambda.
Speaker 1: And once it's running, we have to decide what happens
Speaker 1: when it finishes. Join or detach.
Speaker 2: Exactly, You have to make a choice before the std
Speaker 2: dot thread object itself is destroyed. Join means the current
Speaker 2: thread waits right there until the launched thread completes.
Speaker 1: Useful if you need its result or need to know
Speaker 1: it's done before cleaning up resources.
Speaker 2: Precisely. Detach. On the other hand, lets the thread run
Speaker 2: completely independently in the background. The original thread continues immediately.
Speaker 1: But that sounds risky. What if the detached thread needs
Speaker 1: data that the original thread owns.
Speaker 2: That's the big danger. If the original thread finishes and
Speaker 2: its data goes out of scope, but the detached thread
Speaker 2: is still running and tries to access that data, Boom,
Speaker 2: undefined behavior again. So the book advises joining, usually strongly
Speaker 2: advises joining, especially if the thread interacts with data whose
Speaker 2: lifetime is tied to the scope where the thread was created.
Speaker 2: Detaching requires very careful management of lifetimes.
Speaker 1: Makes sense. What about std dot thread dot hardware concurrency.
Speaker 2: It's said to us it gives you a hint, basically,
Speaker 2: an estimate of how many threads the hardware can genuinely
Speaker 2: run in parallel, often related to the number of CPU
Speaker 2: cores or hyperthreads.
Speaker 1: A hint, not a rule, definitely just a hint.
Speaker 2: The optimal number of threads depends heavily on the specific task, io, contention,
Speaker 2: et cetera. Using exactly this number isn't always best. The
Speaker 2: book mentions, it's just a starting point, a native handle
Speaker 2: that's an escape patch. It gives you direct access to
Speaker 2: the underlying operating systems thread handle like a thread on
Speaker 2: Linux or a handle on Windows. If you need to
Speaker 2: do something platform specific that the C plus plus standard
Speaker 2: library doesn't cover, use with caution though.
Speaker 1: Okay, got it. Let's move on to the tools we
Speaker 1: use with threads synchronization primitives, starting with the most basic
Speaker 1: STD mutex.
Speaker 2: Right, the mutex its core job is mutual exclusion, protecting
Speaker 2: shared data.
Speaker 1: How does it do that?
Speaker 2: Think of it as a lock guarding a piece of data.
Speaker 2: Before a thread can touch that data, it has to
Speaker 2: lock the mutex. If another thread already holds the lock,
Speaker 2: the first thread weights. Once it's done, it must unlock
Speaker 2: the mutex, allowing another waiting thread to proceed.
Speaker 1: So only one thread gets access at a time. Prevents
Speaker 1: data rases on that protected data exactly.
Speaker 2: Mutexes are your go to for protecting shared mutable state
Speaker 2: first line of defense.
Speaker 1: But the book warns about deadlocks. How did mutexes lead
Speaker 1: to that?
Speaker 2: Ah? The classic deadlock scenario. Imagine thread one locks mutex A,
Speaker 2: then tries to lock mutex B. Simultaneously, Thread two locks
Speaker 2: mutex B, then tries to lock mutex A.
Speaker 1: Oh. Thread one has A and wants B. Thread two
Speaker 1: has B and wants a.
Speaker 2: And they're stuck. Neither can proceed because it's waiting for
Speaker 2: the resource the other one holds. That's a deadlock. They
Speaker 2: wait forever, masty.
Speaker 1: How do we avoid that when we need multiple locks?
Speaker 2: The standard solution is std dot lock. You pass it
Speaker 2: all the mutexts you need to acquire. It uses a
Speaker 2: deadlock avoidance algorithm internally to try and lock all of them.
Speaker 1: Atomically atomically, meaning it gets all of them or none
Speaker 1: of them.
Speaker 2: Essentially. Yes, it guarantees it won't end up in a
Speaker 2: state where it holds some locks while blocking waiting for
Speaker 2: others in a way that contributes to deadlock. If it
Speaker 2: can't get all locks, it'll release any it acquired and
Speaker 2: try again, or perhaps throw an exception, depending on the context.
Speaker 1: Okay, so std dot lock for multiple mutexes. Yeah, good tip.
Speaker 1: We mentioned threads saf initialization earlier. Besides a simple lock.
Speaker 1: What other techniques does the book cover?
Speaker 2: Several good ones. If something can be a const expert,
Speaker 2: its value is fixed at compile time, so that's inherently thread.
Speaker 1: Safe, right, no runtime race possible.
Speaker 2: Well, then there's std dotkalents with the std dot once flag.
Speaker 2: You pass it a flag and a function like your
Speaker 2: initialization function. The standard guarantees that function will be executed
Speaker 2: exactly once by the first thread that calls it, even
Speaker 2: if many threads call it concurrently, other threads will wait
Speaker 2: until the first one is done.
Speaker 1: Okay, that sounds robust.
Speaker 2: Very Another common C plus plus idiom, especially since C
Speaker 2: plus plus eleven, is the Meyers singleton. Using a static
Speaker 2: variable inside a function.
Speaker 1: Like static my singleton instance return.
Speaker 2: Instance exactly that. The language guarantees that the initialization of
Speaker 2: that static local variable is thread safe. The compiler and
Speaker 2: runtime handle the locking implicitly. It's often the simplest and
Speaker 2: preferred way.
Speaker 1: Now simple as good any others.
Speaker 2: Well of all, if your program structure allows it is
Speaker 2: just initialize the shared resource in your main thread before
Speaker 2: you create any other threads. No concurrency Doing initialization means
Speaker 2: no problem?
Speaker 1: Fair enough? What about signaling between threads, like, hey, the
Speaker 1: data you're waiting for is ready. That's std dot condition
Speaker 1: variable precisely.
Speaker 2: Condition variables let threads weight efficiently until some condition becomes true.
Speaker 1: How do they work? Do they need a mutex?
Speaker 2: Yes? They always work together with the mutex. A waiting
Speaker 2: thread must first lock the mutex protecting the shared state
Speaker 2: at the condition. Then it calls weight on the condition variable,
Speaker 2: and weight does what. It atomically releases the mutex and
Speaker 2: puts the thread to sleep. It waits until another thread notifies.
Speaker 1: It notifies it how by calling.
Speaker 2: Notify one or notifile on the same condition variable. When
Speaker 2: the waiting thread wakes up, it automatically reacquires the mutex
Speaker 2: before weight returns.
Speaker 1: Okay, it wakes up, gets the locked back. Then it
Speaker 1: can check the condition exactly.
Speaker 2: And this is crucial. It must check the condition again
Speaker 2: after waking up.
Speaker 1: Why didn't get notified because the condition is true.
Speaker 2: Not necessarily, you can get spurious wakeups where the thread
Speaker 2: wakes up even though no notification happen or the condition
Speaker 2: changed back. That's why weight functions usually take a predicate,
Speaker 2: a lambda or function that checks the actual condition. The
Speaker 2: weight will only return if the predicate is true or
Speaker 2: if interrupted.
Speaker 1: Ah, so the predicate handles spurious wakeups. Never wait with
Speaker 1: that one.
Speaker 2: That's the rule. Always weight with the predicate.
Speaker 1: Now C plus plus twenty brought cooperative interruption std dot
Speaker 1: stop source stop token. How does that fit in? Especially
Speaker 1: with j thread and condition.
Speaker 2: Very blany right, This is a much better way to
Speaker 2: ask threads to stop than say, just setting a boolean flag.
Speaker 2: It's more integrated.
Speaker 1: How does it work.
Speaker 2: You create a std dot stop source. This object can
Speaker 2: request that associated operations stop. From the stop source, you
Speaker 2: get std dot stop tokens. You pass these tokens to
Speaker 2: the threads or operations.
Speaker 1: You might want to interrupt, and the thread checks up
Speaker 1: the token.
Speaker 2: Yes, a thread can periodically call stop requested on its token.
Speaker 2: Or even better, many blocking functions, like the weight functions
Speaker 2: on std dot condition variably needs, and the ones in
Speaker 2: J thread implicitly can accept a stop token. They'll automatically
Speaker 2: wake up if a stop is requested on that token.
Speaker 1: So J thread uses this automatically.
Speaker 2: J thread has a stop source built in. If you
Speaker 2: create a J thread with a function that takes a
Speaker 2: stop token as its first argument, the J threads destructor
Speaker 2: will automatically request stop before joining. It makes graceful shut down.
Speaker 1: Much easier and std dot stop call back that lets.
Speaker 2: You register a function that gets called immediately when stop
Speaker 2: is requested on a given token, useful for things like
Speaker 2: quickly closing a socket or canceling an io operation.
Speaker 1: Okay, a much cleaner stop mechanism. What about STD dot
Speaker 1: counting semaphore also C plus plus twenty. How's that different
Speaker 1: from a mutex?
Speaker 2: A mutex is about exclusive access only one thread in
Speaker 2: at a time. A semaphore maintains a counter representing available
Speaker 2: resources or permits.
Speaker 1: How does that work?
Speaker 2: A thread calls a choir to take a permit, decrementing
Speaker 2: the counter. If the counter is zero, the thread blocks.
Speaker 2: A thread calls release to return a permit, incrementing the counter,
Speaker 2: potentially waking up a blocked thread.
Speaker 1: Can different threads acquire and release?
Speaker 2: Yes, that's a key difference from utexas, which are usually
Speaker 2: locked and unlocked by the same thread Somemophores are great
Speaker 2: for controlling access to a pool of n resources or
Speaker 2: for producer consumer scenarios where one thread signals another about
Speaker 2: available work. They're thread agnostic.
Speaker 1: Interesting. Lastly, for basic sinc C plus plus twenty also
Speaker 1: give us STD dot barrier and std dot latch. Yeah,
Speaker 1: coordinating multiple threads exactly.
Speaker 2: Both are for synchronizing a group of threads at a
Speaker 2: specific point.
Speaker 1: What's the difference latch versus barrier.
Speaker 2: A SSTD dot latch is basically a one shot countdown.
Speaker 2: You initialize it with a count threads call countdown. When
Speaker 2: the count reaches zero, any threads waiting on the latch
Speaker 2: using weight are unblocked. After that, the latch is done.
Speaker 2: It can't be reset.
Speaker 1: One time use and a barrier.
Speaker 2: A std dot barrier is reusable. You initialize it with
Speaker 2: the number of threads in the group. Each thread calls
Speaker 2: arrive and weight. When all threads have arrived, they are
Speaker 2: all unblocked simultaneously. Crucially, the barrier resets ready for the
Speaker 2: next synchronization phase. You can even run a completion function
Speaker 2: when all threads arrive but before they're unblocked.
Speaker 1: So latch for a single sync point barrier for repeated
Speaker 1: phases of computation.
Speaker 2: That's a good way to think about it. The book
Speaker 2: shows an example of barriers being used across different stages
Speaker 2: where the number of workers might even change.
Speaker 1: Okay, let's move up a level to tasks and futures.
Speaker 1: Std dot ASNC sounds really convenient for running stuff in
Speaker 1: the background.
Speaker 2: It is. It's a high level way to say, run
Speaker 2: this function, possibly on another thread, and give me back
Speaker 2: something I can use to get the result later.
Speaker 1: That's something is the future exactly.
Speaker 2: Std dot ACNC returns std dot future object, and it
Speaker 2: handles the thread management, often using an internal.
Speaker 1: Threadpool you mentioned, possibly on another thread right.
Speaker 2: Std dot ACNC takes an optional launch policy. The default
Speaker 2: std dot launch dot acing std dot launch dot deferred
Speaker 2: usually runs it on a new thread eager evaluation. But
Speaker 2: you can specify sdd dot launch dot acing to guarantee
Speaker 2: a new thread, or std dot launch dot deferred to
Speaker 2: make it.
Speaker 1: Lazy lazy evaluation meaning.
Speaker 2: Meaning the function only runs when you actually call get
Speaker 2: or wait on the future it runs synchronously in the
Speaker 2: thread that calls get.
Speaker 1: Interesting trade off. What about std dot package task How
Speaker 1: does that fit?
Speaker 2: In std dot package task gives you more control. It
Speaker 2: bundles up a function or callable with a promise the
Speaker 2: thing that will eventually hold the result. Okay, it gives
Speaker 2: you back at sdd dot future associated with that promise.
Speaker 2: But crucially, the task doesn't run yet. You are responsible
Speaker 2: for invoking the package task object itself, maybe passing it
Speaker 2: to a thread you manage, or putting it in your
Speaker 2: own queue for a threadpool.
Speaker 1: So ACNC is fire and forget with a future back
Speaker 1: package task is prepare and run later.
Speaker 2: That's a good way to put it package. Task decouples
Speaker 2: defining the work from executing it.
Speaker 1: In std dot future itself. Yeah, just a placeholder for
Speaker 1: the result pretty much.
Speaker 2: It represents a result that will eventually be available from
Speaker 2: some asynchronous operation. You call get on it to retrieve
Speaker 2: the value those get block Yes. If the result isn't
Speaker 2: ready yet, debt blocks the calling thread until it is. Also, importantly,
Speaker 2: you can typically only call get once on a regular
Speaker 2: sdd dot future.
Speaker 1: The result is moved out only once. What if multiple
Speaker 1: threads need the result?
Speaker 2: Ah, that's where std dot shared future comes in. You
Speaker 2: can create a shared future from a sdd dot future
Speaker 2: which consumes the original future. Copies of the shared future
Speaker 2: can then be given to multiple threads, and they can
Speaker 2: all call get to retrieve a copy of the result
Speaker 2: once it's ready.
Speaker 1: Okay, so future for single result retrieval, shared future for
Speaker 1: multiple correct. How do futures compared to condition variables for synchronization,
Speaker 1: The book mentioned a comparison.
Speaker 2: They serve different purposes. Mostly, conditioned variables are more general
Speaker 2: purpose for complex waiting logic, maybe involving multiple conditions or
Speaker 2: repeated signaling. Futures are primarily designed for getting a single
Speaker 2: result back from a one off task.
Speaker 1: So futures are simpler for the GATA result case.
Speaker 2: Often yes, they bundle the data transmission, the result or
Speaker 2: exception with the synchronization. With conditioned variables, you manage the
Speaker 2: shared data and locking separately, which can be more error
Speaker 2: prone if not done carefully. Tasks futures are often less
Speaker 2: susceptible to issues like lost way cups.
Speaker 1: Right DA, that makes sense. Let's switch gears to the
Speaker 1: parallel algorithms in the SEL. Since C plus plus seventeen
Speaker 1: many of them can run in parallel.
Speaker 2: Yes, a huge addition. Many standard algorithms like four each, transform,
Speaker 2: reduced sort, et cetera, now have overloads that take in
Speaker 2: execution policy as the first argument.
Speaker 1: Execution policy, what are the options?
Speaker 2: The main ones are std dot execution dot SICK for
Speaker 2: sequential execution, the old default std dot execution dot PR
Speaker 2: for parallel execution on multiple threads, and std dot execution
Speaker 2: dot PARENTSEC for parallel and potentially vectorized execution.
Speaker 1: Vectorized like SIMD.
Speaker 2: Exactly, parentsec gives the implementation the most freedom. It can
Speaker 2: run jumps in parallel, and within each thread, it can
Speaker 2: reorder or interleave operations on different elements. Often to take
Speaker 2: advantage of SIMD instructions if the hardware supports.
Speaker 1: It, so potentially the fastest, but maybe harder for the
Speaker 1: programmer to reason about if side effects are involved.
Speaker 2: Precisely, PARENTSEC demands more care regarding thread safety and lack
Speaker 2: of dependencies between element operations. The book shows four each
Speaker 2: with parentsec using an atomic counter, which is safe.
Speaker 1: What about algorithms like std dot reduce or std dot
Speaker 1: transform reduce, any special rules for parallelizing.
Speaker 2: Them, Yes, a very important one. The operation you provide
Speaker 2: addition for reduce or multiplication and addition for transform reduce
Speaker 2: must be associative and commutative for the parallel versions to
Speaker 2: guarantee the same result as the sequential one.
Speaker 1: Associative and commutative like addition A plus B plus C
Speaker 1: plus B plus c and A plus b egals b
Speaker 1: plus a exactly.
Speaker 2: If your operation doesn't have those properties, the result might
Speaker 2: differ depending on how the parallel execution chunks and combines
Speaker 2: the data. Floating point Edition strictly speaking, isn't associative, which
Speaker 2: can sometimes cause tiny differences.
Speaker 1: Good point. Are these policies just hints or guarantees of parallelism, They're.
Speaker 2: More like permission slips or strong hints SDD dot execution.
Speaker 2: DOT par allows parallel execution, but the library implementation might
Speaker 2: decide to run it sequentially if it thinks that's faster. Eventually,
Speaker 2: for very small ranges, it's not a strict guarantee of
Speaker 2: end threads being used.
Speaker 1: Does the book give any performance numbers? Is this speed
Speaker 1: up real?
Speaker 2: Yes, it shows a test case calculating tangents. The PAR
Speaker 2: version on their quad core machine was significantly faster than SAKE,
Speaker 2: close to a four x speed up. The parentsec version
Speaker 2: was similar to PAR in that specific test, but your
Speaker 2: mileage may vary absolutely. Performance depends heavily on the hardware,
Speaker 2: the compiler, the specific algorithm, the data size, and the
Speaker 2: operation being performed. Always benchmark your own code.
Speaker 1: Sound advice. Okay, let's tackle a really modern feature. C
Speaker 1: plus plus twenty quarantines. What's the fundamental difference from a
Speaker 1: regular function.
Speaker 2: The key idea is that they are resumable functions stackless, specifically.
Speaker 1: Resumable, meaning they can pause and.
Speaker 2: Continue later exactly. A regular function runs from start to
Speaker 2: finish in one go. A core utine can execute a bit,
Speaker 2: then cowight some operation or coiled of value, which suspends
Speaker 2: its execution. Later, something can resume the core routine and
Speaker 2: it picks up right where it left off, with all
Speaker 2: its local variables intact.
Speaker 1: So the state is saved somewhere, not just on the stack.
Speaker 2: Right the state local variables suspension point is typically allocated
Speaker 2: on the heap or in a quarutine frame managed by
Speaker 2: the compiler, not just the traditional call stack. That's the
Speaker 2: stackless part.
Speaker 1: What makes a function become a core routine? Is there
Speaker 1: a special keyword?
Speaker 2: It becomes a quarantine if its body uses any of
Speaker 2: the three Cortine keywords core return to return a value
Speaker 2: and finish, suspend, co weight to suspend and wait for something,
Speaker 2: or coiled to produce a value in a generator like sequence.
Speaker 2: Even a range based for loop using co weight makes
Speaker 2: it a core routine.
Speaker 1: Okay, co return, cowight, coy yield. The book mentions handles,
Speaker 1: suspend points, awaitables. Sounds like the machinery behind it it is.
Speaker 2: Let's break it down quickly, go for it.
Speaker 1: Quarantine handle that's.
Speaker 2: Your remote control for the qure routine. An object you
Speaker 2: can use to resume it, destroy its state, or check
Speaker 2: if it's done.
Speaker 1: Initial and final suspend points.
Speaker 2: Every quarantine has a promise object associated with it. This
Speaker 2: promise defines initial suspend and final suspend. These return special
Speaker 2: awaitable objects like std dot suspenda ways or std dot
Speaker 2: suspend never that control whether the quarantine suspends immediately when called,
Speaker 2: and whether it suspends when it finishes via core return
Speaker 2: or falling off the end.
Speaker 1: So you can have a couroretine start suspended or run until.
Speaker 2: The first CO eight exactly, and you can control if
Speaker 2: it cleans itself up automatically when done, or waits to
Speaker 2: be destroyed.
Speaker 1: Via its handle and awaitables of waiters. That's for coit right.
Speaker 2: When you cowight something that's something has to be an awaitable.
Speaker 2: The compiler calls three key methods on the corresponding a
Speaker 2: weight object, often the awaitable itself. A weight ready checks
Speaker 2: if suspension is even needed. If not, it continues. If
Speaker 2: suspension is needed, a weight suspend is called, which suspends
Speaker 2: the quarantine and can schedule it for resumption later. When
Speaker 2: it's time to resume, a weight resume is called and
Speaker 2: its return value becomes the result of the cowight expression.
Speaker 1: Okay, that's the core mechanism. The book had examples. One
Speaker 1: preparing a job another using an event core routine.
Speaker 2: Yeah, the job example was basic, showing the structure even
Speaker 2: if it didn't suspend much initially. The event example was
Speaker 2: more interesting for synchronization.
Speaker 1: How did the event work.
Speaker 2: It was a quarantine helper. You could cowight an event
Speaker 2: object that courantine would suspend elsewhere code could call notify
Speaker 2: on the event, which would resume the weighting core routine.
Speaker 2: It's a way to build synchronization primitives using the quarantine
Speaker 2: machinery itself.
Speaker 1: Like a condition variable, but maybe fitting more naturally into
Speaker 1: acen code flow.
Speaker 2: Kind of. Yeah, it shows how coroutines can help manage
Speaker 2: asynchronous waiting.
Speaker 1: Let's look at the case studies. The book compared something
Speaker 1: numbers in different ways. What was the fastest concurrent approach?
Speaker 2: Right? They compared symbol, threaded, mutex protected, shared, some atomic shared,
Speaker 2: some with different orderings, and finally a local sum approach.
Speaker 2: Then the winner was by far the best concurrent performance
Speaker 2: came from having each thread calculate a sum for its
Speaker 2: own portion of the data into a local non shared variable.
Speaker 2: Then only at the very end, each thread atomically adds
Speaker 2: its local sum to the final shared result.
Speaker 1: Variable, so minimize the shared operations do most work locally exactly.
Speaker 2: Contention on the mutex or even the atomic variable in
Speaker 2: the other approaches really killed performance locking or atomic ops
Speaker 2: on every single edition was very slow compared to the
Speaker 2: local accumulation.
Speaker 1: Makes sense. The dining Philosopher's problem also came up. What
Speaker 1: classic concurrency issues does that highlight?
Speaker 2: Oh Dining Philosophers is the poster child for deadlock. It
Speaker 2: perfectly illustrates how multiple actors philosophers competing for multiple shared
Speaker 2: resources forks can easily get into a state where none
Speaker 2: can proceed because they're all waiting for a resource held by.
Speaker 1: Another the circular weight exactly.
Speaker 2: The book uses it to show how flawed synchronization attempts
Speaker 2: can lead to deadlock or maybe livelock, where they're busy
Speaker 2: trying but making no progress, and it shows solutions like
Speaker 2: establishing a strict ordering for acquiring the resources always pick
Speaker 2: up the lower numbered fork first, for instance, to break
Speaker 2: that circular dependency.
Speaker 1: So resource ordering is a key deadlock prevention technique.
Speaker 2: One of the most common and effective.
Speaker 1: Ones singleton initialization again block based double checked locking, Meyer singleton,
Speaker 1: what's the verdict?
Speaker 2: Lock based is simple but potentially slow under contention double
Speaker 2: check locking tries to optimize by checking first before locking,
Speaker 2: but it's notoriously hard to get right and C plus
Speaker 2: plus without hitting subtle memory ordering bugs. Avoid it unless
Speaker 2: you really know what you're doing.
Speaker 1: So Meyer's singleton datic local variable.
Speaker 2: That's generally the way to go in modern C plus
Speaker 2: plastatic tea instance return instance, since C plus plus eleven
Speaker 2: the language guarantees this is thread safe and efficient, simple correct,
Speaker 2: usually fast enough.
Speaker 1: Good takeaway, Yeah, prefer meyer singleton CPMM was used again
Speaker 1: to look at memory ordering and data races.
Speaker 2: Yes, analyzing small examples, it visually reinforces how without proper
Speaker 2: synchronization like mutexes or acquire release semantics, rights in one
Speaker 2: thread are simply not guaranteed to be visible to reads
Speaker 2: in another thread if they access the same non atomic
Speaker 2: memory location. It makes the abstract memory ordering rules much
Speaker 2: more concrete.
Speaker 1: Seeing is believing, basically pretty much.
Speaker 2: It helps you spot potential data races you might otherwise miss.
Speaker 1: There was also a comparison condition variables versus atomic flags
Speaker 1: for synchronization between two threads.
Speaker 2: What was faster in the specific tests shown, which involved
Speaker 2: repeated ping pong synchronization using atomic flags like std dot
Speaker 2: atomic flag or std dot com atomic bool for the
Speaker 2: signaling was found to be faster than using conditioned variables
Speaker 2: in mutexes.
Speaker 1: Why would that be It's likely due to overhead.
Speaker 2: Atomic operations, especially on flags, can often be implemented very
Speaker 2: efficiently by the processor, sometimes without involving the operating system kernel.
Speaker 2: Conditioned variables in mutexes usually involves system calls for blocking
Speaker 2: and waking threads, which adds more overhead.
Speaker 1: So for simple signaling, atomics might be quicker, but condition
Speaker 1: variables are more general.
Speaker 2: That's a reasonable summary.
Speaker 1: Yeah, and the last case study bit a coroutine returning
Speaker 1: a future.
Speaker 2: Yes, that just showed the nice integration. You can write
Speaker 2: a function as a coroutine using coweight for internal ACYNC operations,
Speaker 2: and then use coreturn to provide the final value. The
Speaker 2: compiler automatically hooks this up, so the corotine returns an
Speaker 2: std dot future or similar awaitable type that completes with
Speaker 2: the core return.
Speaker 1: To value, seamlessly bridging the two models exactly.
Speaker 2: Let's use the corotine syntax internally while still interacting with
Speaker 2: other code expecting futures.
Speaker 1: Okay, let's gaze into the crystal ball see plus plus
Speaker 1: twenty three and beyond. Executors are presented as a big deal.
Speaker 1: What's the core concept?
Speaker 2: Executors are intended to be a fundamental abstraction for how, where,
Speaker 2: and when work gets done. They define the execution context.
Speaker 1: Execution context like which threadpool or run on the GPU or.
Speaker 2: Inline potentially all of the above. The idea is to
Speaker 2: separate what the function or task you want to run
Speaker 2: from the how or when, which is defined by the executor.
Speaker 2: You'd submit your task to an executor, and the executor
Speaker 2: decides how to run it.
Speaker 1: So it's a unified way to handle thread pools, inline execution,
Speaker 1: maybe event loops.
Speaker 2: That's the goal, a standard, composable way to represent and
Speaker 2: manage different execution strategies. The book sees them as foundational
Speaker 2: for future concurrency libraries, networking, etc.
Speaker 1: What kind of properties can these executors have?
Speaker 2: The proposals discuss properties like directionality, is it fire and
Speaker 2: forget one way? Does it return a future two way?
Speaker 2: Does it support continuations then then cardinality? Does it run
Speaker 2: one task, single or many? Bulk blocking behavior? Does submitting
Speaker 2: work potentially block the caller? Possibly always never?
Speaker 1: And you could potentially query or acquire executors with certain properties.
Speaker 2: That's the idea, using mechanisms like execution dot require or
Speaker 2: prefer to tailor the execution context to your needs.
Speaker 1: How might this integrate with things like std dot ASNC
Speaker 1: or parallel algorithms.
Speaker 2: The vision is you'd be able to pass an executor
Speaker 2: object to std dot ASNC or to the parallel algorithms
Speaker 2: to control where and how they run, instead of them
Speaker 2: always using some default mechanism like a hidden global threadpool.
Speaker 2: More control, more flexibility.
Speaker 1: What are the main design goals for executors?
Speaker 2: Usability both for library writers building on them and for
Speaker 2: application developers using them, composabilities so you can layer and
Speaker 2: combine executors, and minimality, keeping the core concepts lean and extensible.
Speaker 1: You mentioned single versus bulk cardinality. What's the difference in
Speaker 1: how they execute?
Speaker 2: Single cardinality functions one way execute, two U way execute
Speaker 2: then execute, take one callable and run at once. Bulk
Speaker 2: functions take a callable and a shape like account and
Speaker 2: run the callable multiple times, possibly in parallel, passing an
Speaker 2: index or other info to each invocation. Useful for parallel
Speaker 2: for style operations.
Speaker 1: Got it? The book mentioned some ongoing concerns like when
Speaker 1: all wenny return types and blocking future destructors.
Speaker 2: Yeah, those are known complexities. Combining futures with when all
Speaker 2: weny can lead to complicated return types, and the fact
Speaker 2: that std dot ASNC can return a future that blocks
Speaker 2: in its destructor if you don't get the result is
Speaker 2: problematic as it can accidentally serialize your code. There are
Speaker 2: active proposals trying to refine these areas.
Speaker 1: What about synchronized in atomic blocks in C plus plus
Speaker 1: twenty three sound related but different.
Speaker 2: They are both aim for atomic execution of a code block.
Speaker 2: Synchronized blocks are more relaxed. They act like the block
Speaker 2: is guarded by a single global mutex, providing a total order.
Speaker 2: They can contain things like io atomic blocks at no
Speaker 2: accept atomic commit, atomic cancel are for true transactions. They
Speaker 2: have stricter rules about what they can contain no non transactions,
Speaker 2: safe operations, and explicit handling of exceptions commit or cancel
Speaker 2: the transaction.
Speaker 1: So atomic blocks are closer to database transactions.
Speaker 2: Conceptually, yes, aiming for that kind of atomicity, though without
Speaker 2: the durability aspect.
Speaker 1: Usually and taskblocks a fork joint model.
Speaker 2: Right. Taskblocks provide structured parallelism. You define a work launch
Speaker 2: subtasks within it. The fork the thread that started the block,
Speaker 2: automatically waits at the end of the block until all
Speaker 2: launch subtasks are complete. The join makes managing parallel task
Speaker 2: dependencies much simpler.
Speaker 1: Okay, And lastly, for C plus plus twenty three, the
Speaker 1: Data Parallel Vector Library SAMD.
Speaker 2: This aims to standardize SIMD programming in C plus plus,
Speaker 2: providing standard vector types that map to hardware SIMD registers
Speaker 2: and operations on them. Features like masked operations apply an
Speaker 2: operation only where a condition is true, and traits to
Speaker 2: query vector properties like size are part of it, making
Speaker 2: SIDY more portable and accessible.
Speaker 1: Lots of interesting stuff potentially coming. Let's switch to synchronization patterns,
Speaker 1: architectural design idioms. What's the difference?
Speaker 2: Think levels of abstraction. Architectural patterns like reactor proactor define
Speaker 2: the high level structure of a concurrent system. Design patterns
Speaker 2: like active object monitor describe common interaction solutions between components.
Speaker 2: Idioms are lower level se plus specific techniques like scope locking.
Speaker 1: And using patterns helps out.
Speaker 2: Gives you a shared vocabulary, makes designs clearer, lets you
Speaker 2: reuse proven solutions instead of reinventing the wheel. They build
Speaker 2: on best practices but are more specific named solutions to
Speaker 2: recurring problems.
Speaker 1: What patterns help with managing shared data?
Speaker 2: The book mentions things like copying the value avoids sharing
Speaker 2: mutable state altogether good for value types, thread specific storage.
Speaker 2: Each thread gets its own copy using futures, share the
Speaker 2: result once it's ready.
Speaker 1: And patterns for handling mutations safely one include scoped locking
Speaker 1: using std dot lockered or std.
Speaker 2: Dot unique lock for RAI mutex management, strategize locking using
Speaker 2: templates or polymorphism to vary the locking strategy, thread safe
Speaker 2: interface designing the class itself to handle internal synchronization, guarded
Speaker 2: suspension using condition variables to wait for preconditions, and always.
Speaker 2: The book warns about lifetimes when passing references to threads.
Speaker 1: Right the architectural patterns active object, monitor, half sync, have
Speaker 1: async reactor proactor.
Speaker 3: Can we get a super quick idea of each okay
Speaker 3: quick fire active object decouple's method call from execution uses
Speaker 3: an internal thread and message que monitor object synchronizes access
Speaker 3: to an object's methods, usually one lock for the whole object.
Speaker 2: Half sync half ACYNC separates asinc tasks verr gio in
Speaker 2: a thread pool from sync processing. You examle main logic thread,
Speaker 2: often using a queue reactor, single thread, weights for events,
Speaker 2: synchronously dispatches to handlers. Proactor waits for completion of ace
Speaker 2: zinc operations, then calls handlers leverages acinc os features.
Speaker 1: Got it. The book uses boost assio for a reactor example.
Speaker 2: Yes, showing how that library implements the event loop and
Speaker 2: handler dispatch typical of the reactor pattern.
Speaker 1: Moving on to best practices, what are the absolute top.
Speaker 2: Ones number one, far and away. Minimize shared mutable state
Speaker 2: If data isn't shared or isn't mutable, concurrency gets vastly simpler.
Speaker 1: Avoid the problem if you can.
Speaker 2: Exactly if you must have shared mutable state. Ensure proper
Speaker 2: synchronization mutext as atomics, et cetera. Minimize waiting time. Amdell's
Speaker 2: law limits speed up based on sequential parts. Use a
Speaker 2: mutability const expert where possible. Use RAII for locks lockguard.
Speaker 2: Don't use condition variables without predicates. Prefer higher level tools
Speaker 2: a std dot acinc. Parallel algorithms over raw threads when appropriate,
Speaker 2: and understand the memory model.
Speaker 1: Understand the memory model. It seems full circle on that one. Okay,
Speaker 1: Concurrent data structures, stacks, ques, What are the challenges?
Speaker 2: The main chate is maintaining the data structure's internal consistency.
Speaker 2: It's invariance when multiple threads are operating on it simultaneously.
Speaker 2: A simple example is a stack. What if one thread
Speaker 2: tries to pop while another is reading the top. You
Speaker 2: might get inconsistent results or errors.
Speaker 1: How do you fix that? Locks?
Speaker 2: Locks are the first step. Coarse grained locking one big
Speaker 2: lock for the whole structure is simpler, but limits concurrency.
Speaker 2: Fine grained locking multiple locks for different parts allows more parallelism,
Speaker 2: but is much harder to get right.
Speaker 1: The book mentioned changing the interface sometimes.
Speaker 2: Yes like instead of separate top on pop on a stack,
Speaker 2: provide a single atomic top app operation. This avoids the
Speaker 2: race condition between checking the top and removing it.
Speaker 1: What about lock free structures?
Speaker 2: That's the next level, avoiding locks entirely using a comic
Speaker 2: operations like compare and swap. This can offer better performance
Speaker 2: and avoids deadlock, but it's extremely complex. You run into
Speaker 2: issues like the ABA problem, and memory reclamation becomes very tricky.
Speaker 2: The book mentions hazard pointers as one solution a problem
Speaker 2: where a location reads value a then computation happens then
Speaker 2: it reads A again, but in the meantime another thread
Speaker 2: changed it to B and then back to A. Your
Speaker 2: comparent swap might succeed, thinking nothing changed, but the underlying
Speaker 2: state is different. Needs careful handling.
Speaker 1: Wow, concurrent data structures sound like a deep topic on their.
Speaker 2: Own, they really are. The book gives a taste, including
Speaker 2: a lock free stack using C plus plus twenty atomic
Speaker 2: smart pointers.
Speaker 1: What about the time library chrono? How did that relate?
Speaker 2: Krono is essential for measuring performance, setting timeouts, managing timed weights.
Speaker 2: It provides clocks system clock for wall time, steady clock
Speaker 2: for intervals, time points, specific moments and durations, intervals, very
Speaker 2: flexible for representing time.
Speaker 1: Accurately, and atomic operations, transactional memory any final points there.
Speaker 2: The book mentions atomics should ideally be addressed free atomic
Speaker 2: even across processes sharing memory, and it touches on ACD
Speaker 2: properties atomicity, consistency, isolation, durability for transactions, noting that C
Speaker 2: plus plus transactional memory proposals focus mainly on AC and I,
Speaker 2: with durability being less of a focus than in databases.
Speaker 1: And finally, a glossary that sounds useful.
Speaker 2: Very concurrency has a lot of specific terminology, acquire, release, data, RaSE, deadlock,
Speaker 2: sequential consistency, et cetera. The glossary helps nail down those definitions.
Speaker 1: Wow, Okay, we have definitely covered a lot of ground
Speaker 1: there based on the material. Real deep dive into modern
Speaker 1: C plus plus.
Speaker 2: Concurrency absolutely from the memory models, tricky foundations, rite up
Speaker 2: to currotines, parallel algorithms, and a peak at what's coming
Speaker 2: with executors. C plus plus provides a pretty powerful set
Speaker 2: of tools.
Speaker 1: So for you lifting in, I think the big takeaway
Speaker 1: is that concurrency really forces a shift in thinking compared
Speaker 1: to sequential code, timing, interaction, ordering. It all becomes critical, right.
Speaker 2: It unlocks performance potential, but also opens up whole new
Speaker 2: categories of bugs like data races and deadlocks if you're
Speaker 2: not careful.
Speaker 1: Vigilance is needed, and this is an evolving field, right.
Speaker 2: Definitely, the C plus plus standard keeps adding features, best
Speaker 2: practices emerge, So we'd encourage you to maybe pick an
Speaker 2: area that caught your interest today and dig deeper. Try
Speaker 2: out the parallel algorithms, maybe write a small program using
Speaker 2: j thread. When C plus plus twenty three features become available,
Speaker 2: experiment with executors.
Speaker 1: Or Even if you're feeling brave, try implementing a simple
Speaker 1: lock free structure just to appreciate the complexity exactly.
Speaker 2: The more you work with it, the better your intuition becomes.
Speaker 1: Ultimately, understanding of these concepts put you in a much
Speaker 1: stronger position to build modern software. Being able to reason
Speaker 1: about concurrency is just It's becoming a non negotiable skill
Speaker 1: in our multi core world.
Speaker 2: Couldn't agree more.
Speaker 1: Thanks for joining us on this deep dive.