C++17多线程编程全面指南

引言

在当今多核处理器普及的时代,多线程编程已成为充分利用硬件性能的关键技术。C++17标准通过引入并行算法、共享互斥锁等新特性,进一步增强了C++的并发编程能力,使开发者能够更高效地编写线程安全、高性能的并行应用程序。本文将全面介绍C++17中多线程编程的各种技术、使用场景及最佳实践,帮助开发者掌握现代C++并发编程的精髓。

基础概念回顾

线程创建与管理

C++11引入的std::thread类是创建和管理线程的基础。在C++17中,这一功能得到了进一步的完善和优化:

cpp 复制代码
#include <iostream>
#include <thread>

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

int main() {
    // 创建线程
    std::thread t(helloFunction);
    
    // 等待线程完成
    t.join();
    
    // 也可以使用detach()让线程在后台运行,但需要确保线程资源正确管理
    // t.detach();
    
    return 0;
}

创建线程时,我们还可以使用lambda表达式、函数对象或成员函数:

cpp 复制代码
// 使用lambda表达式
std::thread t1([]() {
    std::cout << "Hello from lambda thread" << std::endl;
});

t1.join();

// 使用成员函数
class Worker {
public:
    void doWork() {
        std::cout << "Hello from member function thread" << std::endl;
    }
};

Worker worker;
std::thread t2(&Worker::doWork, &worker);
t2.join();

线程安全与同步原语

在多线程环境中,多个线程访问共享数据可能导致数据竞争和不一致。C++17提供了多种同步原语来确保线程安全:

  • std::mutex:最基本的互斥锁,确保在同一时间只有一个线程可以访问共享资源
  • std::lock_guard:RAII风格的互斥锁包装器,自动管理锁的获取和释放
  • std::unique_lock:更灵活的互斥锁包装器,支持延迟锁定、条件变量等高级功能
  • std::condition_variable:用于线程间通信,等待特定条件达成
  • std::atomic:提供原子操作,无需显式锁定即可确保线程安全

C++17多线程新特性

并行算法

C++17最大的亮点之一是引入了并行算法,这些算法位于<algorithm>头文件中,可以通过执行策略参数控制算法的执行方式:

cpp 复制代码
#include <vector>
#include <algorithm>
#include <execution>
#include <iostream>

void parallelTransformExample() {
    std::vector<int> input(1000000, 1);
    std::vector<int> output(input.size());
    
    // 并行执行transform操作
    std::transform(std::execution::par,
                   input.begin(), input.end(),
                   output.begin(),
                   [](int x) { return x * 2; });
    
    // 还可以使用sequenced_policy (seq)和parallel_unsequenced_policy (par_unseq)
    // std::execution::seq - 顺序执行
    // std::execution::par_unseq - 并行执行,可能会向量化处理
}

并行算法使得开发者可以轻松地将串行代码转换为并行代码,而无需手动管理线程。这些算法会根据系统资源自动调整线程数量,实现最优性能。

std::shared_mutex

C++17引入了读写锁std::shared_mutex,允许多个读操作同时进行,但写操作需要独占访问。这在读取频繁而写入较少的场景下可以显著提高性能:

cpp 复制代码
#include <shared_mutex>
#include <string>
#include <map>

class ConcurrentMap {
private:
    mutable std::shared_mutex mutex;
    std::map<std::string, int> data;
    
public:
    // 读操作使用shared_lock
    int getValue(const std::string& key) const {
        std::shared_lock<std::shared_mutex> lock(mutex);
        auto it = data.find(key);
        return (it != data.end()) ? it->second : 0;
    }
    
    // 写操作使用unique_lock
    void setValue(const std::string& key, int value) {
        std::unique_lock<std::shared_mutex> lock(mutex);
        data[key] = value;
    }
};

常见使用场景

生产者-消费者模式

生产者-消费者模式是多线程编程中最经典的模式之一,通常使用条件变量来实现:

cpp 复制代码
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>

class ThreadSafeQueue {
private:
    std::queue<int> queue;
    mutable std::mutex mutex;
    std::condition_variable cv;
    bool finished = false;
    
public:
    void push(int value) {
        {   // 作用域控制锁的生命周期
            std::lock_guard<std::mutex> lock(mutex);
            queue.push(value);
        }
        cv.notify_one(); // 通知一个等待的消费者
    }
    
