前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >再也不用std::thread编写多线程了

再也不用std::thread编写多线程了

作者头像
用户9831583
发布2022-12-04 16:12:28
2.3K0
发布2022-12-04 16:12:28
举报
文章被收录于专栏:码出名企路码出名企路

Part1第7章 并发API

1条款35:优先选用基于任务而非基于线程的程序设计

/**

* @brief

* 标准库中的并发元素:任务,期望,线程,互斥量,条件变量和原子对象,为期望提供了两个模板:std::future和std::shared_future

*

*基于任务的:std::async

基于线程的:std::thread

*/

代码语言:javascript
复制
int sys = 1;

int doAsyncWork()
{
    sys ++;
    
    std::cout<<"sys: "<<sys<<std::endl;

    return sys;
}


int main()
{
    //方式一:基于线程
    std::thread t(doAsyncWork);
    t.join();
    //t.detach();//子线程和主线程分离,主线程不再干预子线程的运行,无法获取子线程的执行结果

    //方式二:基于任务
    auto fut = std::async(doAsyncWork);
    std::cout<<"fut: "
    //有返回值,自然就可以用返回值判断执行结果
    /**
     * @brief 
     *好处是什么呢?
      如果申请的软件线程数量多于系统可以提供的,调用std::thread会抛出异常,然而调用std::async时
      系统不保证会创建一个新的软件线程,相反,它允许调度器把指定函数(doAsyncWork)运行在请求doAsyncWork
      结果的线程中,例如,对fut调用了get或者wait的线程,如果系统发生了超订或线程耗尽,合理的调度器就可以
      利用这个自由度
     * 
     */

}

2条款36:如果异步是必要的,则指定std::launch::async

//调用std::async的启动策略,并不一定是异步的,做到以下两点才是

/**

* @brief

*

* 1,std::launch::async启动策略意味着函数f必须以异步方式运行,在另一个线程上执行

*

* 2,std::launch::deferred启动策略意味着函数f只会在 std::async所返回的期望值的get

* 或wait得到调用时才运行,也就是,执行会推迟到其中一个调用发生的时刻。当调用get或wait时

* ,f会同步运行,也就是,调用方会阻塞至 f运行结束为止。如果 get或wait都没有得到调用,f是不会运行的

*

* 3,如果你不积极指定一个,std::async采用的并非以上两者中的一个,相反地,它采用的是对二者进行或运算的结果

*

* @return int

*/

代码语言:javascript
复制
//两个效果一样的
auto fut1 = std::async(doAsyncWork);//采用默认启动策略运行doAsyncWork
//采用或者异步或者推迟的方式运行doAsyncWork
auto fut2 = std::async(std::launch::async |
                         std::launch::deferred, doAsyncWork);

//如果在给定线程 t上执行 以上语句,会发生以下三种可能
/**
 * @brief 
 *     
 *  doAsyncWork 认为是 f
 * 1, 无法预知 f是否会和 t并发运行,因为 f 可能会被调度为推迟运行
 * 
 * 2,无法预知f是否运行在与调用 fut的get或wait函数的线程不同的某线程之上。如果那个线程是t,那就是说无法预知f是否运行
 * 在与t不同的某线程之上
 * 
 * 3,连f是否允许这件起码的事情都是无法预知的,这个因为无法保证在程序的每条路径上,fut的get或wait都会得到调用
 * 
 * @return int 
 */

//3

//std::async默认启动策略在使用 thread_local变量时,无法预知会取到的是哪个线程的局部存储

代码语言:javascript
复制
using namespace std::literals;
void f()//f睡眠1s后返回
{
    std::this_thread::sleep_for(1s);
    sys ++;
    std::cout<<"f(): "<<sys<<std::endl;
}

//测试3
//真正以异步方式启动f
auto fut3 = std::async(std::launch::async,f);
if (fut3.wait_for(0s) == std::future_status::deferred)
{
    //如果任务被推迟了, 则使用fut的wait或get以异步方式调用f
}
else
{
    //任务没被推迟
    while (fut3.wait_for(100ms) != std::future_status::ready)
    {
        //任务没被推迟,也未就绪,则做并发工作,直到任务就绪
        sys ++;
        std::cout<<"ready: "<<sys<<std::endl;
    }

}

//4

//自动使用 std::launch::async作为启动策略的函数实现

代码语言:javascript
复制
//C++11
template<typename F, typename... Ts>
inline std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync11(F&& f, Ts&&... params)
{
    //返回异步所需的期望值
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}
//C++14
template<typename F, typename... Ts>
inline auto
reallyAsync14(F&& f, Ts&&... params)
{
    //返回异步所需的期望值
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}

//测试4
auto fut4 = reallyAsync11(f);

// • std:: async 的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行

// • 如此的弹性会导致使用 thread_local 变量时的不确定性,隐含着任务可能永远不会执行,还会影响运用了基于超时的 wait 调用的程序逻辑

// • 如果异步是必要的,则指定 std: :launch: :async

3条款37:使 std::thread 型别对象在所有路径皆不可联结

