C++ Learning Manual – New Features 48 – Concurrency Programming (std::thread, std::async)

In modern computer architecture, concurrency programming has become a key technology for enhancing application performance. The C++11 standard introduced powerful support for concurrency programming, allowing developers to write multithreaded programs in a more intuitive and safer manner. Among these, <span>std::thread</span> and <span>std::async</span> are two of the most important tools for concurrency programming.

1. Why is Concurrency Programming Needed?

With the prevalence of multi-core processors, fully utilizing hardware resources has become a necessary means to improve program performance. Concurrency programming allows us to:

  • Enhance program performance: By processing tasks in parallel, reduce overall execution time
  • Improve responsiveness: Execute time-consuming operations in the background, keeping the user interface smooth
  • Simplify complex problems: Break down large problems into smaller tasks that can be processed in parallel

2. std::thread: Basic Thread Management

<span>std::thread</span> is the core class in C++11 used for creating and managing threads.

Basic Usage
#include <iostream>
#include <thread>

void helloFunction() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    // Create and start thread
    std::thread t(helloFunction);
    
    // Wait for thread to finish
    t.join();
    
    std::cout << "Hello from main!" << std::endl;
    return 0;
}
Thread Function with Parameters
#include <iostream>
#include <thread>
#include <string>

void printMessage(const std::string& message, int count) {
    for (int i = 0; i < count; ++i) {
        std::cout << message << std::endl;
    }
}

int main() {
    std::thread t(printMessage, "Hello from thread!", 3);
    t.join();
    return 0;
}
Lambda Expressions and Threads
#include <iostream>
#include <thread>

int main() {
    std::thread t([](){
        std::cout << "Hello from lambda thread!" << std::endl;
    });
    
    t.join();
    return 0;
}

3. std::async: Asynchronous Task Handling

<span>std::async</span> provides a higher-level asynchronous programming approach that can automatically manage the creation and destruction of threads.

Basic Usage
#include <iostream>
#include <future>
#include <chrono>

int computeFactorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    return result;
}

int main() {
    // Start asynchronous task
    std::future<int> futureResult = std::async(std::launch::async, computeFactorial, 5);
    
    // Main thread can continue executing other work
    std::cout << "Main thread working..." << std::endl;
    
    // Get the result of the asynchronous task (if needed)
    int result = futureResult.get();
    std::cout << "Factorial result: " << result << std::endl;
    
    return 0;
}
Launch Policies

<span>std::async</span> supports two launch policies:

// Asynchronous execution (executed in a new thread)
auto future1 = std::async(std::launch::async, computeFactorial, 5);

// Deferred execution (executed when get() or wait() is called)
auto future2 = std::async(std::launch::deferred, computeFactorial, 5);

// Automatic policy selection (determined by implementation)
auto future3 = std::async(computeFactorial, 5);

4. Thread Synchronization and Data Sharing

When multiple threads access shared data, synchronization mechanisms are needed to avoid race conditions.

Using std::mutex to Protect Shared Data
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex coutMutex;

void safePrint(const std::string& message) {
    std::lock_guard<std::mutex> lock(coutMutex);
    std::cout << message << std::endl;
}

void threadFunction(int id) {
    safePrint("Thread " + std::to_string(id) + " is running");
}

int main() {
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(threadFunction, i);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

5. Exception Handling

Correctly handling exceptions in asynchronous operations is crucial.

#include <iostream>
#include <future>
#include <stdexcept>

int riskyComputation(int x) {
    if (x < 0) {
        throw std::runtime_error("Negative value not allowed");
    }
    return x * 2;
}

int main() {
    try {
        auto future = std::async(std::launch::async, riskyComputation, -5);
        int result = future.get(); // This will throw an exception
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    
    return 0;
}

6. Practical Application Examples

Parallel Data Processing
#include <iostream>
#include <vector>
#include <future>
#include <numeric>

// Parallel computation of the sum of vector elements
template<typename Iterator>
int parallelSum(Iterator begin, Iterator end) {
    auto length = std::distance(begin, end);
    if (length <= 0) return 0;
    
    const int minPerThread = 25;
    if (length < 2 * minPerThread) {
        return std::accumulate(begin, end, 0);
    }
    
    auto mid = begin;
    std::advance(mid, length / 2);
    
    auto leftHalf = std::async(std::launch::async, parallelSum<Iterator>, begin, mid);
    int rightSum = parallelSum(mid, end);
    
    return leftHalf.get() + rightSum;
}

int main() {
    std::vector<int> numbers(1000);
    for (int i = 0; i < 1000; ++i) {
        numbers[i] = i + 1;
    }
    
    int sum = parallelSum(numbers.begin(), numbers.end());
    std::cout << "Sum of 1 to 1000: " << sum << std::endl;
    
    return 0;
}

7. Best Practices and Considerations

  1. Resource Management: Ensure all threads are joined or detached before destruction
  2. Avoid Data Races: Use appropriate synchronization mechanisms to protect shared data
  3. Thread Count: Set the number of threads reasonably based on hardware concurrency capabilities
  4. Exception Safety: Ensure exceptions do not lead to resource leaks or program crashes
  5. Performance Considerations: Thread creation and context switching have overhead, avoid excessive use
// Use RAII to manage thread resources
class ThreadGuard {
    std::thread& t;
public:
    explicit ThreadGuard(std::thread& t_) : t(t_) {}
    ~ThreadGuard() {
        if (t.joinable()) {
            t.join();
        }
    }
    ThreadGuard(const ThreadGuard&) = delete;
    ThreadGuard& operator=(const ThreadGuard&) = delete;
};

void safeThreadManagement() {
    std::thread t([](){ /* Thread task */ });
    ThreadGuard g(t);
    // Even if an exception is thrown, the thread will be correctly joined
}

Conclusion

The <span>std::thread</span> and <span>std::async</span> introduced in C++11 provide powerful and flexible tools for concurrency programming:

  • <span>std::thread</span>: Provides low-level thread control, suitable for scenarios requiring fine management
  • <span>std::async</span>: Provides a high-level abstraction for asynchronous tasks, automatically managing thread lifecycles
  • Combined Use: Both can be used together to build complex concurrent systems

Mastering these tools not only allows your programs to run faster but also helps you better understand the concurrency programming model of modern C++. As the C++ standard continues to evolve, support for concurrency programming is also being continuously improved, so it is recommended to keep an eye on new features and best practices.

Leave a Comment