学懂C++ (二十一):高级教程——深入C++多线程开发详解

  C++多线程开发详解     

         多线程编程是现代应用程序开发中不可或缺的一部分。C++11引入了对多线程的支持,使得程序员能够更方便地利用多核处理器,提高程序的性能和响应能力。本文将全面而深入地探讨C++的多线程相关概念,包括线程的创建与管理、互斥量、条件变量、线程池以及原子操作等内容。

        C++11提供了语言层面上的多线程,包含在头文件<thread>中。它解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11 新标准中引入了5个头文件来支持多线程编程,如下图所示:

5d4c9a23bc2447a3a2e1b3c1a75065c4.png

这个图表展示了C++11中与多线程相关的5个头文件:

  1. <thread>: 提供了创建和管理线程的功能,包括std::threadstd::this_thread
  2. <mutex>: 提供了互斥量和锁的功能,包括std::mutexstd::recursive_mutexstd::lock_guardstd::unique_lock
  3. <condition_variable>: 提供了条件变量的功能,包括std::condition_variable
  4. <atomic>: 提供了原子操作的支持,包括std::atomic
  5. <future>: 提供了异步计算的机制,包括std::futurestd::promisestd::async

以下是C++11中与多线程相关的主要头文件及其功能:

1. <thread>

  • 功能: 提供创建和管理线程的功能。
  • 常用类std::thread,用于启动线程并管理其生命周期。

2. <mutex>

  • 功能: 提供互斥量和锁的功能,确保在多线程环境中安全访问共享资源。
  • 常用类:
    • std::mutex:基本互斥量。
    • std::recursive_mutex:支持递归加锁的互斥量。
    • std::lock_guardstd::unique_lock:RAII风格的锁管理类。

3. <condition_variable>

  • 功能: 实现线程间的条件变量,用于阻塞线程直到某个条件满足。
  • 常用类std::condition_variable,用于线程之间的同步。

4. <atomic>

  • 功能: 提供原子操作的支持,确保在多线程环境中对共享数据的安全访问而无需使用互斥量。
  • 常用类std::atomic,支持各种基本数据类型。

5. <future>

  • 功能: 提供异步计算的机制,包括获取线程计算结果的功能。
  • 常用类:
    • std::future:用于获取异步操作的结果。
    • std::promise:用于在异步操作中设置值。
    • std::async:启动异步任务并返回std::future

通过使用这些头文件和类,C++开发者可以更方便地编写高效和安全的多线程程序。

1. 多线程

在C++11之前,实现多线程主要依赖于操作系统提供的API,如Linux的<pthread.h>或Windows的<windows.h>。C++11通过引入<thread>头文件,提供了语言层面的多线程支持,解决了跨平台的问题。

1.1 多进程与多线程

  • 多进程并发:将应用程序划分为多个独立的进程,每个进程有独立的地址空间。虽然多进程提供了更好的隔离性,但进程间的通信复杂且速度较慢。此外,进程创建和销毁的开销相对较大。

  • 多线程并发:在同一个进程中执行多个线程,这些线程共享相同的地址空间。线程的轻量级特性和共享内存的优势使得多线程更适合于高效的数据共享和通信。

1.2 多线程理解

在单CPU内核上,多个线程的执行是通过时间片轮转的方式实现的,并不是真正的并行计算。而在多CPU或多核系统上,可以实现真正的并行处理,从而提升程序的执行效率。

1.3 创建线程

创建线程非常简单,可以使用std::thread类来启动线程。以下是几种创建线程的方式

#include <iostream>
#include <thread>

void thread_1() {
    std::cout << "子线程1" << std::endl;
}

void thread_2(int x) {
    std::cout << "x:" << x << std::endl;
    std::cout << "子线程2" << std::endl;
}

int main() {
    std::thread first(thread_1);         // 无参数线程
    std::thread second(thread_2, 100);   // 有参数线程

    std::cout << "主线程" << std::endl;

    first.join();  // 等待第一个线程结束
    second.join(); // 等待第二个线程结束

    std::cout << "子线程结束." << std::endl; // 确保所有线程完成后输出
    return 0;
}

 

1.4 join与detach方式

(1) join举例

使用join会使主线程等待子线程完成后再继续执行:

#include <iostream>
#include <thread>

void thread_function() {
    while (true) {
        // 执行一些操作
    }
}

int main() {
    std::thread th(thread_function);
    th.join();  // 等待线程结束
    return 0;
}

(2) detach举例