/**

* @brief

*

* std::thread型别对象处于两种状态之一:可联结或不可联结

*

* 可联结状态:底层线程若处于阻塞或等待调度,或已运行结束

* 不可联结状态:上面反之

*

* std::thread可联结性重要原因:如果可联结的线程对象的析构函数被调用,则程序的执行就终止了

*

* 设计一个函数 doWork,接收一个筛选器函数filter和一个最大值maxVal作为形参

* doWork会校验它做计算的条件全部成立,之后会针对筛选器选出的0到maxVal之间的值进行计算

*

* 需要设置实施筛选的那个线程的优先级别,要去使用线程的低级句柄,只能用基于线程的std::thread来做,基于任务的std::asyc没有这个功能

*/

代码语言:javascript
复制
constexpr auto tenMillion = 10'000'000;

bool conditionAreSatisfied()
{
    return true;
    /**
     * @brief 
     * 如何在doWork中返回了false或者抛出了异常,那么在doWork的末尾调用std::thread型别对象t的析构函数时
     * 它会处于可联结合=状态,从而导致程序执行终止
     * 
     */
}

void performComputation( std::vector<int> &goodVals)
{
    for(auto i:goodVals)
        std::cout<<"i: "<<i<<std::endl;
}
bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion)
{
    std::vector<int> goodVals;
    std::thread t([&filter, maxVal, &goodVals]
    {
        for(auto i=0; i <= maxVal; ++i)
        {
            if(filter(i))
            {
                goodVals.push_back(i);
            }
        }
    });

    //使用t的低值句柄设定t的优先级别
    auto nh = t.native_handle();

    //让t执行结束
    if(conditionAreSatisfied())
    {
        t.join();
        //计算实施
        performComputation(goodVals);
        return true;
    }
    //计算没有实施
    return false;
}

//2

/**

* @brief

*

* //因此,如果你使用了std::thread型别对象,就得确保从它定义得作用域出去得任何路径,使它成为不可联结状态

//但是,覆盖所有路径是复杂得,包括return,continue,break,goto或者异常跳出作用域

//想在每个出向得路径上都执行某动作,最常用得方法就是在局部对象得析构函数中执行该动作,这样得对象成为 RAII对象

RAIIl类:关键在于析构

1,STL容器:各个容器得析构函数都会析构容器内容并释放其内存

2,标准智能指针,std::unique_ptr得析构函数会对它指涉得对象调用删除器, sdt::shared_ptr和std::weak_ptr得析构函数会对引用计数实施自减

std::fstream型别对象其析构函数会关闭对应得文件

但是std::thread型别对象没有对应得RAII类,那就自己实现了一个了,销毁时是调用join还是detach

*/

代码语言:javascript
复制
class ThreadRAII{
    public:
        enum class DtorAction{
            join,
            detach
        };
        //在析构函数中在t上采取行动a
        ThreadRAII(std::thread&& t, DtorAction a): action(a),t(std::move(t)){
        //1,std::thrrad型别对象是不可复制得,只接受右值型别
        }
        //可联结
        ~ThreadRAII()
        {
            if(t.joinable())
            {
                if(action == DtorAction::join)
                {
                    t.join();
                }
                else
                {
                    t.detach();
                }
            }
        }
        
        ThreadRAII(ThreadRAII&&) = default;
        ThreadRAII& operator=(ThreadRAII&&) = default;

        std::thread& get(){
            return t;
        }

    private:
        DtorAction action;
        std::thread t;//在成员列表得最后声明std::thread型别对象
};

bool doWork2(std::function<bool(int)> filter, int maxVal = tenMillion)
{
    std::vector<int> goodVals;
    ThreadRAII t(std::thread([&filter, maxVal, &goodVals]{
        for(auto i=0; i <= maxVal; ++i)
        {
            if(filter(i))
            {
                goodVals.push_back(i);
            }
        }
    }),
    ThreadRAII::DtorAction::join
    );

    //使用t的低值句柄设定t的优先级别
    auto nh = t.get().native_handle();

    //让t执行结束
    if(conditionAreSatisfied())
    {
        t.get().join();
        //计算实施
        performComputation(goodVals);
        return true;
    }
    //计算没有实施
    return false;
}


bool filter(int i)
{
    if(i < 3)
        return true;
    return false;
}
代码语言:javascript
复制
int main()
{
    //测试1
    doWork(filter);

    //测试2
    doWork2(filter);
}

4条款38:对变化多端的线程句柄析构函数行为保持关注

代码语言:javascript
复制
//讨论这样一种情况:期望值位于信道的一端,被调方把结果通过该信道传输给调用方
//被调方,通常以异步方式运行把其计算所得的结果写入信道,通常经由一个std::promise型别对象,而调用方则使用一个期望值来读取该结果

        期望值                                        std::promise
调用方<------------------------------------------------------------ 被调方

//被调方的结果要存储在哪里呢?
/**
 * @brief 
 * 1,在调用方唤起对期望值的 get 之前,被调方可能已经执行完毕,因此结果不会存储在被调方的 std::promise型别对象里
 * 因为那个对象,对于被调方来说是个局部量,在被调方结束后会实施析构
 * 
 * 2,该结果也不能存储在调用方的期望值中,因为可能会从 std::future型别对象出发创建 std::shared_future型别对象,
 * 因此把被调方结果的所有权从 std::future型别对象转移到 std::shared_future型别对象,而后者可能会在原始的 std::futrue析构之后复制多次
 * 
 */

//因此,被调用的结果存储在一个叫 共享状态 的地方

        期望值                   共享状态                               std::promise
调用方<-------------------------被调方结果----------------------------------- 被调方

