Launch your tech mastery with us—your coding journey starts now!
Course Content
Introduction to C++ Programming
0/2
Control Flow Statements
Control flow statements in C++ allow the program to make decisions, repeat tasks, or jump to specific parts of code based on conditions. These statements give a program logical structure and control over the sequence of execution. Mastering control flow is essential for writing efficient and responsive programs. This section covers decision-making statements, looping constructs, and jump statements in detail with syntax and examples.
0/4
Functions in C++
Functions in C++ are blocks of reusable code designed to perform a specific task. They help break large programs into smaller, manageable pieces and improve readability, modularity, and reusability. Functions reduce code duplication by allowing programmers to call the same block of logic from multiple places. This modular approach also makes debugging easier and enhances program structure and clarity.
0/4
Modern C++ and Concurrency
0/2

Introduction

Multithreading in C++ allows a program to execute multiple threads concurrently, enhancing performance, responsiveness, and efficient CPU utilization. A thread is the smallest sequence of programmed instructions that can be managed independently by a scheduler. Starting with C++11, the <thread> library provides robust support for thread creation, management, and synchronization. By splitting tasks into threads, C++ programs can perform multiple operations in parallel—crucial for modern computing where responsiveness and concurrency are vital. This section explores thread creation, joining, detaching, mutexes, deadlocks, thread safety, and condition variables with proper syntax, examples, and deeper insight.

 

std::thread and Thread Creation

The std::thread class is introduced in C++11 and allows developers to create and manage threads. Threads can be launched by passing a function (or callable object) to the std::thread constructor. Once a thread is launched, it runs concurrently with the main thread.

General Syntax:

#include <iostream>

#include <thread>

void functionName() {

    // Code to run in thread

}

int main() {

    std::thread t1(functionName); // Start thread

    t1.join(); // Wait for thread to finish

    return 0;

}

Example:

#include <iostream>

#include <thread>

using namespace std;

void printNumbers() {

    for (int i = 1; i <= 5; i++)

        cout << “Number: ” << i << endl;

}

int main() {

    thread t1(printNumbers); // Create a new thread

    t1.join(); // Main thread waits for t1 to finish

    return 0;

}

 

Joining & Detaching Threads

The join() function blocks the current thread until the thread it’s called on finishes execution. In contrast, detach() allows the thread to run independently (in the background), and its resources are automatically released upon completion.

Syntax:

thread t1(functionName);

t1.join();    // Waits for thread

// OR

t1.detach();  // Detaches thread (runs independently)

Example:

void backgroundTask() {

    cout << “Running in background…” << endl;

}

int main() {

    thread t(backgroundTask);

    t.detach(); // Now it runs independently

    cout << “Main thread ends.” << endl;

    return 0;

}

Do not call both join() and detach() on the same thread. It causes a runtime error.

 

Mutex, Lock, and Deadlock

A mutex (mutual exclusion) is used to protect shared data from being accessed simultaneously by multiple threads. If not managed properly, mutexes can cause deadlocks where two or more threads wait indefinitely for each other to release resources.

Syntax:

#include <mutex>

mutex m;

void safeAccess() {

    m.lock();      // Lock the critical section

    // Access shared data

    m.unlock();    // Unlock it

}

Example (with lock_guard):

#include <iostream>

#include <thread>

#include <mutex>

using namespace std;

mutex m;

void printSafe(string msg) {

    lock_guard<mutex> guard(m); // Automatically locks and unlocks

    cout << msg << endl;

}

int main() {

    thread t1(printSafe, “Hello”);

    thread t2(printSafe, “World”);

    t1.join();

    t2.join();

    return 0;

}

Use lock_guard or unique_lock for exception-safe locking.

 

Deadlock Example & Avoidance

Deadlocks occur when multiple threads are blocked forever, waiting for each other. This usually happens when locks are acquired in an inconsistent order across threads.

Deadlock Example:

