C++多线程编程

发布于:2025-02-23 ⋅ 阅读:(13) ⋅ 点赞:(0)

几个概念

同步和异步:同步和异步是两种常见的编程模式,特别是在处理任务执行时,它们定义了如何处理任务的顺序和等待时间。

同步 (Synchronous)

  • 定义:同步意味着任务按顺序执行,一个任务必须等前一个任务完成之后才能开始。也就是说,当一个任务运行时,程序会等待该任务完成后再继续执行下一个任务。

  • 特点:执行顺序严格,任务完成前不能开始下一个任务。

  • 例子:比如你去银行排队办理业务,只有前一个客户办理完了,才能轮到你。

异步 (Asynchronous)

  • 定义:异步意味着任务可以并行执行。任务的执行不需要等待其他任务完成,可以在等待任务完成的同时继续执行其他任务。
  • 特点:任务可以在后台执行,程序在等待一个任务时可以继续做其他事情。
  • 例子:就像在银行办理业务时,你可以同时去旁边的自动取款机取钱,不需要等待前面客户的业务完成。

并行和并发:并行(Parallelism)和并发(Concurrency)是计算机科学中描述多任务处理的两个术语,虽然它们常常一起出现,但有着不同的含义。

并发(Concurrency)

  • 定义:并发是指在同一时间段内,系统可以管理多个任务(或线程)的执行,但这些任务不一定是同时执行的。并发的目标是有效地切换和管理多个任务的执行,使它们看起来像是同时进行的。
  • 特点:并发不一定要求任务同时执行,它强调的是任务的合理安排和调度。它涉及任务的交替执行,通常依赖于操作系统的调度策略。并发是一个管理多个任务的概念,可以通过单核或多核CPU实现。
  • 例子:在一个单核CPU上,操作系统通过快速切换任务,使得看起来像多个任务在“同时”进行,这就是并发。比如,在一个程序中,一边处理用户输入,一边处理网络请求,虽然 CPU 可能只有一个核心,但通过任务切换,给用户提供了“并发执行”的感觉。

并行(Parallelism)

  • 定义:并行是指同时执行多个任务,通常是在多核处理器或多处理器系统上实现的。并行计算的目标是加速任务的执行,通过同时处理多个任务来提高计算效率。
  • 特点:并行要求任务真正同时执行。它是硬件支持的,通常依赖多核或多处理器系统。并行能够显著提升处理速度,尤其在计算密集型任务中。
  • 例子:在一个多核CPU上,可以同时运行多个线程,每个线程在不同的核心上执行,这就是并行。例如,科学计算中的矩阵运算,可以将不同的计算任务分配到多个处理器上并行执行,从而提高计算效率。

并发与并行的主要区别:

  • 执行方式:
    并发:多个任务交替执行,看起来像是同时进行,但实际上它们是顺序执行的。
    并行:多个任务真正同时执行。

  • 资源需求:
    并发:可以在单核或多核的系统中实现,依赖操作系统的调度来切换任务。
    并行:需要多核或多处理器的硬件支持,任务需要被分割成可以并行执行的部分。

  • 适用场景:
    并发:适用于I/O密集型任务(例如网络请求、文件操作等),在这些任务中,程序常常需要等待某些操作完成。
    并行:适用于计算密集型任务(例如科学计算、图像处理等),通过同时处理多个数据块来加速执行。

  • 举例
    并发:假设你有三个任务,分别是任务A、任务B、任务C。它们在同一个CPU上执行,任务A、B和C会交替执行,操作系统会在不同的时间片切换它们,给你一种它们在并行执行的感觉。
    并行:假设你有三个任务,分别是任务A、任务B、任务C。它们在三个不同的CPU核心上执行,任务A、B、C确实是同时运行的。

C++多线程

在 C++ 中,多线程编程可以用来提高程序的性能,尤其是当程序涉及到计算密集型或 I/O 密集型任务时。C++11 引入了对多线程的直接支持,使得创建和管理线程变得更为简便。以下是 C++ 中多线程的基本概念和示例代码。

一、std::thread

std::thread 是 C++11 引入的一个标准库类,用于在多线程环境下创建和管理线程。它提供了一个简单的接口来启动并控制多个线程的执行。下面是一些关于 std::thread 的基本介绍:

1. 创建和启动线程

要创建一个线程,你可以直接传递一个可调用对象(如函数指针、Lambda 表达式或者函数对象)给 std::thread 的构造函数,线程会在后台启动执行。

#include <iostream>
#include <thread>

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

int main() {
    std::thread t(printHello);  // 创建并启动线程
    t.join();  // 等待线程完成
    return 0;
}

2. join() 和 detach() 方法

