This episode covers concurrency in modern C++: creating threads with std::thread, synchronization with std::mutex and std::lock_guard, condition variables, std::future and std::promise along with std::async, avoiding data races and deadlocks, as well as thread pools.

Modern processors have many cores, and using them requires concurrency — running many pieces of work simultaneously. Since C++11, concurrency has been an official part of the language through the <thread> library, so you don't need to rely on operating system APIs.
Episode 12 builds your concurrency understanding step by step: creating threads with std::thread, protecting shared data with std::mutex and std::lock_guard, synchronizing work with condition variables, retrieving results with std::future and std::async, avoiding data races and deadlocks, and assembling it all into a thread pool.
std::thread takes a function along with its arguments and runs it on another core. You must call join() so main waits for the thread to finish, or detach() to release it:
cat > thread.cpp <<'EOF'
#include <iostream>
#include <thread>
void kerja(int id) {
std::cout << "Thread " << id << " bekerja\n";
}
int main() {
std::thread t1(kerja, 1);
std::thread t2(kerja, 2);
t1.join();
t2.join();
std::cout << "Semua selesai\n";
}
EOF
g++ -std=c++20 thread.cpp -pthread -o thread
./threadstd::thread t1(kerja, 1) runs the function kerja with argument 1 on a new thread. The -pthread flag is required at compile time so the thread library is linked, and t1.join() makes main wait for the thread to finish.
The program above can produce random output — two threads write to std::cout simultaneously. That's an example of a data race: two threads accessing shared data without synchronization. The solution is a mutex.
A mutex locks access to data so only one thread enters at a time. std::lock_guard wraps the mutex and locks it automatically for the whole scope — safe against exceptions because it's managed by RAII:
cat > mutex.cpp <<'EOF'
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int counter = 0;
void tambah(int n) {
for (int i = 0; i < n; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::vector<std::thread> ts;
for (int i = 0; i < 4; ++i) {
ts.emplace_back(tambah, 1000);
}
for (auto& t : ts) {
t.join();
}
std::cout << "Counter: " << counter << "\n";
}
EOF
g++ -std=c++20 mutex.cpp -pthread -o mutex
./mutexstd::lock_guard<std::mutex> lock(mtx) locks mtx when created and unlocks when leaving the block. Without a mutex, four threads incrementing counter concurrently would lose many increments because the read-modify-write operations overwrite each other.
A deadlock happens when two threads each wait for a mutex held by the other. The golden rule: lock multiple mutexes in the same order in all threads, or use std::lock, which locks several mutexes at once safely.
A condition variable makes a thread wait for a condition before continuing. The classic producer-consumer pattern: one thread produces, another waits. Use std::unique_lock with a condition variable because wait unlocks and relocks:
cat > condvar.cpp <<'EOF'
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool siap = false;
void pekerja() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return siap; });
std::cout << "Pekerja mulai bekerja\n";
}
int main() {
std::thread t(pekerja);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock(mtx);
siap = true;
}
cv.notify_one();
t.join();
}
EOF
g++ -std=c++20 condvar.cpp -pthread -o condvar
./condvarcv.wait(lock, [] { return siap; }) releases the lock, waits until siap becomes true, then relocks it. cv.notify_one() wakes up the waiting thread. The second predicate argument avoids missed wakeups.
Returning a value directly from a thread isn't possible. The solution is std::future and std::promise: the promise fills in the value, the future retrieves it. The most practical way is std::async, which combines both:
cat > async.cpp <<'EOF'
#include <iostream>
#include <future>
int kuadrat(int x) {
return x * x;
}
int main() {
std::future<int> hasil = std::async(std::launch::async, kuadrat, 9);
std::cout << "Hasil: " << hasil.get() << "\n";
}
EOF
g++ -std=c++20 async.cpp -pthread -o async
./asyncstd::async(std::launch::async, kuadrat, 9) runs kuadrat(9) asynchronously and returns a std::future<int>. hasil.get() waits for the result. If the function throws an exception, get() rethrows it in the caller.
Creating a thread is expensive. For many tasks, create a fixed number of threads (for example, as many as cores) and distribute the work — this is a thread pool. The idea is simple: a shared task queue and workers that take one at a time. Each worker waits on cv_.wait, processes incoming tasks, and stops when the destructor sets berhenti_ and wakes all workers. This pattern is the foundation of servers and large parallel applications.
Warning
std::cout output from many threads can get mixed up. For production logging, serialize it with a mutex or use a logging library — episode 22 will cover that.
Here's what to take away:
std::thread runs a function on a new thread; you must join or detach.std::mutex and std::lock_guard protect shared data from data races.std::async and std::future easily retrieve results from threads.In the next episode, episode 13, we'll discuss networking basics — TCP and UDP socket programming with BSD sockets, client-server communication and simple protocol design, byte order and address structures, error handling, as well as cross-platform considerations on Windows and Linux.