//期望值的析构行为是由与其关联的共享状态决定的
/**
 * @brief 
 * 1,指涉由  std::aysnc 启动的未推迟任务的共享状态的最后一个期望值会保持阻塞,直到该任务结束。
 * 本质上,这样一个期望值的析构函数是对底层异步执行任务的线程实施了一次隐式 join
 * 
 * 2,其他所有期望值对象的析构函数只仅仅将期望值对象析构就结束了。 对于底层异步运行的任务,类似
 * 对线程实施了一次隐式 detach。对于那些被推迟任务而言,如果这一期望值是最后一个,也就意味着被推迟的任务
 * 将不会有机会运行了
 * 
 */

//常规行为析构函数
//仅仅会析构期望对象,她不会针对任何东西实施 join,也不会从任何东西实施 detach,也不会对运行任何东西,仅仅会析构期望的成员变量

//非常规行为析构函数
//行为的具体表现为阻塞直到异步运行的任务结束,从效果上看,这相当于针对正在运行的 std::async所创建的任务的线程实施了一次隐式 join
//必须满足以下三点,才发挥非常规行为析构函数的作用
/**
 * @brief 
 * 1,期望所指涉的共享状态是由于调用了 std::async 才创建的
 * 
 * 2,该任务的启动策略是 std::launch::async,这既可能是运行时系统的选择,也可能是在调用std::async时指定的
 * 
 * 3,该期望是指涉到该共享状态的最后一个期望,对于 std::future型别对象,这一点总成立。但是 对于 std::shared_futrue型别对象而言,
 * 在析构时如果不是最后一个指涉到共享状态的期望,则它会遵守常规行为准测,仅仅析构其成员变量
 * 
 */

//问题1
//期望的API没有提供任何办法判断其指涉的共享状态是否诞生于 std::async 的调用,所以给定任意期望对象的前提下,它不可能知道自己是否会在析构
//函数中阻塞到异步任务执行结束

//该容器的析构函数可能会在其析构函数中阻塞,因为它所持有的期望中可能会有一个或多个指涉到经由 std::async启动未推迟任务所产生的共享状态
std::vector<std::future<void>> futs;
class Widget{ //Widget型别对象可能会在其析构函数中阻塞,除非有办法分析程序逻辑判定给定的期望值不满足触发非常规析构行为条件
    public:
        ...
    private:
        std::shared_future<double> fut;
}

//解决问题
//std::packaged_task型别对象会准备一个函数或其他可调用的对象,以供异步执行,把结果放入共享状态
{
    //待运行函数
    int calcValue();
    //给calcValue加上包装使之能以异步方式运行
    std::packaged_task<int()> pt(calcValue);//https://www.cnblogs.com/haippy/p/3279565.html
    //取得pt的期望
    auto fut = pt.get_future();//此时我们知道期望对象 fut没有指涉到由 std::async调用产生的共享状态,所以它的析构函数将表现为常规行为
    //但是 std::packsgaed_task不能复制,将pt传递给 std::thread的构造函数一定要将它强制转型到右值
    std::thread t(std::move(pt));

    ......

    /**
     * @brief 
     * 这个地方 t 的命运如下
     * 
     * 1,未对 t实施任何操作,其作用域结束点是可联结的,而这将导致程序终止
     * 
     * 2,针对 t实施了 join,在此情况下 fut无需再析构函数中阻塞,因为在调用的代码已经有过join
     * 
     * 3,针对 t 实施了 detach
     * 
     * 换句话说,当你的期望值所对应的共享状态是 由 std::packaged_task产生的,则通常不用采用特别析构策略
     * 
     */
} //代码块结束

5条款39:考虑针对一次性事件通信使用以void为模板型别实参得期望值

/** * @brief * 需求:提供一个任务通知另一个以异步方式运行得任务发生了特定得事件得能力 * */

//方法一

//https://blog.csdn.net/qq_34999565/article/details/120876201

* 方法一:条件变量 * 检测条件得任务称为检测任务,把对条件作出反应得任务称为反应任务:反应任务等待着条件变量, * 而检测任务则在事件发生时通知条件变量

代码语言:javascript
复制
std::condition_variable cv;//事件得条件变量
 std::mutex m;//在运用cv时给它加得互斥量

//检测任务得代码
{
    /**
     * @brief 
     * 检测事件
     */

    cv.notify_one();//通知反应任务 一个
    //cv.notify_all();//通知反应任务,多个
}

//反应任务得代码
//准备反应
{
    //临界区域开始
    std::unique_lock<std::mutex> lk(m);//为互斥量加锁
    cv.wait(lk);//等待通知到来

    /**
     * @brief 
     * 这对事件作出反应得操作
     * 
     */

    //临界区域结束,通过lk得析构函数为m解锁
}
//继续等待反应,m已经解锁

/** * @brief * * 方法一得缺点:

* 1, * 需要使用互斥体,互斥体是用来控制共享数据访问得,但是检测和反应任务之间大有可能根本不需要这种介质 * 例如:检测任务负责初始化一个全局数据结构,然后把它转交给反应任务使用。如果检测任务在初始化之后从不访问该数据结构 * 并且在检测任务指示它已就绪之前,反应任务从不访问它,但是根据以上程序逻辑,这两个任务互相阻止对方访问。 * * 2, * 如果检测任务在反应任务调用wait之前就通知了条件变量,则反应任务将失去响应;因为为了实现通知条件变量唤醒 * 另一个任务,该任务必须已在等到该条件变量。 * * 3, * 反应任务得wait语句无法应对虚假唤醒:即使没有通知条件变量,针对该条件变量等待得代码也可能被唤醒。避免这一问题: * 通过确认等待得条件确实已经发生,并将其作为唤醒后得首个动作来处理这种情况 * * cv.wait(lk, []{ return 事件是否已经发生; }) * * 但是,反应线程可能无法确认它正在等待得事件是否已经发生,这也是为什么它等待得是个条件变量 * * @return int */

