首页 > 编程笔记 > C++笔记 阅读:1

C++中的多线程编程(附带实例)

C++11 之前,如果要进行多线程开发,则需要调用系统或者第三方提供的 API,例如在 Linux 系统下需要调用 pthread 的一组 API,在 Windows 系统下则需要调用 CreateThread() 的一组 API。这种操作导致代码需要在不同的操作系统上各个独立写一遍,造成代码量增加,从而使模块的通用性降低。

从 C++11 开始引入了对多线程编程的支持,包括头文件和相关的类和函数,使在 C++ 中使用多线程变得更加简便和安全。

C++11 中的多线程支持是由多个模块提供的,C++11 多线程的一些主要模块如下:

模块 描 述
线程类 引入了 std::thread 类,可以用于创建和管理线程。通过 std::thread 类可以创建新的线程,并指定线程要执行的函数或函数对象,还可以将参数传递给线程函数。std::thread 类提供了一些控制线程的方法,例如 join() 方法可以等待一个线程执行完成,detach() 方法可以将一个线程分离,使线程的执行与主线程独立。
异步类 引入了 std::future 和 std::async 两个异步处理类。提供了简单的接口用来处理异步任务和异步任务的返回值。
并发原语 提供了一些原子操作和锁机制,以便实现线程之间的同步和互斥,其中 std::atomic 类型用于原子操作,std::mutex、std::lock_guard 等类可以用于实现互斥锁。
条件变量 引入了 std::condition_variable 类,以便实现多线程之间的通信和同步。条件变量可以结合互斥锁一起使用,实现线程的等待和唤醒。
并行算法 引入了一些支持并行执行的算法,例如 std::for_each、std::transform 等。这些算法可以将一个任务并行化执行,从而提高程序的性能。

使用多线程可以更加有效地利用硬件的计算资源,在很多情况下多线程开发是不可或缺的,在一些情况下不使用多线程则无法满足实际的需要,但是多线程编程开发的难度和单线程开发相比会明显增加。例如需要注意线程的同步和互斥,以避免出现竞态条件和数据访问冲突等问题。

在 C++11 中提供了一组工具和机制,从而能够简化多线程编程,并使多线程程序更加易于理解和维护。尽管如此,在实际开发中仍然需要特别小心地处理好多线程数据的竞争关系,以避免数据竞争造成的计算错误和死锁。

C++ std::thread

std::thread 是 C++11 提供的多线程库中的一个类,用于创建和管理线程。

下面是一个示例,展示了如何使用 std::thread 创建两个线程并执行并行任务,代码如下:
#include <iostream>
#include <thread>
// 线程函数 1
void threadFunc1()
{
    std::cout << "Thread 1 running" << std::endl;
    // 执行一些任务...
}
// 线程函数 2
void threadFunc2(int numIterations)
{
    std::cout << "Thread 2 running" << std::endl;
    for (int i = 0; i < numIterations; ++i) {
       // 执行一些任务...
    }
}
int main()
{
    // 创建线程 1,并执行 threadFunc1 函数
    std::thread t1(threadFunc1);
    // 创建线程 2,并执行 threadFunc2 函数,传递参数 10 作为 numIterations
    std::thread t2(threadFunc2, 10);
    // 等待线程 1 和线程 2 执行完成
    t1.join();
    t2.join();
    std::cout << "Main thread exiting" << std::endl;
    return 0;
}
在上面的示例中首先定义了两个线程函数 threadFunc1() 和 threadFunc2(),它们分别用于线程 1 和线程 2 的执行逻辑,然后在 main() 函数中使用 std::thread 类创建了两个线程 t1 和 t2,分别指定了要执行的函数和需要传递的参数。最后,通过调用 join() 方法,主线程会等待线程 1 和线程 2 执行完成后再退出。

运行上述程序,输出的结果如下:

Thread 1 running
Thread 2 running
Main thread exiting

这表示线程 1 和线程 2 被创建并执行了对应的线程函数,并且主线程等待它们完成后才退出。

C++ std::future和std::async

std::future 是 C++11 提供的一个类模板,用于访问异步任务的结果或异常。它可以用于获取异步任务的返回值,或者通过 wait() 方法等待其完成。