join(): 用于等待线程执行完毕,主线程会阻塞直到该线程结束。如果在主线程中调用 join(),会阻塞并等待该线程完成。
detach(): 将线程与主线程分离,使线程在后台继续执行而不阻塞主线程。需要小心使用,防止线程资源泄漏。

std::thread t(printHello);
t.detach();  // 线程继续后台执行,不会阻塞主线程

3. 传递参数到线程

可以通过传递参数给线程来传递数据。如果传递的是值,则会复制一份副本;如果传递的是引用,则需要使用 std::ref 来标识引用。

void printNumber(int n) {
    std::cout << "Number: " << n << std::endl;
}

int main() {
    int num = 10;
    std::thread t(printNumber, num);  // 传递值
    t.join();
    
    std::thread t2(printNumber, std::ref(num));  // 传递引用
    t2.join();
    
    return 0;
}

4. 线程的生命周期

线程在创建后,直到调用 join() 或 detach() 才会结束。如果线程没有 join() 或 detach(),会导致程序抛出异常。

5. 线程ID 和管理

每个线程都有一个唯一的线程 ID,可以通过 std::this_thread::get_id() 获取当前线程的 ID。还可以使用 std::thread::get_id() 获取其他线程的 ID。

std::thread t(printHello);
std::cout << "Thread ID: " << t.get_id() << std::endl;
t.join();

二、std::mutex

在 C++ 中,std::mutex 是一个互斥量(mutex),用于保护共享数据,避免多个线程同时访问导致的竞态条件。std::mutex 是 C++11 引入的,用于实现线程同步,确保同一时刻只有一个线程可以访问某些资源。

1. std::mutex 的基本用法

std::mutex 提供了两个基本的方法:lock() 和 unlock()。lock() 用于请求互斥锁,如果锁被其他线程占用,当前线程将阻塞,直到锁可用。unlock() 用于释放锁。

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

std::mutex mtx;  // 创建一个互斥量

void printHello(int id) {
    mtx.lock();  // 请求锁
    std::cout << "Hello from thread " << id << std::endl;
    mtx.unlock();  // 释放锁
}

int main() {
    std::thread t1(printHello, 1);
    std::thread t2(printHello, 2);
    
    t1.join();
    t2.join();
    
    return 0;
}

2.lock_guard

std::lock_guard 是 C++ 中用于管理互斥锁(mutex)的一种工具类,它提供了一种基于作用域的自动加锁和解锁方式。它属于 头文件。主要特点:

  • 目的:std::lock_guard 主要用于在作用域内自动锁定和解锁互斥锁,确保在作用域结束时(即 std::lock_guard 对象超出作用域时),互斥锁被自动解锁,即使发生异常也能保证锁的释放。
  • 加锁:当创建一个 std::lock_guard 对象时,它会自动锁定传入的互斥锁。
  • 解锁:当 std::lock_guard 对象超出作用域时,析构函数会自动解锁互斥锁。
#include <iostream>
#include <mutex>

std::mutex mtx;

void printNumbers() {
    std::lock_guard<std::mutex> lock(mtx); // 自动加锁
    std::cout << "Hello from thread!" << std::endl;
    // 自动解锁,在作用域结束时
}

int main() {
    printNumbers();
    return 0;
}

3.unique_lock

std::unique_lock 是 C++11 引入的另一种互斥锁管理类,提供比 std::lock_guard 更灵活的锁管理功能。它也是基于作用域的,但相比于 std::lock_guard,std::unique_lock 允许更多的控制,如手动解锁、延迟加锁、尝试加锁等功能。主要特点:

  • 灵活性:std::unique_lock 可以在构造时不立即加锁,也可以在任何时候手动加锁和解锁。
  • 可解锁:与 std::lock_guard 只能在析构时自动解锁不同,std::unique_lock 允许手动解锁(通过调用 unlock() 方法)。
  • 可重新加锁:std::unique_lock 支持“重新加锁”功能,允许在解锁之后再次加锁同一互斥锁。
  • 可以与条件变量配合使用:std::unique_lock 支持与 std::condition_variable 配合使用,这是 std::lock_guard 不支持的功能。

主要成员函数:

  • lock():显式加锁。
  • unlock():显式解锁。
  • try_lock():尝试加锁,如果加锁失败则返回 false。
  • release():释放当前的锁,但不会解锁,它返回指向原锁的指针。
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;

void printNumbers() {
    std::unique_lock<std::mutex> lock(mtx); // 自动加锁
    std::cout << "Hello from thread!" << std::endl;
    
    // 可以手动解锁并在之后重新加锁
    lock.unlock();
    std::cout << "Mutex unlocked!" << std::endl;
    
    lock.lock(); // 重新加锁
    std::cout << "Mutex re-locked!" << std::endl;
}

int main() {
    std::thread t1(printNumbers);
    t1.join();
    return 0;
}

4.其他锁