//方法二

//https://blog.csdn.net/stone_overlooking/article/details/78652337

/** * @brief * 使用共享得布尔标志,该标志位得初始值是 false,当检测线程识别出它正在查找得事件时,会设置该标志位 * */

代码语言:javascript
复制
std::atomic<bool> flag(false);//共享得布尔标志位
{
    //检测线程 通知反应任务
    flag = true;
}

{
    //反应线程会轮询标志位
    while (!flag)
    {
        /* code */
    }
}

/** * @brief * 方法二缺点: * * 1, * 该方法没有任何基于条件变量得设计得缺点:不需要互斥体,如果检测任务在反应任务开始轮询之前就设置了标志位,也OK,虚假唤醒也不存在了 * * 2, * 反应任务得轮询可能成本高昂,在等待标志位被设置得时候,它实质上应该被阻塞,但却仍然在运行。因此,它就占有了另一个任务本应该能够 * 利用得硬件线程,而且在每次运行以及时间片结束时,都会产生语境切换的成本。真正处于阻塞状态的任务不会耗用以上内容。 * * 这倒是基于条件变量的一个优点,因为等待调用的任务会真正地被阻塞。 * * @return int */

//方法三

//https://blog.csdn.net/Myair_AC/article/details/77453590

/** * @brief * 结合条件变量和基于标志位地设计。 * 1,标志位表示是否发生了有意义地事件,但是访问该标志要通过互斥量加以同步 * 2,因为互斥锁会阻止并发访问该标志位 */

代码语言:javascript
复制
//检测任务
std::condition_variable cv;
std::mutex m;
bool flag(false);
{
    std::lock_guard<std::mutex> g(m);//由g的构造函数锁定m
    flag = true;//通知反应任务,第一部分
}//由g的析构函数为m解锁
cv.notify_one();//通知反应任务,第二部分

//反应部分
{
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk,[]{
        return flag;
    })
}

/** * @brief * 方法三的特点: * * 1,能够运作,在检测任务通知之前响应任务就开始等待也没关系,在存在虚假唤醒的前提下也没影响,而且不需要轮询 * * 2,检测任务和反应任务的沟通方式非常奇特:通知条件变量在这里的目的是告诉反应任务,它正在等到的事件可能以及发生 * 了,然而反应任务必须检查标志位才能确定 * * 3,设置标志位在这里的目的是告诉反应任务事件确实已经发生饿,但是检测任务仍然需要通知条件变量才能让反应任务被唤醒 * 并去检测标志位 * * @return int */

//方法四

//https://blog.csdn.net/godmaycry/article/details/72844159

//https://en.cppreference.com/w/cpp/thread/promise

//https://juejin.cn/post/7079952174884585509

/** * @brief * 让反应任务去等待检测任务设置的期望:摆脱条件变量,互斥量和标志位 * * 期望代表了从被调用者到调用者的信道接收端,在检测和反应任务之间并不存在这种调用和被调用者的关系 * 而 std::promise型别对象,也可以用于任何需要将信息从一处传输到另一处的场合,从检测任务传输到响应任务 * * 设计方式 * 1,检测任务有一个 std::promise型别对象,信道的写入端,反应任务有对应的期望,当检测任务发现它正在查找的事件已经发生 * 时,它会设置std::promise型别对象,即信道的写入,与此同时,反应任务调用wait以等待它的期望,该wait调用会阻塞反应任务到 * std::promise型别对象被设置为止 * * 2,发生端std::promise,接收端 std::future和std::shared_future都需要型别形参的模板,该形参表示的是要通过信道发送数据的型别 * 本例,没有数据要传送,传送的型别就是 void;;;因此,当有意义的事件发生时,检测任务将设置 std::pormise,反应任务将等待期望值 * ,即使反应任务不会接收任何来自检测任务的数据,信道也会允许反应任务通过在其 std::promise型别对象上调用 set_value来了解检测任务何时“写入” * 了其 void型别的数据 */

代码语言:javascript
复制
std::promise<void> p;//信道的约值

//检测任务
{
    //检测事件
    p.set_value();//通知反应任务
}

//反应任务
{
    //准备反应
    p.get_future().wait();//等待p对应的期望值
    //针对事件做出反应
}

/** * @brief * 方法四特点: * * 1,避免以上问题 * * 2,std::promise和期望值之间是共享状态,而共享状态通常是动态分配的,因此,你就得假设这种设计会招致在堆上进行分配和回收的成本 * * 3,std::promise型别对象只能设置一次,它和期望值之间的通信通道是个一次性机制:不能重复使用。它是基于条件变量和基于标志位的设计之间的显著差异 * 前两者都可以用来进行多次通信(条件变量可以被重复通知,标志位可以被清除并重新设置) * * 4,假定你只想暂定线程一次,在它创建之后,但在它运行线程函数之前,使用void期望值的设计是合理的选择,一开始把与创建线程相关的所有开销提前付清 * 避免在线程上执行某些操作时,线程创建的延迟,也可以完成在线程运行之前对其实施一些配置动作,比如设置其优先级和内核亲和性之类 * * @return int */

