C++ 11新特性之并发

简介: C++ 11新特性之并发

概述

随着计算机硬件的发展,多核处理器已经成为主流,对程序并发执行能力的需求日益增长。C++ 11标准引入了一套全面且强大的并发编程支持库,为开发者提供了一个安全、高效地利用多核CPU资源进行并行计算的新框架,极大地简化了多线程开发。

std::thread

在C++ 11中,std::thread是用于创建和管理线程的核心组件。使用线程的一些要点如下。

1、创建线程。

通过调用std::thread构造函数,传入要在线程中执行的函数(或可调用对象)以及任何必要的参数来创建线程。

2、线程函数。

可以是一个全局函数、类成员函数(此时需要传递指向该类实例的指针或引用),或者是一个满足Callable要求的类型。

3、线程执行。

一旦创建了std::thread对象,线程就会尝试启动执行,但具体何时开始执行由操作系统调度决定。

4、线程同步。

若多个线程共享数据,通常需要使用互斥锁(std::mutex)、条件变量(std::condition_variable)或其他同步机制来避免竞态条件和数据不一致问题。

5、线程生命周期管理。

join()方法会阻塞当前线程,直到被调用join()的线程完成其任务。detach()方法将线程从std::thread对象中分离,使其成为一个守护线程,当主线程退出而未调用join()时,这个分离的线程仍然可以继续运行。但是,如果分离的线程最后仍在运行且没有其他引用,则可能会导致资源泄漏。

std::thread的具体使用,可参考下面的示例代码。

#include <iostream>
#include <thread>
using namespace std;
void ThreadFunc()  
{
    cout << "Sub thread" << endl;
}
int main() {
    thread myThread(ThreadFunc);
    cout << "Main thread init" << endl;
    myThread.join();
    cout << "Main thread exit" << endl;
    return 0;
}


std::mutex

std::mutex是用于实现线程间同步的基础工具,它确保同一时间内只有一个线程能够访问被保护的资源或执行一段代码。为了简化锁的管理并防止死锁,通常建议使用std::lock_guard或std::unique_lock这样的RAII(Resource Acquisition Is Initialization)机制来自动管理锁的生命周期。同时,在复杂的情况下,还可以结合条件变量等工具来实现更为灵活的线程同步逻辑。

在<mutex>头文件中定义了std::mutex类型,它代表一个可重入互斥量。当多个线程试图同时获取已锁定的互斥量时,除了已经持有该互斥量的线程外,其他线程会被阻塞直到互斥量被解锁。std::mutex的主要成员函数如下。

1、构造函数:默认构造一个未锁定的互斥量。

std::mutex mtx;


2、lock():将互斥量锁定,如果互斥量已经被另一个线程锁定,则调用此方法的线程将被阻塞,直至互斥量变为可用。

mtx.lock();


3、unlock():解锁互斥量,允许等待的线程(如果有)获得所有权并继续执行。

mtx.unlock();


4、try_lock():尝试锁定互斥量,但不会阻塞。如果成功获取锁则返回true,否则(即互斥量已被锁定)立即返回false。

if (mtx.try_lock())
{
    // 已经获取到锁
}
else
{
    // 未能获取到锁
}


5、RAII包装器类。为了确保即使在异常情况下也能正确释放互斥锁,C++ 11提供了几个基于RAII(Resource Acquisition Is Initialization)原则的包装器类,比如:std::lock_guard和std::unique_lock。

std::lock_guard:当lock_guard对象创建时自动锁定互斥量,并在其析构时自动解锁互斥量,从而避免忘记解锁导致的死锁问题。

std::lock_guard<std::mutex> lock(mtx);
// 这里是受保护的代码区域
// lock析构时,自动解锁


std::unique_lock:提供了比lock_guard更多的灵活性,比如:手动锁定、解锁以及尝试锁定等。

std::condition_variable

condition_variable是C++标准库中的一个同步原语,它是多线程编程中的一种关键工具,主要用于线程间的通信和同步。它与互斥量(mutex)配合使用,可以实现线程的等待和通知机制。具体来说,std::condition_variable类提供了以下的功能。

1、等待。

当某个条件不满足时,线程可以调用wait()函数释放互斥锁并进入等待状态,直到其他线程对同一个条件变量调用notify_one()或notify_all()函数唤醒它。

2、唤醒。

notify_one():唤醒一个正在等待此条件变量的线程(如果有多个线程在等待,则唤醒其中一个)。

notify_all():唤醒所有正在等待此条件变量的线程。

