You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-[Worker threads and thread pools](#worker-threads-and-thread-pools)
17
+
-[Step 1: How to create a thread](#step-1-how-to-create-a-thread)
18
+
-[Stopping threads cooperatively with `std::stop_token`](#stopping-threads-cooperatively-with-stdstop_token)
19
+
-[Step 2: Adding another thread and a Mutex](#step-2-adding-another-thread-and-a-mutex)
20
+
-[Step 3: Sleeping with Condition Variables](#step-3-sleeping-with-condition-variables)
21
+
-[Step 4: Putting it all together into a Generic Thread Pool](#step-4-putting-it-all-together-into-a-generic-thread-pool)
22
+
-[What if I don't have C++20?](#what-if-i-dont-have-c20)
17
23
-[Deadlocks](#deadlocks)
18
24
-[Summary](#summary)
19
25
@@ -1173,15 +1179,96 @@ int main() {
1173
1179
1174
1180
So you see, there are not that many changes, but if we have the luxury of being able to use C++20s `std::jthread` we definitely should as it avoid quite some boilerplate code and potential bugs.
1175
1181
1182
+
### Deadlocks
1183
+
Before we wrap up, there is one more major pitfall we must mention when working with multiple threads and mutexes: **deadlocks**.
1184
+
1185
+
A deadlock is a kind of counterpart of data race. When we fix a data race we might end up with a deadlock instead. A deadlock occurs when two or more threads are stuck waiting for each other to release a lock, resulting in all of them waiting forever. For example, imagine Thread A locks Mutex 1 and then tries to lock Mutex 2. Meanwhile, Thread B locks Mutex 2 and tries to lock Mutex 1. Neither thread can proceed because the other is holding the mutex it needs.
1186
+
1187
+
<!--
1188
+
`CPP_COPY_SNIPPET` parallelism_deadlock/main.cpp
1189
+
-->
1190
+
```cpp
1191
+
#include<chrono>
1192
+
#include<iostream>
1193
+
#include<mutex>
1194
+
#include<thread>
1195
+
1196
+
namespace {
1197
+
// Global variables for simplicity of an example. Don't do it in real code.
1198
+
std::mutex mutex1;
1199
+
std::mutex mutex2;
1200
+
1201
+
void ThreadA() {
1202
+
std::lock_guard lock1{mutex1};
1203
+
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Give B time to lock mutex2
1204
+
std::lock_guard lock2{mutex2};
1205
+
std::cout << "Thread A got both locks!\n";
1206
+
}
1207
+
1208
+
void ThreadB() {
1209
+
std::lock_guard lock2{mutex2};
1210
+
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Give A time to lock mutex1
1211
+
std::lock_guard lock1{mutex1};
1212
+
std::cout << "Thread B got both locks!\n";
1213
+
}
1214
+
} // namespace
1215
+
1216
+
int main() {
1217
+
std::jthread a{ThreadA};
1218
+
std::jthread b{ThreadB};
1219
+
// This will hang forever!
1220
+
return 0;
1221
+
}
1222
+
```
1223
+
1224
+
To avoid deadlocks, a common rule of thumb is to always acquire multiple locks in the exact same order across all threads. Alternatively, from C++17 onwards, we can use `std::scoped_lock` which safely locks multiple mutexes at once without the risk of a deadlock using a deadlock-avoidance algorithm under the hood:
And with this, I believe that this is everything one needs to know to understand the basics of multithreading in C++! At least these examples are a simplified version of what I've seen in many production codebases. Have I missed some pattern that you've seen?
1262
+
And with this, I believe we covered everything one needs to know to understand the basics of multithreading in C++! At least these examples are a simplified version of what I've seen in many production codebases over the last 15 or so years. Have I missed some pattern that you've seen?
1178
1263
1179
1264
Anyway, as a short summary, I hope I could convince you that writing parallel code in C++ is not all that complex. Here are the key takeaways again:
1180
1265
1181
1266
- When faced with large tasks that have to run in the background, `std::async` seems to be the right tool.
1182
1267
- When needing to parallelize many small-ish operations over a large corpus of data, available ahead of time, the parallel algorithms should do the trick. Or the oneTBB library if more control is needed.
1183
1268
- Finally, when more flexibility is needed and when the data is loaded dynamically, a thread pool is something that people typically reach for.
1184
-
- And don't forget to protect any shared mutable state with a mutex! Well, technically, there is the whole so-called "lock free" programming paradigm that avoids mutexes, but it is its own completely different can of worms which we won't talk about in this course.
1269
+
- And don't forget to protect any shared mutable state with a mutex! And while at it avoid deadlocks by always acquiring multiple locks in the same order or by using `std::scoped_lock`.
1270
+
1271
+
Well, technically, there is the whole so-called "lock free" programming paradigm that avoids mutexes, but it is its own completely different can of worms which we won't talk about in this course.
1185
1272
1186
1273
And remember, as the very first thing, try to avoid parallel code altogether. In 90% of the cases, a sequential implementation is fast enough and avoids all the pitfalls of parallel programming!
0 commit comments