//方法五

//实现方法四中的特点4

代码语言:javascript
复制
std::promise<void> p;
void react(){
    //反应任务函数
}

//检测任务函数
void detect()
{
    std::thread t([]{ //创建线程
        p.get_future().wait();//暂定t到其期望被设置
        react();
    });
    //在这里t处于暂定状态,在调用react之前
    
    p.set_value();//取消暂停t,调用react

    //做其他工作

    t.join();//设置t为不可连结状态
}

//方法六

/** * @brief * 扩展方法五:实现可以是很多反应任务实施先暂停再取消暂停的功能 * * 1,关键在 react的代码中使用std::shared_futures而非std::future,前者是把共享状态的所有权转移给了由share生成的 * std::shared_future型别对象 * * 2,每个反应线程都需要自己的那份 std::shared_future副本去指涉到共享状态,所有,从share中获取的std::shared_future被 * 运行在反应线程上的lambda按值捕获 * */

代码语言:javascript
复制
std::promise<void> p;
//线程可以处理多个反应任务了
void detect()
{
    //sf的型别是 std::shared_future<void>
    auto sf = p.get_future().share();
    //反应任务的容器
    std::vector<std::thread> vt;

    for(int i=0; i < 10; ++i)
    {
        vt.emplace_back([sf]{
            sf.wait();
            react();
        });//sf局部副本之上的wait
    }

    //

    p.set_value();//让所有线程取消暂停

    //

    for(auto& t:vt)
    {
        t.join();//把所有线程设置不可联结状态
    }
}

1,如果仅为了实现平凡事件通信,基于条件变量的设计会要求多余的互斥量, 这会给相互关联的检测和反应任务带来约束,并要求反应任务校验事件确 已发生 2,使用标志位的设计可以避免上述问题,但这一设计基于轮询而非阻塞 3, 条件变量和标志位可以一起使用 但这样的通信机制设计结果不甚自然 4,使用 std:: promise 型别对象和期值就可以回避这些问题,但是一来这个途 径为了共享状态需要使用堆内存,而且仅限于一次性通信

6条款40:对并发使用 std::atomic,对特种内存使用 volatile

/** * @brief * * std::atomic 模板:std::atomic,std::atomic和 std::atomic<Widget*> 等 提供的操作 * 可以保证被其他线程视为原子的,也就是像外加了一层互斥量进行保护一样,原子操作比使用互斥量更加高效 * * volatile对于访问特种内存有用,但不能用于并发程序设计 * */

代码语言:javascript
复制
void doAsyncWork1()
{   
    ++ac;
    ++vc;
    std::cout<<"doAsyncWork: "<<"ai: "<<ai<<" vi: "<<vi<<" ac: "<<ac<<" vc: "<<vc<<std::endl;
}

void doAsyncWork2()
{   
    ++ac;
    ++vc;
    std::cout<<"doAsyncWork: "<<"ai: "<<ai<<" vi: "<<vi<<" ac: "<<ac<<" vc: "<<vc<<std::endl;
}

//情况1:std::atomic

代码语言:javascript
复制
    //情况1:std::atomic
   // std::atomic<int> ai(0);//将 ai视为 0
    ai = 10;//将ai 原子地设置为10
    std::cout << ai <<std::endl;//原子地读取 ai的值
    ++ai;//原子地将 ai自增为 11
    --ai;//原子地将ai自减为10

    //在以上语句执行期间,其他读取 ai的线程可能只会看到它取值为 0 ,10 或 11,而不可能由其他值,当然,前提假设这是
    //修改 ai值得唯一线程

     auto fut1 = std::async(doAsyncWork1);

//情况2:volatile 不能提供任何保障

代码语言:javascript
复制
    //情况2:volatile 不能提供任何保障
    //volatile int vi(0);//初始为0
    vi = 10;//将vi设置为10
    std::cout<<vi;//读取vi得值
    ++vi;//将vi自增为11
    --vi;//将vi自减为10

    auto fut2 = std::async(doAsyncWork1);

    //但是,在这段代码执行期间,如果其他线程正在读取 vi得值,他们可能会看到任何值,例如 -12, 68,4044040040.因此
    //会出现在即非std::atomic,也非由互斥量保护得同时读写操作,数据竞险

//情况3,再次对比

代码语言:javascript
复制
    //情况3,再次对比
    //考虑两者由多个线程执行自增得简单计数器,都初始化为0
    // std::atomic<int> ac(0);
    // volatile int vc(0);
    //之后,在两个同时运行得线程中将两者各自增一次

    /**
     * @brief 
     * 
     *  线程1                 线程2
     * 
     *  ++ac                 ++ac
     *  ++vc                 ++vc
     * 
     * 1, std::atomic型别对象的值一定是2,每次自增都是原子操作
     * 2,vc的值不一定是2,自增可能会不以原子方式发生,每次自增包括读取vc的值,自增读取的值,并将结果写回vc
     */

//i情况4

//假设一个任务负责计算第二个任务所需的重要值,当第一个任务已经计算出该值时,它必须把这个值通信到第二个任务