    bool pop(int& value) {
        std::unique_lock<std::mutex> lock(mutex);
        cv.wait(lock, [this] { 
            return finished || !queue.empty(); 
        });
        
        if (queue.empty()) return false;
        
        value = queue.front();
        queue.pop();
        return true;
    }
    
    void setFinished() {
        {   
            std::lock_guard<std::mutex> lock(mutex);
            finished = true;
        }
        cv.notify_all(); // 通知所有等待的消费者
    }
};

void producer(ThreadSafeQueue& queue, int count) {
    for (int i = 0; i < count; ++i) {
        queue.push(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    queue.setFinished();
}

void consumer(ThreadSafeQueue& queue, const std::string& name) {
    int value;
    while (queue.pop(value)) {
        std::cout << name << " consumed: " << value << std::endl;
    }
}

int main() {
    ThreadSafeQueue queue;
    
    std::thread producerThread(producer, std::ref(queue), 10);
    std::thread consumer1(consumer, std::ref(queue), "Consumer 1");
    std::thread consumer2(consumer, std::ref(queue), "Consumer 2");
    
    producerThread.join();
    consumer1.join();
    consumer2.join();
    
    return 0;
}

线程池实现

线程池是一种重用线程的设计模式,避免频繁创建和销毁线程带来的开销。下面是一个基于C++17的线程池实现:

cpp 复制代码
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
    
public:
    ThreadPool(size_t threads) : stop(false) {
        for(size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this] {
                while(true) {
                    std::function<void()> task;
                    
                    {   
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock, [this] {
                            return this->stop || !this->tasks.empty();
                        });
                        
                        if(this->stop && this->tasks.empty()) return;
                        
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    
                    task();
                }
            });
        }
    }
    
    // 添加任务并返回future
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type> {
        
        using return_type = typename std::result_of<F(Args...)>::type;
        
        auto task = std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
        
        std::future<return_type> result = task->get_future();
        
        {   
            std::unique_lock<std::mutex> lock(queue_mutex);
            
            if(stop) throw std::runtime_error("enqueue on stopped ThreadPool");
            
            tasks.emplace([task]() { (*task)(); });
        }
        
        condition.notify_one();
        return result;
    }
    
    ~ThreadPool() {
        {   
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        
        condition.notify_all();
        
        for(std::thread &worker: workers) {
            worker.join();
        }
    }
};

使用示例:

cpp 复制代码
int main() {
    // 创建4线程的线程池
    ThreadPool pool(4);
    std::vector<std::future<int>> results;
    
    for(int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] { 
                std::cout << "Task " << i << " is running" << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                return i * i;
            })
        );
    }
    
    for(auto &&result: results) {
        std::cout << "Result: " << result.get() << std::endl;
    }
    
    return 0;
}

并行计算与任务分解

对于计算密集型任务,可以将工作负载分解为多个子任务,利用多线程并行执行以提高性能:

cpp 复制代码
#include <vector>
#include <thread>
#include <numeric>
#include <iostream>
#include <future>

// 计算子数组的和
long long sumRange(const std::vector<int>& data, size_t start, size_t end) {
    long long sum = 0;
    for (size_t i = start; i < end; ++i) {
        sum += data[i];
    }
    return sum;
}

// 并行计算数组总和
long long parallelSum(const std::vector<int>& data) {
    const size_t num_threads = std::thread::hardware_concurrency();
    const size_t chunk_size = data.size() / num_threads;
    
    std::vector<std::future<long long>> futures;
    
    // 提交计算任务
    for (size_t i = 0; i < num_threads; ++i) {
        size_t start = i * chunk_size;
        size_t end = (i == num_threads - 1) ? data.size() : (i + 1) * chunk_size;
        
        futures.emplace_back(
            std::async(std::launch::async, 
                      sumRange, std::cref(data), start, end)
        );
    }
    
    // 收集结果
    long long total = 0;
    for (auto& future : futures) {
        total += future.get();
    }
    
    return total;
}

高级主题