std::recursive_mutex:

  • 用途:std::recursive_mutex 是一种递归锁,允许同一个线程多次加锁同一把锁,而不会导致死锁。
  • 特点:一个线程可以多次锁定 recursive_mutex,每次加锁时锁的计数器会增加,只有解锁相同次数后,锁才会被真正释放。
  • 使用场景:当一个线程需要在递归函数中多次加锁时,recursive_mutex 是一个很好的选择。

std::timed_mutex:

  • 用途:std::timed_mutex 是一种可以设置超时时间的互斥锁,尝试加锁时,如果超时则返回失败。
  • 特点:它提供了 try_lock_for() 和 try_lock_until() 方法,用于在特定时间内尝试加锁。
  • 使用场景:适用于需要等待锁一段时间但不希望无限期阻塞的场景。

std::recursive_timed_mutex:

  • 主要特点:递归特性:与 std::recursive_mutex 相似,std::recursive_timed_mutex 允许同一个线程多次加锁同一把锁,而不会导致死锁。每次加锁时,锁的计数器会增加,直到计数器为零时,锁才会被释放。

  • 定时功能:与 std::timed_mutex 相似,它允许在指定的时间内尝试加锁。如果在超时前没有获取到锁,它会返回失败。这避免了线程在无法获取锁时被无限期阻塞。

  • 使用场景:当你需要递归加锁的能力,并且还希望在加锁时能够控制超时,避免长时间阻塞,可以使用 std::recursive_timed_mutex。

三、条件变量condition_variable

std::condition_variable 是 C++11 引入的同步机制,它用于线程间的通信,允许一个线程在某个条件满足之前等待,而另一个线程可以通知它条件已经满足。它通常和 std::mutex 一起使用,用来在多线程程序中实现线程的等待和通知机制。

1. std::condition_variable 的基本概念

std::condition_variable 允许一个线程在某个条件成立时等待,而另一个线程可以通过通知来唤醒等待的线程。它有两个主要操作:

  • 等待:等待一个条件发生,通常与互斥量一起使用。
  • 通知:唤醒等待线程,通常是通过条件变量的 notify_one() 或 notify_all() 来完成。

2. 常用成员函数

  • wait(std::unique_lockstd::mutex& lock): 使调用线程进入等待状态,直到条件满足(通常是某个共享数据的状态)。
  • wait_for(std::unique_lockstd::mutex& lock, std::chrono::duration<Rep, Period> rel_time): 使调用线程等待指定时间,或者直到条件满足。
  • wait_until(std::unique_lockstd::mutex& lock, std::chrono::time_point<Clock, Duration> abs_time): 使调用线程等待直到指定的时间点,或者直到条件满足。
  • notify_one(): 唤醒一个等待该条件的线程。
  • notify_all(): 唤醒所有等待该条件的线程。

这是一个经典的使用条件变量的例子,生产者线程将数据放入共享缓冲区,消费者线程从缓冲区取出数据。当缓冲区为空时,消费者线程需要等待生产者生产数据;当缓冲区满时,生产者线程需要等待消费者消费数据。

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

std::mutex mtx;  // 互斥量
std::condition_variable cv;  // 条件变量
std::queue<int> buffer;  // 缓冲区
const unsigned int maxBufferSize = 5;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [](){ return buffer.size() < maxBufferSize; });  // 等待直到缓冲区有空位
        buffer.push(i);
        std::cout << "Produced: " << i << std::endl;
        cv.notify_all();  // 通知消费者有新数据
    }
}

void consumer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [](){ return !buffer.empty(); });  // 等待直到缓冲区有数据
        int value = buffer.front();
        buffer.pop();
        std::cout << "Consumed: " << value << std::endl;
        cv.notify_all();  // 通知生产者缓冲区有空位
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();
    
    return 0;
}

实例:用C++实现两个线程交替打印一个1-100的奇偶数字。

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

std::mutex mtx;  // 用于保护共享资源
std::condition_variable cv;  // 条件变量
int current = 1;  // 当前要打印的数字

void printOdd() {
    for (int i = 1; i <= 100; i += 2) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待,直到 `current` 是奇数
        cv.wait(lock, []{ return current % 2 != 0; });
        std::cout << current << " ";
        ++current;  // 增加当前数字
        cv.notify_all();  // 通知另一个线程可以打印
    }
}

void printEven() {
    for (int i = 2; i <= 100; i += 2) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待,直到 `current` 是偶数
        cv.wait(lock, []{ return current % 2 == 0; });
        std::cout << current << " ";
        ++current;  // 增加当前数字
        cv.notify_all();  // 通知另一个线程可以打印
    }
}

int main() {
    std::thread t1(printOdd);  // 打印奇数
    std::thread t2(printEven);  // 打印偶数
    
    t1.join();
    t2.join();
    
    return 0;
}