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