下面示例展示了如何使用 std::future 获取异步任务的返回值,代码如下:
#include <iostream>
#include <future>    // 异步任务函数
int asyncFunc()
{
    // 执行一些耗时的任务...
    return 42;
}
int main()
{
    // 创建一个异步任务并执行 asyncFunc 函数
    std::future<int> fut = std::async(std::launch::async, asyncFunc);
    // 执行其他的操作...
    // 等待异步任务执行完成,并获取结果
    fut.wait();
    int result = fut.get();
    std::cout << "Async task completed with result: " << result << std::endl;
    return 0;
}
在上面的示例中首先定义了一个异步任务函数 asyncFunc(),它会执行一些耗时的任务并返回一个整数值。在 main() 函数中使用 std::async() 函数创建了一个异步任务,并传递了要执行的函数 asyncFunc()。std::launch::async 参数表示这个异步任务应该在一个新线程中执行。

提交任务后主线程可以执行其他操作,而不需要等待异步任务完成,但是在想要获取异步任务的返回值之前需要调用 std::future 的 wait() 方法等待异步任务完成。

最后,通过调用 std::future 的 get() 方法等待异步任务执行完成并获取它的返回值。运行上述程序,结果如下:

Async task completed with result:42

C++ std::mutex

当多个线程同时访问和修改共享数据时,可能会导致数据竞争和不确定的行为。为了确保线程之间的数据同步和避免竞态条件,可以使用互斥量(Mutex)和条件变量(Condition Variable)。

互斥量是一种同步原语,用于保护共享数据,只允许一个线程在任意时刻访问共享数据。互斥量通过 lock() 和 unlock() 方法实现加锁和解锁操作。

下面是一个示例,展示了如何在 std::thread 中使用互斥量进行数据同步,代码如下:
#include <iostream>
#include <thread>
#include <mutex> // 互斥量,用于保护共享数据
std::mutex mtx; // 共享数据
int counter = 0;
void increment()
{
    std::lock_guard<std::mutex> lock(mtx); // 加锁
    // 修改共享数据
    counter++;
    // 执行其他操作...
} // 解锁时,std::lock_guard的析构函数会自动调用 unlock()

int main()
{
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter value: " << counter << std::endl;
    return 0;
}
在上面的示例中定义了一个全局变量 counter 作为共享数据。在 increment() 函数中使用 std::lock_guard 来管理互斥量的锁定和解锁。锁定互斥量后可以安全地修改共享数据,确保在任意时刻只有一个线程可以访问共享数据。

在主函数中创建了两个线程 t1 和 t2,并分别调用 increment() 函数。最后等待两个线程执行完成,并打印 counter 的值。运行上述程序,结果如下:

Counter value:2

这表明两个线程安全地增加了 counter 的值,最终得到了正确的结果。

C++ std::condition_variable

std::condition_variable 用于线程之间的等待和通知。它可以让一个线程等待,直到满足某个条件然后由另一个线程通知它继续执行。

std::condition_variable 需要配合 std::mutex 一起使用以确保线程安全。当线程等待某个条件时,它会释放已经持有的锁,进入等待状态。当另一个线程满足了条件并进行通知时,等待的线程被唤醒,重新获取锁,并继续执行。

下面的示例演示了如何使用这个模块进行线程同步控制,代码如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;            // 互斥量,用于保护共享数据
std::condition_variable cv; // 条件变量
bool is_consume_ready = false; // 消费者准备好的标志

int data = 0; // 共享数据

void producer()
{
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟生产过程
    std::lock_guard<std::mutex> lock(mtx); // 加锁
    // 更新共享数据
    data = 42;
    is_consume_ready = true;
    cv.notify_one(); // 通知等待的消费者线程
}

void consumer()
{
    std::unique_lock<std::mutex> lock(mtx); // 加锁
    cv.wait(lock, []{ return is_consume_ready; }); // 等待条件满足
    // 执行消费操作
    std::cout << "Consumed data: " << data << std::endl;
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}
在上面的示例中定义了一个生产者线程和一个消费者线程。生产者线程会在一段时间后生成数据,并将 is_consume_ready 设置为 true,表示消费者准备好了,然后生产者线程通过 cv.notify_one() 通知等待的消费者线程。

消费者线程首先会加锁,并调用 cv.wait() 以等待条件满足。如果条件不满足,则消费者线程会释放锁,并进入等待状态。一旦生产者线程通知条件满足,消费者线程会重新获取锁,并继续执行。

在本例中运行结果如下:

Consumed data:42

这表明消费者线程成功地获取了生产者线程生成的数据。通过使用 std::condition_variable 实现了线程之间的同步和等待/通知机制,从而确保数据正确和安全访问。

相关文章