使用detach可以让线程在后台运行,主线程不会等待其结束:

#include <iostream>
#include <thread>

void thread_function() {
    while (true) {
        std::cout << "子线程在运行" << std::endl;
    }
}

int main() {
    std::thread th(thread_function);
    th.detach();  // 子线程在后台运行
    for (int i = 0; i < 10; ++i) {
        std::cout << "主线程在运行" << std::endl;
    }
    return 0; // 主线程结束时,子线程也可能被终止
}

1.5 this_thread

std::this_thread是一个类,它有4个功能函数,具体如下:

  • std::this_thread::get_id():获取当前线程的ID。
  • std::this_thread::yield():让当前线程放弃执行,回到就绪状态。
  • std::this_thread::sleep_for(...):暂停线程指定的时间。
  • std::this_thread::sleep_until(...):暂停线程直到指定的时间点。
#include <iostream>
#include <thread>
#include <chrono>

void demonstrate_this_thread_functions() {
    // 获取当前线程的ID
    std::cout << "Current thread ID: " << std::this_thread::get_id() << std::endl;

    // 让当前线程放弃执行,回到就绪状态
    std::cout << "Thread is yielding..." << std::endl;
    std::this_thread::yield();
    std::cout << "Thread resumed execution after yield." << std::endl;

    // 暂停线程指定的时间
    std::cout << "Thread is sleeping for 2 seconds..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Thread woke up after sleep_for." << std::endl;

    // 暂停线程直到指定的时间点
    auto wake_up_time = std::chrono::system_clock::now() + std::chrono::seconds(2);
    std::cout << "Thread will sleep until a time point in the future..." << std::endl;
    std::this_thread::sleep_until(wake_up_time);
    std::cout << "Thread woke up after sleep_until." << std::endl;
}

int main() {
    std::thread t(demonstrate_this_thread_functions);
    t.join();
    return 0;
}

 示例说明:

  1. 获取当前线程的ID

    std::cout << "Current thread ID: " << std::this_thread::get_id() << std::endl;

    通过 std::this_thread::get_id() 获取当前线程的ID并输出。

  2. 让当前线程放弃执行,回到就绪状态

    std::cout << "Thread is yielding..." << std::endl; 
    std::this_thread::yield(); 
    std::cout << "Thread resumed execution after yield." << std::endl;

    使用 std::this_thread::yield() 让当前线程放弃执行,回到就绪状态。之后线程会重新开始执行。

  3. 暂停线程指定的时间

    std::cout << "Thread is sleeping for 2 seconds..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Thread woke up after sleep_for." << std::endl;
    

    使用 std::this_thread::sleep_for(std::chrono::seconds(2)) 暂停当前线程2秒。

  4. 暂停线程直到指定的时间点

    auto wake_up_time = std::chrono::system_clock::now() + std::chrono::seconds(2);
    std::cout << "Thread will sleep until a time point in the future..." << std::endl;
    std::this_thread::sleep_until(wake_up_time);
    std::cout << "Thread woke up after sleep_until." << std::endl;
    

    使用 std::this_thread::sleep_until(wake_up_time) 暂停当前线程直到指定的时间点(当前时间点加2秒)。

通过这个综合示例,您可以看到 std::this_thread 中各个功能函数的实际应用。

2. mutex

互斥量是用于保护共享资源的一种机制。C++11提供了几种不同类型的互斥量:

  • std::mutex:最基本的互斥量。
  • std::recursive_mutex:支持递归加锁的互斥量。
  • std::timed_mutex:支持超时的互斥量。
  • std::recursive_timed_mutex:支持递归和超时的互斥量。

2.1 lock与unlock

互斥量的基本操作包括:

  • lock():加锁。
  • unlock():解锁。
  • try_lock():尝试加锁。
    #include <iostream>
    #include <thread>
    #include <mutex>
    
    std::mutex mtx;
    
    void print_block(int n, char c) {
        mtx.lock(); // 上锁
        for (int i = 0; i < n; ++i) {
            std::cout << c;
        }
        std::cout << '\n';
        mtx.unlock(); // 解锁
    }
    
    int main() {
        std::thread th1(print_block, 50, '*');
        std::thread th2(print_block, 50, '$');
    
        th1.join();
        th2.join();
        return 0;
    }
    

     

2.2 lock_guard

std::lock_guard提供了一个简单的RAII风格的互斥量管理方式,确保在作用域内自动加锁和解锁。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex g_i_mutex;
int g_i = 0;

void safe_increment() {
    std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
}