通常,condition_variable的典型使用场景包括:生产者/消费者模式、 barrier同步等,通过它可以有效地控制线程在满足特定条件时才继续执行,从而避免无效的循环检查或者竞争条件等问题。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
static mutex s_mutex;
static condition_variable s_condition;
static bool s_bReady = false;
void PrintID(int nID)
{
    unique_lock<mutex> lock(s_mutex);
    while (!s_bReady)
    {
        // 当ready为false时,线程会一直等待
        s_condition.wait(lock);
    }
    // 当其他线程修改ready为true,并调用cv.notify_all()后,这里会被唤醒
    cout << "Thread " << nID << " is running." << endl;
}
void Process()
{
    unique_lock<mutex> lock(s_mutex);
    s_bReady = true;
    // 唤醒所有等待的线程
    s_condition.notify_all();
}
int main()
{
    thread pThreads[10];
    for (int i = 0; i < 10; ++i)
    {
        pThreads[i] = thread(PrintID, i + 1);
    }
    // 创建一个线程来修改ready并唤醒其他线程
    thread threadOther(ref(Process));
    for (auto& t : pThreads)
    {
        t.join();
    }
    threadOther.join();
    return 0;
}




在上面的示例代码中,我们创建了10个线程,它们都在等待一个条件:s_bReady变为true。当Process线程将s_bReady设为true并调用s_condition.notify_all()后,所有等待的线程都会被唤醒,并打印自己的ID。

std::atomic

std::atomic是C++ 11引入的标准库中的一个模板类,它提供了一种能够在多线程环境中进行原子操作的类型安全方式。原子操作意味着:即使在没有互斥量或其他同步机制的情况下,该操作也能够从多个线程中以不可分割的方式执行,即不会出现半个操作的现象,确保了数据一致性。

使用std::atomic可以有效地处理简单的同步需求,比如:无锁计数器、标志位等,并且相比传统的互斥锁而言,其开销通常更小,性能更高。

#include <atomic>
#include <thread>
#include <cassert>
using namespace std;
static atomic<int> s_nCounter(0);
void Increment()
{
    ++s_nCounter;
}
int main()
{
    thread t1(Increment);
    thread t2(Increment);
    t1.join();
    t2.join();
    // 这个断言总是成立,因为s_nCounter的递增是原子的
    assert(s_nCounter == 2);
    return 0;
}


std::atomic支持多种类型的对象,包括但不限于:基本内置类型、指针以及用户自定义类型(如果满足特定条件)。它提供了load、store、exchange、compare_exchange_strong/weak等一系列原子操作方法,用于读写和更新其内部封装的数据成员。

std::future和std::async

std::future 和 std::async是C++ 11标准引入的异步编程工具,它们位于<future>头文件中,用于简化并发任务的管理和结果的获取。

std::future是一个模板类,它代表了一个可以在未来某个时间点获取的结果。当你启动一个异步计算时,该计算的结果可以通过std::future对象来访问。std::future 提供了以下功能。

获取结果:调用std::future::get()会阻塞当前线程,直到异步计算完成并返回结果。

检查是否已准备好:可以检查future是否已经包含有效结果或异常。

取消异步操作:虽然不能直接取消异步操作,但可以关联一个可取消的共享状态,然后取消那个状态。

获取异常:如果异步计算过程中抛出了异常,则可以在future上捕获到这个异常。

std::async是一个函数模板,它用来异步执行一个函数,并返回一个表示其结果的std::future对象。

#include <iostream>
#include <future>
#include <chrono>
using namespace std;
int LongTimeCompute(int nNumber)
{
    // 模拟耗时操作
    this_thread::sleep_for(chrono::seconds(5));
    return nNumber * nNumber;
}
int main()
{
    // 使用async启动异步任务
    auto future_result = async(launch::async, LongTimeCompute, 10);
    cout << "Main thread running..." << endl;
    // 当需要结果时,调用get()
    int nResult = future_result.get();
    cout << "Result of async computing: " << nResult << endl;
    return 0;
}



在上面的示例代码中,我们定义了一个模拟耗时计算的函数LongTimeCompute。然后,在main函数中,我们通过std::async创建了一个异步任务,并指定其策略为std::launch::async,确保在新的线程上运行该函数。

主线程在等待异步任务完成的同时可以继续执行其他任务,当主线程需要得到异步任务的结果时,它调用了 future_result.get(),这将阻塞直到异步计算完成并将结果返回给主线程。最后,主线程输出了异步计算得到的结果。

总结

C++ 11提供的并发特性不仅简化了多线程编程的复杂性,而且增强了程序的安全性和可靠性。通过合理利用这些工具和技术,我们能够更好地设计和实现适应现代多核架构的应用程序,从而提升软件的整体性能表现。在实践中,还需注意避免死锁、竞态条件等并发问题,并结合实际情况选择适当的并发策略。正确理解和熟练运用C++ 11的并发库,是构建高效、稳定且可扩展应用程序的关键。


相关文章
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
113 59
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
37 0
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(二)
【C++】面向对象编程的三大特性:深入解析继承机制
|
2月前
|
安全 程序员 编译器
【C++】面向对象编程的三大特性:深入解析继承机制(一)
【C++】面向对象编程的三大特性:深入解析继承机制
|
2月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值