mutex m1, m2;

void task1() {

    lock_guard<mutex> lock1(m1);

    this_thread::sleep_for(chrono::milliseconds(100));

    lock_guard<mutex> lock2(m2); // waits for m2

}

void task2() {

    lock_guard<mutex> lock2(m2);

    lock_guard<mutex> lock1(m1); // waits for m1

}

Avoidance: Use std::lock() to lock multiple mutexes atomically:

std::lock(m1, m2);

lock_guard<mutex> lock1(m1, adopt_lock);

lock_guard<mutex> lock2(m2, adopt_lock);

 

Thread-safe Practices

Thread safety means that shared data structures are protected and accessed in a way that prevents race conditions. This often involves the use of synchronization primitives like mutexes, atomic variables, or thread-safe containers.

Tips:

  • Avoid sharing state unnecessarily.
  • Use std::atomic for simple atomic operations.
  • Always lock shared resources.
  • Prefer lock_guard or unique_lock over manual locking.

Example with atomic:

#include <iostream>

#include <thread>

#include <atomic>

using namespace std;

atomic<int> counter(0);

void increment() {

    for (int i = 0; i < 1000; ++i)

        ++counter;

}

int main() {

    thread t1(increment), t2(increment);

    t1.join(); t2.join();

    cout << “Counter = ” << counter << endl;

}

 

Condition Variables

Condition variables allow threads to wait (block) until a particular condition is true. They are useful for thread communication and coordination (like producer-consumer problems).

Syntax:

#include <condition_variable>

condition_variable cv;

mutex m;

bool ready = false;

void waitTask() {

    unique_lock<mutex> lk(m);

    cv.wait(lk, []{ return ready; }); // Wait until ready == true

    // Continue execution

}

Example:

#include <iostream>

#include <thread>

#include <condition_variable>

using namespace std;

mutex m;

condition_variable cv;

bool ready = false;

void waitForSignal() {

    unique_lock<mutex> lock(m);

    cv.wait(lock, []{ return ready; });

    cout << “Thread resumed after signal!” << endl;

}

void sendSignal() {

    this_thread::sleep_for(chrono::seconds(1));

    {

        lock_guard<mutex> lock(m);

        ready = true;

    }

    cv.notify_one(); // Send signal

}

int main() {

    thread t1(waitForSignal);

    thread t2(sendSignal);

    t1.join();

    t2.join();

    return 0;

}

 

Advantages of Multithreading in C++

  1. Improved Performance: Multiple threads can run concurrently and utilize multi-core processors effectively.
  2. Responsiveness: Applications stay responsive (e.g., UI doesn’t freeze) while doing background tasks.
  3. Resource Sharing: Threads share the same memory space, making data exchange faster and easier.
  4. Efficient Parallelism: Allows performing independent tasks like downloading and processing simultaneously.

 

Disadvantages of Multithreading

  1. Complex Debugging: Multithreaded code can be difficult to debug due to non-deterministic behavior.
  2. Race Conditions: Improper handling of shared resources may lead to race conditions or corrupted data.
  3. Deadlocks: Threads waiting for each other’s resources can cause deadlocks, halting program execution.
  4. Overhead: Creating and managing threads adds overhead and complexity.

 

Applications of Multithreading

  1. Gaming and Graphics: For rendering, physics, and event handling in parallel.
  2. Web Servers: To handle multiple client requests simultaneously.
  3. Scientific Computing: Performing parallel matrix operations or simulations.
  4. Real-Time Systems: Embedded systems that perform real-time monitoring or control.

 

Limitations of Multithreading

  1. Thread Creation Limit: Systems can only create a finite number of threads depending on memory and OS.
  2. Context Switching Cost: Frequent context switching between threads can degrade performance.
  3. Shared Memory Risks: Mismanagement of shared memory can lead to hard-to-find bugs.
  4. Platform Dependency: Threading behavior and limits may vary across platforms and compilers.