无锁编程与原子操作

原子操作是实现无锁数据结构的基础,C++17中的std::atomic提供了丰富的原子操作API:

cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>

class AtomicCounter {
private:
    std::atomic<int> value;
    
public:
    AtomicCounter() : value(0) {}
    
    // 原子递增并返回新值
    int increment() {
        return ++value; // 原子操作
    }
    
    // 获取当前值
    int getValue() const {
        return value.load();
    }
};

无锁编程的优势在于避免了锁竞争带来的开销,但需要更加小心地处理内存顺序和原子操作语义。

线程局部存储

thread_local关键字可以为每个线程创建变量的独立副本,避免了共享变量带来的线程安全问题:

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>

thread_local int threadLocalVar = 0;

void worker(int id) {
    threadLocalVar = id; // 每个线程都有自己独立的副本
    std::cout << "Thread " << std::this_thread::get_id() 
              << " - Local value: " << threadLocalVar << std::endl;
    
    // 修改不会影响其他线程
    threadLocalVar++;
    
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    
    std::cout << "Thread " << std::this_thread::get_id() 
              << " - Modified local value: " << threadLocalVar << std::endl;
}

性能优化与最佳实践

避免常见陷阱

  1. 线程过多:创建过多线程会导致线程调度开销大于计算收益。通常线程数量不应超过CPU核心数的2倍。

  2. 虚假共享:多个线程频繁修改同一缓存行的不同变量会导致缓存一致性协议开销。使用缓存行填充技术可以避免这一问题。

  3. 过度同步:锁的范围过大或过于频繁的锁操作会导致性能下降。尽量减小临界区大小,使用细粒度锁。

性能优化技巧

  1. 使用std::launch::deferred:对于短期任务,可以延迟启动,避免创建过多线程

  2. 任务粒度控制:合理划分任务大小,避免任务过细导致的调度开销,或任务过粗导致的负载不均

  3. 使用std::call_once:确保初始化代码只执行一次,比手动加锁更高效

  4. 使用std::scoped_lock:一次锁定多个互斥锁,避免死锁

cpp 复制代码
#include <mutex>
#include <iostream>

std::mutex mtx1, mtx2;

void safeLock() {
    // 安全地锁定多个互斥锁,避免死锁
    std::scoped_lock lock(mtx1, mtx2);
    
    // 临界区代码
    std::cout << "Critical section with multiple locks" << std::endl;
    
    // 锁会在作用域结束时自动释放
}
  1. 使用移动语义:减少线程间的数据拷贝,提高性能
cpp 复制代码
// 传递大对象时使用std::move避免拷贝
std::thread t(processLargeData, std::move(largeObject));

结语

C++17的多线程功能为现代应用程序提供了强大的并行处理能力。通过合理使用并行算法、线程池、原子操作等技术,开发者可以充分发挥多核处理器的性能,同时保持代码的可读性和可维护性。

在实际开发中,应根据具体场景选择合适的多线程模式,并注意线程安全和性能优化问题。随着C++20/23的推出,更多先进的并发特性如协程、同步原语等将进一步丰富C++的多线程编程模型,为高性能应用开发提供更多可能。


参考资料:

相关推荐
Chrikk41 分钟前
【下篇】C++20 约束、NCCL 通信与并发模型
c++·c++20·c++40周年
獭.獭.43 分钟前
C++ -- STL【vector的使用】
c++·stl·vector
郝学胜-神的一滴1 小时前
Linux C++系统编程:使用mmap创建匿名映射区
linux·服务器·开发语言·c++·程序人生
李余博睿(新疆)1 小时前
双向指针算法(练习)
c++
新手村领路人1 小时前
c++ opencv缺少openh264-1.8.0-win64.dll
开发语言·c++
周杰伦fans1 小时前
C# - 直接使用 new HttpClient() 和使用 HttpClientFactory 的区别
开发语言·c#
kyle~1 小时前
C++ --- noexcept关键字 明确函数不抛出任何异常
java·开发语言·c++
不知所云,1 小时前
6. c++ 20 Modules 使用
开发语言·c++20·c++ modules
lijiatu100861 小时前
[C++ ]qt槽函数及其线程机制
c++·qt