C++之多线程(二)

简介: C++进阶之多线程下

导读

做开发的人都知道多线程是一个很复杂的问题,一不下心就会出现莫名其妙的八哥,有句话调侃说:

一个程序员碰到了一个问题,他决定用多线程来解决。现在他有了两个问题。。。

在前面《C++之多线程(一)》 一文中,我们介绍了C++11中多线程的一些基本使用以及给线程传递参数时的一些注意事项。
今天我们继续了解下C++11中多线程一些比较现代化的用法,以及一些线程同步的方法。

async、future

设想一下现在我们有这样的一个需求,我们需要开启一个线程去某件事情,待这件事情执行完毕后将结果返回,那该如何实现呢?
当然我们可以可以通过全局变量或者给线程传参的方式来实现这个需求,但是否有更简单灵活的方式呢?

furure的中文意思是将来,它表示在未来的等待线程执行完毕后可以获取线程返回的结果值。

async是一个函数模版,通过它可以启动一个异步任务,并且返回一个future,通过这个future可以获取到异步任务的执行结果。

以下是一个简单的例子:

#include <future>
int request(){
    std::cout << "request线程:" << std::this_thread::get_id() << std::endl;
    this_thread::sleep_for(std::chrono::seconds(3)); // 休眠3s
    return 20;
}

int main() {
    std::future<int> future = async(request);
    int result = future.get(); // 阻塞,并且只能get一次
    std::cout << "当前线程:" << std::this_thread::get_id() << std::endl;
    std::cout << "结果的值:" << result << std::endl;
    return 0;
}

正如上述代码注释所写的那样,std::future仅仅允许get一次结果值,如果希望多次获取结果值怎么办?使用shared_future代替std::future即可。

