
Chapter 4 - Part 3. Syncronizing concurrent operations

If not specified, all the code is written in c23 standard, compiled with g 13.2
command: g\++ -std=c\++2b -O0 ch4.cpp -o out && ./out
Chapter 4. Synchronizing concurrent operations - Part 2
Futuristic(?) programming
One major source of race condition is data race, which multiple threads modifies one function. To avoid this, we can use mutex, but it will reduce efficiency since it makes the operation not concurrently occur. However, with future, we can implement a more efficient and safe way to do it.
Functional programming(FP)
One critical trait of functional programming is that with FP, functions doesn’t depend on external states, and it will always return the same result when given the same input. Another trait of a pure function is that it doesn’t and shouldn’t modify external values and the influence of the function is limited to the return value.
C++ also have some support for FP, like lambda expression, std::function
, std::bind
, and auto type deduction for function return type. The first two made function can be passed around as variables and parameters, the third allowed us to create function objects by binding parameters, and the last one made function definition more like pure functional language(like Haskell or OCaml). And std::future
made it possible to do FP-style concurrency in C++ because it allowed us to pass around results of functions among functions with no explicit access to shared data.
FP-style concurrency example: quick sort
Idea behind quick sort is simple: pick a pivot, move all elements smaller than the pivot to the left and all elements larger than the pivot to the right. Then recursively perform this operation on the left and right part. The following diagram illustrated this process:
icon-padding
Legend:
- green: not sorted
- purple: pivot
- gray: sorted
- white: sorted after operation
We first implement a normal quick sort algorithm:
1 | template<typename T> |
This is a normal quick sort algorithm, but we used some functional programming concept like no modification to external data and use of lambda expression. However, functional programming brought a lot of copying, so apart from the functional programming part, we used a lot of standard library functions to reduce copying and lines of code.
icon-padding
std::partition
is a function that takes a range and a predicate, and it will move all elements that satisfy the predicate to the front of the range and return an iterator to the first element that doesn’t satisfy the predicate.
std::list::splice
is a function that takes a position and a range, and it will move all elements in the range to the position.
Now, let’s convert that into an FP-style quick sort. Since it’s already a pure function, we can do it with a slight twist of code:
1 | template<typename T> |
Here we made the sort of the lower part async, which will save a lot of time… or not? Let’s see the result:
benchmark code
1 | int main() { |
This output is very long.
Output
1 | 0 elements: |
We can see that the parallel version is much slower than the sequential version. This is because when we are using std::async
, we are creating a thread each time we call qsort_parallel
, which is very expensive. Also, each time std::async
creates a thread, that thread won’t use the full CPU core resource, leads to suboptimal use of resources. Even we assume std::async
allocates system resources efficiently, std::partition
is still not running in parallel( according to cppreference), and we called that very often.
Communicating Sequential Processes(CSP)
Another paradigm of concurrent programming is Communicating Sequential Processes, where threads are by definition separate, independent, and can only contact each other through message passing. This is used by Erlang and by the Message Passing Interface for high-perfomance computing in C++.
The idea of CSP is that threads are purely on the basis of how it behaves in response to the message it receives. In this case, each thread is a state machine, with the state being modified by messages. Thus, one way to implement CSP is to write a finite state machine, but that’s not the only way. The state machine structure can be implicit in the structure of the application and that’s fine.
warning
Pure CSP cannot have shared data, which is impossible in C++ because every thread shares a part of memory. Thus, it’s library author’s job to ensure there’s no data shared between threads.
CSP-style concurrency example: ATM sceneario
A typical ATM state machine (withdraw only) can be illustrated as the following diagram:
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#add8e6', 'noteTextColor': '#0f0f0f', 'noteBkgColor': '#f5f6fa', 'lineColor': '#eaa1af', 'labelBoxBkgColor': '#add8e6' } } }%% flowchart TD init -->|card inserted| pin[getting pin] pin -->|last pin pressed|pin_check[check pin] pin -->|digit pressed|pin pin -->|delete last digit|pin pin_check -->|pin not ok|done pin_check -->|pin ok|action[wait for withdraw amount] action -->|cancel|done action -->wait[getting withdraw amount] wait -->|withdraw amount entered|amount[check balance] amount -->|insufficient fund|done amount -->|give cash|done
In this diagram, each blue box represents a state, each arrow represents sending a message, and each lime box represent the message sent.
In actual implementation, we will use a message queue to send messages between threads. Each message will be a struct that implements a common interface. For the actual processor, we will have a class represents the state machine, with each state as a member function. Then we can simply use function pointer as states and use a loop to run the state machine.
The implementation of message queue is complicated and will be explained in another article.
icon-padding
Originally the book talked about concurrency TS after this part, but according to this paper from open-std, they are still nor sure whether to include it or not and they are still experimental (under std::experimental
namespace). Thus, I will skip that part.
Summary
- Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
- Since in functional programming, functions don’t modify external data, and by definition variables are immutable, it’s easier to write concurrent code.
- Concurrency doesn’t necessarily mean faster execution, since system coordination, allocation, and context switching between threads is expensive.
- CSP is a programming paradigm that treats threads as state machines and communicate with each other through message passing.
- C++ is by definition not CSP, but we can still use this paradigm by using message queue.