代码语言:javascript
复制
//情况4
//假设一个任务负责计算第二个任务所需的重要值,当第一个任务已经计算出该值时,它必须把这个值通信到第二个任务
int computeValue()
{

}
std::atomic<bool> val(false);
auto tmp = computeValue();//计算值
val = true;
/**
 * @brief 
 * 1, 使用 atomic可以保证 tmp的赋值一定在 val = true之前,否则编译器可能将这些不相关的赋值重新排序
 * 
 * 2,相反,将atomic改成   volatile bool val(false); 
 * 不会给代码施加同样的重新排序的约束,可能会改变赋值顺序,机器代码阻止底层硬件在其内核上的顺序
 * 
 */

//情况5

//volatile难道就没有用武之地了嘛?即使对并发无用,也会告诉编译器,正在处理的内存不具备常规行为

代码语言:javascript
复制
//情况5
//volatile难道就没有用武之地了嘛?即使对并发无用,也会告诉编译器,正在处理的内存不具备常规行为
/**
 * @brief 
 * 
 * 1, 常规内存
 *   1,1 如果你向某个内存位置写入了值,该值会一直保留在那里,直到它被覆盖为止
 *   1,2 如果向某内存位置写入某值,期间未读取该内存位置,然后再次写入该内存位置,则第一次写入可以消除,因为其写入结果从未使用过
 *     auto y =x;
 *         y =x;
 * 
 * 2,特种内存
 *  最常见的是用于内存映射 I/O的内存,这种内存的位置实际上是用于与外部设备通信,而非用于读取或写入常规内存(RAM)
 *    比如 1 中的例子,x对应于由温度传感器报告的值,则 x的第二次读取操作并非多余,因为在第一次和第二次读取之间,温度可能已经改变
 * 
 * 而 volatile的用处就是告诉编译器,正在处理的是特种内存,不要对在此内存上的操作做任何优化,此时std::atomic就失去了作用
 * 
 * 但是,两者可以结合使用,如果 vai对应由多个线程同时访问的内存映射的 I/O 位置,会有用
 * volatile std::atomic<int> vai;// 针对
 * 
 * @return int 
 */
volatile int x;
auto y = x;//读取x
y = x;//再次读取x,不会被优化掉
x =10;//写入x,不会被优化掉
x=20;//再次写入x

std:: atomic 用于多线程访问的数据 且不用互斥量 它是摸写并发软件的 工具。volatile 用于读写操作不可以被优化掉的内存 它是在面对特种内存时使 用的工具

Part2第8章 微调

/** * @brief * 本不应该设计按值传递的,在什么情况下可以使用? * 本不该置入的,在什么情况下使用? * */

7条款41:针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递

//例子:有些函数的形参是拿来复制的

//方式一:为了效率 左值实施复制,右值实施移动

代码语言:javascript
复制
//方式一:为了效率 左值实施复制,右值实施移动
class Widget{
    public:
        void addName(const std::string& newName)
        {
            names.push_back(newName);//接收左值,对其实施复制
        }

        void addName(std::string&& newName)
        {
            names.push_back(std::move(newName));//接收右值,对其实施移动
        }
    private:
        std::vector<std::string> names;
};
//局限性:需要撰写两份函数声明,两份函数实现,维护多

//方式二:接收万能引用的函数模板

代码语言:javascript
复制
//方式二:接收万能引用的函数模板
class Widget{
    public:
        template<typename T>
        void addName(T&& newName)
        {
            names.push_back(std::forward<T>(newName));
        }
    private:
        std::vector<std::string> names;
};
//局限性:作为模板,addName的实现必须在头文件中。并且有些函数不适合通用引用方式传递 std::string

//方式三:按值传递

//此时你要放弃你身为C++程序员学到的第一条规则:避免按值传递用户定义型别对象

//但是本例可能是个特例,重点看看为什么?

代码语言:javascript
复制
//方式三:按值传递
//此时你要放弃你身为C++程序员学到的第一条规则:避免按值传递用户定义型别对象
//但是本例可能是个特例,重点看看为什么?
class Widget{
    public:
       void addName(std::string newName)
       {
        names.push_back(std::move(newName));//即接收左值也接收右值,对后者实施移动
       }
     private:
        std::vector<std::string> names;
};

/** * @brief * 我们知道:std::move仅会针对右值引用实施,但这种情况 * * 1,无论调用方传入什么,newName都对它没有任何依赖,所以更改 newName不会对调用方产生任何影响 * * 2,这是使用 newName是对它的最后一次使用,所以移动它也不会对函数的其余部分产生任何影响 * * 但是,会导致效率问题嘛?会不会发生高昂的成本呢? * * c++98中肯定会发生的,无论调用方传入的是什么,形参newName都会经过复制构造函数创建 * * 不过,在C++11中,newName仅在传入左值时候才会被复制构造,若传入右值,会被移动构造 * */

代码语言:javascript
复制
Widget w;
std::string name("Bart");
w.addName(name);//在调 addName时传入左值,对newName实施的是复制构造

w.addName(name + "jenne");//在调addName时传入右值,对newName实施的是移动构造

