Skip to content

Commit a279353

Browse files
committed
Add deadlocks
1 parent 119f86a commit a279353

2 files changed

Lines changed: 111 additions & 8 deletions

File tree

animation/projects/src/parallelism/scenes/code.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import jthread2ClassSwapCode from '@lectures/parallelism.md?snippet=parallelism_
1919
import jthread3Code from '@lectures/parallelism.md?snippet=parallelism_jthread_3/main.cpp';
2020
import jthreadCode from '@lectures/parallelism.md?snippet=parallelism_jthread/main.cpp';
2121
import threadpool17Code from '@lectures/parallelism.md?snippet=parallelism_threadpool_17/main.cpp';
22+
import deadlockCode from '@lectures/parallelism.md?snippet=parallelism_deadlock/main.cpp';
23+
import deadlockFixedCode from '@lectures/parallelism.md?snippet=parallelism_deadlock_fixed/main.cpp';
2224

2325
import { MyStyle } from '../../styles';
2426
import { centerOn, zoomInOn, zoomOut, getCodeBBox, getFutureCodeBBox } from '../../utils';
@@ -703,4 +705,18 @@ int main() {
703705
);
704706
yield* waitFor(duration);
705707

708+
// #### Deadlocks
709+
yield* all(
710+
codeRef().code(deadlockCode, 0),
711+
centerOn(codeRef(), DEFAULT, 0, 25),
712+
cppVersionTxt().text("", 0),
713+
);
714+
yield* waitFor(duration);
715+
716+
// #### Deadlocks Fixed
717+
yield* all(
718+
codeRef().code(deadlockFixedCode, duration),
719+
centerOn(codeRef(), DEFAULT, duration, 25),
720+
);
721+
yield* waitFor(duration);
706722
});

lectures/parallelism.md

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ Parallelism: Threads, Async, and Mutexes
55
</p>
66

77
- [Parallelism: Threads, Async, and Mutexes](#parallelism-threads-async-and-mutexes)
8+
- [Disclaimer](#disclaimer)
89
- [What is parallelism anyway?](#what-is-parallelism-anyway)
9-
- [No parallelism is often fastest](#no-parallelism-is-often-fastest)
10+
- [No parallelism is always safer and often faster](#no-parallelism-is-always-safer-and-often-faster)
1011
- [High-level Task-Based Parallelism](#high-level-task-based-parallelism)
12+
- [Execution Strategies (`std::launch`)](#execution-strategies-stdlaunch)
1113
- [Parallel Algorithms](#parallel-algorithms)
12-
- [Worker Threads and Thread Pools](#worker-threads-and-thread-pools)
13-
- [Under the Hood: Data Races, Mutexes, and Condition Variables](#under-the-hood-data-races-mutexes-and-condition-variables)
14-
- [Data Races](#data-races)
15-
- [Mutexes and Locks](#mutexes-and-locks)
16-
- [Condition Variables](#condition-variables)
14+
- [Execution Policies (`std::execution`)](#execution-policies-stdexecution)
15+
- [Raw TBB Parallelism](#raw-tbb-parallelism)
16+
- [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)
1723
- [Deadlocks](#deadlocks)
1824
- [Summary](#summary)
1925

@@ -1173,15 +1179,96 @@ int main() {
11731179

11741180
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.
11751181

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:
1225+
1226+
<!--
1227+
`CPP_COPY_SNIPPET` parallelism_deadlock_fixed/main.cpp
1228+
`CPP_RUN_CMD` CWD:parallelism_deadlock_fixed c++ -std=c++20 main.cpp
1229+
-->
1230+
```cpp
1231+
#include <chrono>
1232+
#include <iostream>
1233+
#include <mutex>
1234+
#include <thread>
1235+
1236+
namespace {
1237+
// Global variables for simplicity of an example. Don't do it in real code.
1238+
std::mutex mutex1;
1239+
std::mutex mutex2;
1240+
1241+
void ThreadA() {
1242+
std::scoped_lock lock{mutex1, mutex2};
1243+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
1244+
std::cout << "Thread A got both locks!\n";
1245+
}
1246+
1247+
void ThreadB() {
1248+
std::scoped_lock lock{mutex2, mutex1}; // Order doesn't matter for scoped_lock!
1249+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
1250+
std::cout << "Thread B got both locks!\n";
1251+
}
1252+
} // namespace
1253+
1254+
int main() {
1255+
std::jthread a{ThreadA};
1256+
std::jthread b{ThreadB};
1257+
return 0;
1258+
}
1259+
```
1260+
11761261
## Summary
1177-
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?
11781263

11791264
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:
11801265

11811266
- When faced with large tasks that have to run in the background, `std::async` seems to be the right tool.
11821267
- 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.
11831268
- 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.
11851272

11861273
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!
11871274

0 commit comments

Comments
 (0)