不同于thread一创建就马上执行线程函数,async是可以通过launch参数控制什么时候执行的,单单是这个就比thread`灵活了。

launch参数有三种可选的值,分别是async=1deferred=2any = async | deferred

1、async 是默认行为,表示马上创新新线程执行
2、deferred 延迟执行,需要等待future的get或await才自行线程任务,不一定会会创建新线程
3、any 是前面两个的结合体,一定会创建新线程,但是也是需要等待future的get或await才自行线程任务

其他的比如给async传递参数时应该用什么传递呢?值传递、指针传递、引用传递怎么选呢?童鞋们可以按照前面的文章《C++之多线程(一)》 自行编码验证。

互斥量与条件变量

1、互斥量

互斥量与条件变量相信大家都不陌生了,一扯到线程同步,线程死锁啥的必然逃不开这两家伙...

std::mutex是C++11引入的关于互斥量最基本的类,同时还附带了一系列RAII语法的模板类,例如std::lock_guard。RAII 在不失代码简 洁性的同时,很好地保证了代码的异常安全性。
为了保证代码安全,避免忘记释放而导致的死锁问题等,我们一般不直接使用std::mutexlock函数,而是使用unique_lock或者std::lock_guard等。

首先说说std::lock_guard,它是内部同时帮我们做了std::mutexlockunlock操作,也就是说使用了std::lock_guard就完全释放了,不用再手动地去调用lockunlock了。

以下是一个使用std::lock_guard保证线程安全的例子:

int main() {
    std::mutex mutex;
    int a = 0;
    thread thread1([&](){
        for (int i = 0; i < 100000; ++i) {
            std::lock_guard<std::mutex> lockGuard(mutex);
            a++;
        }
    });
    thread thread2([&](){
        for (int i = 0; i < 100000; ++i) {
            std::lock_guard<std::mutex> lockGuard(mutex);
            a++;
        }
    });
    thread1.join();
    thread2.join();
    std::cout << "a的值:" << a << std::endl;
    return 0;
}

通过使用互斥量,我们保证了变量a的值每次只允许一个线程进行修改,所以保证了运作完毕之后a的值就是我们所期待的结果。

std::unique_lock的功能和std::lock_guard功能类似,但是比std::lock_guard更为灵活,可配性更高。

2、条件变量

条件变量就类似于一个跑腿的通信工,当条件满足的时候它会通知某线程继续执行任务,当条件不满足的时候它就通知某线程进入等待休息。

C++11中提供了std::condition_variable作为条件变量。下面我们使用std::condition_variable来模拟一个典型的消费者生产者的线程同步的例子:

int main() {
    std::mutex mutex;
    std::condition_variable conditionVariable;
    std::vector<int> task_queue;
    // 生产者线程
    thread product_thread([&](){
        while (true){
            std::this_thread::sleep_for(std::chrono::milliseconds (600));
            task_queue.emplace_back(1);
            std::cout << "生产成功一个产品" << std::endl;
            // 通知唤醒
            conditionVariable.notify_all();
        }
    });
    // 消费者线程
    thread consume_thread([&](){
        while (true){
            std::unique_lock<std::mutex> uniqueLock(mutex);
            // 防止虚假唤醒
            if(task_queue.empty()){
                std::cout << "等待生产者生产" << std::endl;
                conditionVariable.wait(uniqueLock);
            } else{
                std::cout << "取出消费" << std::endl;
                task_queue.erase(task_queue.cbegin());// 移除第0个
                std::this_thread::sleep_for(std::chrono::milliseconds (500));
            }
            uniqueLock.unlock();
        }
    });
    product_thread.join();
    consume_thread.join();
    return 0;
}

上面的注释有一个虚假唤醒的判断,所谓的虚假唤醒意思就是:当一个正在等待条件变量的线程由于条件变量被触发而唤醒时,却发现它等待的条件没有满足就被唤醒了,比如在条件其实没有满足时却调用了notify_all去通知。

原子操作

上面我们讲述了互斥量与条件变量的相关操作,虽然它们搭配使用做到了线程安全,但是仔细想想,有时候我们仅仅是需要在不同的线程读写一个简单的数据而已,也需要动用到互斥量与条件变量吗?


杀鸡焉用牛刀,这不是平A骗大招吗?

大家平时在了解多线程概念的时候都知道,即使是一条简单的赋值语言,相对于CPU而言,也是需要拆分成多个步骤才能完成的,而CPU一般又是抢占式的,那么在这个拆分到修改完成的这个过程中如果原有变量的再次被修改了那么矛盾就产生了,
如果需要避免这种矛盾的产生就需要变量的修改应该是原子操作。

在C++11中引入了std::atomic来代表原子操作,我们通过以下程序来看看原子操作的魅力:

int main() {
    int a = 0;
    thread thread1([&](){
        for (int i = 0; i < 100000; ++i) {
            a++;
        }
    });
    thread thread2([&](){
        for (int i = 0; i < 100000; ++i) {
            a++;
        }
    });
    std::atomic<int> b{0};
    thread thread3([&](){
        for (int i = 0; i < 100000; ++i) {
            b++;
        }
    });
    thread thread4([&](){
        for (int i = 0; i < 100000; ++i) {
            b++;
        }
    });
    thread1.join();
    thread2.join();
    thread3.join();
    thread4.join();
    std::cout << "a的值:" << a << std::endl;
    std::cout << "b的值:" << b << std::endl;
    return 0;
}

在上面的程序中,变量a和变量b都使用了多线程进行修改值,但是最后的结果只有变量b的值被正确地修改为200000,而a的随着每次运行都是不确定,不准确的,因为对变量a修改时不是原子操作。

注意,虽然atomic表示原子操作,但是也并不意味着对atomic变量进行的所有表达式操作都是原子,在使用atomic的过程最好使用atomic内的函数进行操作,不要像笔者的例子那样直接进行加加渐渐,比如想要增加就调用fetch_add函数等。

推荐阅读

C++之指针扫盲
C++之智能指针
C++之指针与引用
C++之右值引用
C++之多线程一

目录
相关文章
|
2月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
119 7
|
2月前
|
消息中间件 存储 安全
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
71 1
|
3月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
3月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
126 6
|
3月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
99 5
|
3月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
53 0
|
3月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
47 0
Linux C/C++之线程基础
|
5月前
|
Java 调度
基于C++11的线程池
基于C++11的线程池
|
5月前
|
Dart 编译器 API
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决