//分析以上三种方法的成本问题 /*** * 1,重载 * :无论传入左值还是右值,调用方的实参都会绑定到名字为 newName 的引用上。而这样做不会在复制或移动时带来任何成本 * 内部实现是,对于左值是一次复制,对于右值是一次移动 * * 2,使我万能引用 * ,调用方的实参会绑定到引用 newName 上。这是个无成本操作 * 内部实现是,对于左值是一次复制,对于右值是一次移动 * * 3,按值传递 * 无论传入的是左值还是右值,针对形参 newName都必须实施一次构造,左值是一次复制构造,右值是一次移动构造 * 并且,在函数体内,newName需要无条件地移入 Widget::names,这样合计成本: * 3,1 左值是一次复制加一次移动 * 3,2 右值是两次移动 * * 因此,无论左值还是右值,都存在一次额外地移动操作 * * / //回到题目,说到是如果移动开销下,可以使用按值传递,避免了重载和万能引用地问题,看下面一个例子 //std::unique_ptr是个只移型别,虽然采用了”“重载”,但只由单个函数组成 ***/

代码语言:javascript
复制
class Widget{
    public:
        void setPtr(std::unique_ptr<std::string>&& ptr)
        {
            p = std::move(ptr);
        }
    private:
        std::unique_ptr<std::string> p;
};

Widget w;
w.setPtr(std::make_unique<std::string>("C++"));
//std::make_unique返回右值 std::unique_ptr<std::string>会以右值引用方式传递给setPtr,在那里它被移入数据成员p,总成本是一次移动
//如果改成按值传递:同一调用会导致针对形参 ptr实施移动构造后,再将ptr移入数据成员p,这样总成本是两次移动

//记住:必须针对一定会被复制的形参才考虑按值传递,假设在 push_back之前有个条件拦住了,没被复制,同样也会导致构造和析构newName的成本 //而如果采用了按引用途径,就不会有这种问题 //如果采用赋值来实施形参复制的话,情况更复杂了!

代码语言:javascript
复制
class Password{
    public:
        explicit Password(std::string pwd): text(std::move(pwd)){} //按值传递 ,对 text实施构造
        //构造函数采用按值传递会导致std::string 的移动构造成本

        void changeTo(std::string newPwd) 
        {
            text = std::move(newPwd);//按值传递,对 text实施赋值
        }
    private:
        std::string text;
};

std::string initPwd("aaaaaaaaaaaaaaaaaaa");
Password p(initPwd);

//修改密码
std::string newPwd = "advsdvsdvs";
p.changeTo(newPwd);//采用赋值操作修改text,会导致成本增加
//旧密码比新密码更长,所以不需要实施任何内存分配和回收,采用重载途径,不会发生任何动态内存管理行为

//按值传递的另外一个缺点:会遭遇切片问题

//有个函数被设计用以接收一个基类型别或从它的派生的任何型别的形参,你肯定不会想要声明该型别的按值传递形参,

//因为传入的任何可能的派生型别对象的派生类特征都将被 切掉

//https://blog.csdn.net/m0_56104219/article/details/123244825

代码语言:javascript
复制
//基类
calss Widget{

};
//派生类
calss SpWidget:public Widget{

};
//未任何Widet而设计的函数,包括派生型别会受到切片问题的侵害
void processWidget(Widget w);
SpWidget sw;
//processWidget看到的只是一个 Widget而非 SpWidget型别的对象
processWidget(sw);

1,对于可复制的、在移动成本低廉的并且一定会被复制的形参而言,按值传 递可能会和按引用传递的具各相近的效率,并可能生成更少量 目标代码 2,构造复制形参的成本可能比经 赋值复制形参高出很多 3, 按值传递肯定会导致切片问题 所以基类型别特别不适用于按值传递

8条款42:考虑置入而非插入

//插入

//情况1

代码语言:javascript
复制
//情况1
std::vector<std::string> vs;//持有std::string型别对象的容器
vs.push_back("xyzzy");//添加字符串字面量

//但是,容器持有的是 std::string型别对象,传入的却是字符串字面量,不是std::string

//std::vectoe的push_back针对左值和右值给了不同的重载版本
template<class T,class Allocator=allocator<T>>
class vector{
    public:
        void push_back(const T& x);//插入左值
        void push_back(T&& x);//插入右值
};

代码语言:javascript
复制
//因此在上面 push_back的调用语句中,编译器会看到实参型别 (const char [6])与 push_back (std::string的引用型别)
//接受的形参型别之间的不匹配。
//解决办法是:从字符串字面量出发创建std::string型别的临时对象,并将该临时对象传递给 push_back,换句话是,看作是这样

vs.push_back(std::string("xyzzy"));//创建std::string型别的临时对象,并将其传递给push_back

//但是,性能问题需要考虑:以上调用了两次构造和一次析构,完整执行流程如下

/** * @brief * 1,从字符串 字面量 “xyzzy” 创建 std::string型别的临时对象。该对象没有名字,成为tmp,针对tmp实施的构造,就是第一次的 * std::string 构造的调用。因为是个临时对象,所有 tmp 是个右值 * * 2,tmp被传递给 push_back的右值重载版本,在那里它被绑定到右值引用形参x。之后,会在内存中为 std::vector构造一个 x的副本 * ,这是第二次的构造,它的结果在 std::vector内创建了一个新的对象 (用来将 x复制到 std::vector中的构造函数,是移动构造函数, * 因为作为右值引用的x,在复制之前被转换成了右值) * * 3,最后 push_back返回的那一时刻,tmp被析构,所有,这就需要调用一次std::string的析构函数 */