int main() {
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);

    t1.join();
    t2.join();
    return 0;
}

 

2.3 unique_lock

std::unique_lockstd::lock_guard的增强版本,支持更复杂的锁定需求。它允许手动加锁和解锁,并可以在构造时选择不加锁。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void example() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不立刻锁
    // do some work
    lock.lock(); // 后来再锁
    // work with the lock
}

3. condition_variable

条件变量用于线程间的同步,允许一个或多个线程等待某个条件的发生。它必须与互斥量结合使用。

3.1 wait

使用wait()时,调用线程会被阻塞,直到其他线程调用notify_one()notify_all()唤醒它。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
int cargo = 0;

void consume() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return cargo != 0; }); // 等待条件
    std::cout << "消费货物: " << cargo << std::endl;
    cargo = 0; // 重置状态
}

int main() {
    std::thread consumer(consume);
    
    {
        std::unique_lock<std::mutex> lock(mtx);
        cargo = 10; // 设置货物
        cv.notify_one(); // 通知消费者
    }

    consumer.join();
    return 0;
}

3.2 wait_for

使用wait_for可以设置超时,等待条件或超时返回。

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>

std::condition_variable cv;

int value;

void read_value() {
    std::cin >> value;
    cv.notify_one();
}

int main() {
    std::cout << "Please, enter an integer (I'll be printing dots): \n";
    std::thread th(read_value);

    std::mutex mtx;
    std::unique_lock<std::mutex> lock(mtx);
    while (cv.wait_for(lock, std::chrono::seconds(1))==std::cv_status::timeout) {
        std::cout << '.' << std::endl;
    }
    std::cout << "You entered: " << value << '\n';

    th.join();
    return 0;
}

4. 线程池

4.1 概念

线程池是一种设计模式,通过维护一定数量的线程,来处理多个任务,从而避免频繁的线程创建与销毁带来的开销。

4.2 线程池的实现

一个基本的线程池包括线程管理、工作线程和任务队列。下面是一个简单的线程池实现:

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>

class ThreadPool {
public:
    ThreadPool(size_t num_threads);
    ~ThreadPool();

    template<class F>
    void enqueue(F&& f);

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

ThreadPool::ThreadPool(size_t num_threads) : stop(false) {
    for(size_t i = 0; i < num_threads; ++i) {
        workers.emplace_back([this] {
            for(;;) {
                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(); // 运行任务
            }
        });
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers) {
        worker.join();
    }
}

template<class F>
void ThreadPool::enqueue(F&& f) {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        tasks.emplace(std::forward<F>(f));
    }
    condition.notify_one(); // 通知工作线程
}

// 使用示例
int main() {
    ThreadPool pool(4); // 创建线程池
    
    for(int i = 0; i < 8; ++i) {
        pool.enqueue([i] {
            std::cout << "任务 " << i << " 正在执行" << std::endl;
        });
    }
    return 0;
}

5. 原子操作

原子操作是指在多线程环境中不会被其他线程中断的操作。C++11引入了<atomic>头文件,提供了原子类型和原子操作的支持,确保在并发环境中对某些数据的安全访问。

5.1 原子类型

C++标准库提供了多种原子类型,如std::atomic<int>std::atomic<bool>等。使用原子变量,可以避免使用互斥量来保护共享数据的访问,进而提高性能。

5.2 原子操作示例

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter++; // 原子操作
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter.load() << std::endl; // 安全读取
    return 0;
}

 

5.3 原子操作的优点

  1. 性能更高:原子操作通常比使用互斥量更高效,因为它们不涉及线程上下文切换和锁的开销。
  2. 简单易用:在某些场景下,使用原子类型可以简化代码,减少错误的可能性。

5.4 原子操作的使用场景

  • 计数器:多个线程对一个整数计数。
  • 标志位:多个线程对某个状态的检查。
  • 状态机:在多线程环境中实现简单的状态机。

总结

C++11为多线程编程提供了强大的支持,通过引入std::threadstd::mutexstd::condition_variablestd::atomic等类,使得跨平台的多线程开发变得更加简便。通过合理的管理线程,使用互斥量保护共享资源,结合条件变量实现线程之间的同步,使用原子操作提高性能,可以有效地提高程序的性能和响应能力。此外,线程池可以帮助优化线程创建与销毁的开销,使得资源利用更加高效。希望本文能为你在C++多线程开发中提供全面的指导。

 

(C++多线程讲解后续持续更新,敬请关注!)

 

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值