代码语言:javascript
复制
//因此,有没有办法将字符串字面量直接传递给步骤2那段std::vector内构造std::string型别对象的代码,就可以避免先构造再析构tmp了
//有,利用 emplace_back : 它使用传入的任何实参在 std::vector内构造一个 std::string,不会涉及任何临时对象
vs.emplace_back("xyzzy");//直接从 “xyzzy” 出发在 vs内构造 std::string型别对象

/** * @brief * emplace_back * * 使用了完美转发,只有没有完美转发的限制,就可以通过 emplace_back传递任意型别的任意数量和任意组合的实参 * vs.emplace_back(50,'x'); * * 置入 插入 * * emplace_back push_back * empalce_front push_front * * 优点:置入接受的是待插入对象的构造函数实参,避免临时对象的创建和析构,而插入接受的是待插入对象,无法避免 * * * * @return int */

代码语言:javascript
复制
//同样,即使插入函数并不要求创建临时对象的情况,也可以使用置入,效率一样
std::string qq("aaaaa");
vs.push_back(qq);//在vs的尾部复制了aaaa
vs.emplace_back(qq);//同上

//既然这样,为何不总使用置入呢?

/** * @brief * 因为不幸的是:存在插入函数运行更快的情况 * * 取决于传递的实参型别,使用的容器种类,请求插入或置入的容器位置,所持有型别构造函数的异常安全性,还有,对于禁止出现重复值的容器 * * std::set,std::map,std::unordered_set, std::unordered_map,容器中是否已经存在要添加的值 * * @return int */

//同时,置入一定比插入高效的情况,需要满足以下情况都成立

/** * @brief * 1,欲添加的值是以构造而非复制方式加入容器 * 本例子xyzzy被添加到vs的结尾,该位置尚不存在对象,因此,新值必须以构造方式加入 std::vector * 如改成: 向vs的开头添加 xyzzy * * vs.empalce(vs.begin(),"xyzzy") * * 对于这段代码,很少会有实现是在待添加的 std::string在由 vs[0] 占用的内存中实施构造,这里一般 * 采用移动赋值的方式来让该值就位,既然是移动赋值,总要有个作为源的移动对象,也意味着需要创建一个 * 临时对象作为移动的源。但是置入相对于插入的主要优点在于既不会创建也不会析构临时对象,那么当添加 * 的值经由赋值放入容器的时候,置入的边际效用也就没有了。 * * 2,传递的实参型别与容器持有之物的型别不同 * * 3,容器不太可能由于出现重复情况而拒绝待添加的新值 * * 同时是否考虑利用置入函数,还要关心两个问题: * * * @return int */

代码语言:javascript
复制
//第一个问题:资源管理
std::list<std::shared_ptr<Widget>> ptrs;
//自定义删除器函数
void killWidget(Widget* pw);
//插入代码
pts.push_back(std::shared_ptr<Widget>(new Widget,killWidget));
//也可以这样
pts.push_back({new Widget,killWidget});

//在调用push_back之前都会创建一个 std::shared_ptr型别的临时对象
//push_back的形参是个 std::shared_ptr型别的引用,所以必须存在一个std::shared_ptr型别对象来让该形参指涉到
//如选用emplace_back,本可以避免创建 std::shared_ptr型别的临时对象,但是有时候有临时对象的收益超过成本

/**
 * @brief 
 * 1,构造一个 std::shared_ptr<Widget>型别的临时对象,用来持有 从 “new Widget”返回的裸指针,该对象成为tmp
 * 
 * 2,push_back会按引用方式接受tmp,在为链表节点分配内存以持有tmp的副本的过程中,抛出了内存不足的异常
 * 
 * 3,该异常传播到 push_back之外,tmp被析构,作为 给 Widget兜底的,指涉到它并对其施加管理的 std::shared_ptr<Widget>
 * 型别对象会自动释放该Widget,调用 killWidget达成该目的
 * 
 * @return int 
 */

//但是,如果调用的是emplace_back,会发生什么
ptrs.emplace_back(new Widget,killWidget);

/**
 * @brief 
 * 1, 从 new Widget返回的裸指针被完美转发,并运行到 emplace_back内为链表节点分配内容的执行点,之后该内存分配失败,并
 * 抛出了内存不足的异常
 * 
 * 2,该异常传播到了 emplace_back之外, 作为唯一可以获取堆上Widget的抓手的罗指针,却丢失了,那个Widget都发生了泄露
 * 
 * 
 * @return int 
 */

//正确的做法
//从 new Widget中获取指针并将其在独立语句中转交给资源管理对象,然后该对象作为右值传递给你最初想要向其传递 new Widget的函数
//push_back
std::shared_ptr<Widget> spw(new Widget,killWidget);//构造Widget并用spw管理它
ptrs.push_back(std::move(spw));//以右值形式添加spw
//emplace_back
ptrs.emplace_back(std::move(spw));

C++正则表达式

https://blog.csdn.net/l357630798/article/details/78235

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码出名企路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Part1第7章 并发API
    • 1条款35:优先选用基于任务而非基于线程的程序设计
      • 2条款36:如果异步是必要的,则指定std::launch::async
        • 3条款37:使 std::thread 型别对象在所有路径皆不可联结
          • 4条款38:对变化多端的线程句柄析构函数行为保持关注
            • 5条款39:考虑针对一次性事件通信使用以void为模板型别实参得期望值
              • 6条款40:对并发使用 std::atomic,对特种内存使用 volatile
              • Part2第8章 微调
                • 7条款41:针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递
                  • 8条款42:考虑置入而非插入
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档