C++多线程笔记(二)

举报
码农爱学习 发表于 2021/12/22 22:48:55 2021/12/22
【摘要】 C++多线程笔记(二)

1 unique_lock

  • unique_lock可完成lock_guard的功能,另外还有额外的参数,实现其它功能

  • unique_lock的defer_lock参数,即先不加锁,只是先绑定unique_lock与mutex,另外,可以 随时进行加锁、解锁操作,某些情况下可提高效率(注意此时的加、解锁是通过unique_lock的成员函数.lock().unlock()实现的)

  • unique_lock还可以 交换管理权(unique_lock可以被移动,lock_guard不可以)

代码示例:

#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<fstream>
​
class LogFile
{
    std::mutex m_mutex;//锁
    std::ofstream f;
public:
    LogFile()
    {
        f.open("log.txt");
    }
    void shared_print(std::string id, int value)
    {
        std::unique_lock<std::mutex> locker(m_mutex,std::defer_lock);//先不加锁
        //...此处代码不需要保护
        locker.lock();
        f << id << ":" << value << std::endl;//此处才是保护区
        std::cout << id << ":" << value << std::endl;
        locker.unlock();
        //...此处代码也不需要保护
        locker.lock();
        //以下可能又需要保护了
​
        //unique_lock可以被移动,lock_guard不可以
        std::unique_lock<std::mutex> locker2 = std::move(locker);
        //将unique_lock管理的lock交给新的unique_lock的管理
        //即将m_mutex与locker2绑定在一起!此时locker已无用
    }
​
};
​
void function_1(LogFile& log)
{
    for (int i = 0; i > -1000; i--)
        log.shared_print(std::string("from t1:"), i);
}
​
int main()//主线程
{
    LogFile log;
    std::thread t1(function_1, std::ref(log));//t1线程开始运行
​
    for (int i = 0; i < 1000; i++)
    {
        log.shared_print(std::string("from main:"), i);
    }
​
    t1.join();
​
    return 0;
}

程序运行结果依然是主线程和子线程各自输出1000条信息以及将信息保存到txt文件中,和上篇中 “死锁 & adopt_lock” 的结果类似,这里不再展示。

2 call_once

之前测试代码中,log.txt文件的创建(f.open("log.txt");)是在类的构造函数中的,如果没有使用到打印函数(shared_print()),则没必要创建该文件,若将其放入打印函数中,会被多次调用。

这时,可以使用call_once()函数并配合once_flag标记,保证即使是多线程情况下,也只被执行一次

代码:

#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<fstream>
​
class LogFile
{
    std::mutex m_mutex;
    std::once_flag m_flag;//once_flag标记
    std::ofstream f;
public:
    LogFile()
    {
        //f.open("log.txt");//不再需要!
    }
    //此种情况是:那个线程先要打印,则首次创建文件(都不需打印,则始终不创建文件)
    void shared_print(std::string id, int value)
    {
        std::call_once(m_flag, [&]() {f.open("log.txt"); });//call_once函数
        std::unique_lock<std::mutex> locker(m_mutex);
        f << id << ":" << value << std::endl;
        std::cout << id << ":" << value << std::endl;
    }
​
};
​
void function_1(LogFile& log)
{
    for (int i = 0; i > -1000; i--)
        log.shared_print(std::string("from t1:"), i);
}
​
int main()//主线程
{
    LogFile log;
    std::thread t1(function_1, std::ref(log));//t1线程开始运行
​
    for (int i = 0; i < 1000; i++)
    {
        log.shared_print(std::string("from main:"), i);
    }
​
    t1.join();
​
    return 0;
}

程序运行结果同上,不再展示。

3 condition_varible & wait

首先引入一个“ 生产者消费者问题 ”,有两个线程,线程A产生数据(生产者),线程B获取数据(消费者),线程A在产生数据时,线程B不能过来取,因而线程B的执行必须要依赖线程A。

下面的程序通过对队列结构中存取和取出数据,模拟生产和消费。采用加锁机制,保证两个子线程互斥,并且消费者线程使用循环查询机制,不断检查是否有可用数据。

代码示例:

#include<deque>
#include<functional>
#include<iostream>
#include<fstream>
#include<thread>
#include<string>
#include<mutex>
#include<condition_variable>
​
using namespace std;
​
deque<int> q;//整形队列(共享内存)
mutex mu;//互斥对象
​
void function_1()//生产者
{
    int count = 10;
    while (count > 0)
    {
        unique_lock<mutex> locker(mu);
        q.push_front(count);//放入一个数
        locker.unlock();
        this_thread::sleep_for(chrono::seconds(1));
        count--;
    }
}
void function_2()//消费者
{
    int data = 0;
    while (data != 1)
    {
        unique_lock<mutex> locker(mu);
        if (!q.empty())
        {
            data = q.back();
            q.pop_back();//取出一个数
            locker.unlock();
            cout << "t2 got a value from t1:" << data << endl;
        }
        else//队列为空时,等待一段时间再继续,减少无意义的判断次数
        {
            locker.unlock();
            this_thread::sleep_for(chrono::seconds(5));//不加此句,function_2一直循环检测q是否为空
        }
        cout << "function_2..." << endl;
    }
}
​
int main()//主线程
{
    thread t1(function_1);
    thread t2(function_2);
    t1.join();
    t2.join();
    return 0;
}

运行结果:

t2 got a value from t1:10
function_2...
function_2...
t2 got a value from t1:9
function_2...
t2 got a value from t1:8
function_2...
t2 got a value from t1:7
function_2...
t2 got a value from t1:6
function_2...
function_2...
t2 got a value from t1:5
function_2...
t2 got a value from t1:4
function_2...
t2 got a value from t1:3
function_2...
t2 got a value from t1:2
function_2...
t2 got a value from t1:1
function_2...
请按任意键继续. . .

程序输出的中间过程会有些停顿,且消费者线程(function_2)存在多次查询无果的情况,注意上面程序的function_2中还特别加入了线程休眠等待(sleep_for),如果不加,function_2会不停的进行无效的查询访问,效率极低,而加入的等待时间过程,又会使function_2不能及时获取数据。

针对上面的问题,就需要引入 条件变量 condition_varible ,配合.wait().notify_one()成员函数,即可通过“ 等待通知 ”的形式使function_2在恰当的时间获得数据,避免无谓低效的查询。

修改后程序如下(头文件包含以及主函数省略,与上同):

deque<int> q;//整形队列
mutex mu;//互斥对象->锁
condition_variable cond;//【条件变量】避免线程无谓的循环
​
void function_1()
{
    int count = 10;
    while (count > 0)
    {
        unique_lock<mutex> locker1(mu);//使用unique_lock来管理锁
        q.push_front(count);//队列中写数据(队前插入)10 9 8...1 0
        locker1.unlock();//提前解锁
        //cond.notify_one();//激活一个等待这个条件的线程
        cond.notify_all();//激活所有类似线程2的等待线程
        this_thread::sleep_for(chrono::seconds(1));//此线程休息1秒
        count--;
    }
}
void function_2()
{
    int data = 0;
    while (data != 1)
    {
        unique_lock<mutex> locker2(mu);//互斥对象mu被线程2锁住,线程不会在锁住的情况下休眠
        cond.wait(locker2, []() {return !q.empty(); });//将线程2休眠直到线程1的notify_one()才将其激活
        //上句第一个参数是解锁加锁,第2个参数为【lambda函数】,防止伪激活
        data = q.back();//队列中读数据
        q.pop_back();//队列中删除数据(队尾删除)
        locker2.unlock();//提前解锁
        cout << "t2 got a value from t1:" << data << endl;
    }
}

运行结果:

t2 got a value from t1:10
t2 got a value from t1:9
t2 got a value from t1:8
t2 got a value from t1:7
t2 got a value from t1:6
t2 got a value from t1:5
t2 got a value from t1:4
t2 got a value from t1:3
t2 got a value from t1:2
t2 got a value from t1:1
请按任意键继续. . .

此时function_2就可以在functon_1产生数据后及时获取了,并且没有了无效的查询过程。

4 future & async

std::async为一函数模板,用来启动一 异步任务(即自动创建一线程并执行对应的线程入口函数),之后返回一个std::future对象(对象中包含线程函数的返回结果),最后通过future对象的.get()成员函数来获取结果。 (如果只是简单地通过引用的方式在子线程和主线程间传递结果,需要 额外的加锁 机制!)

  • .get()成员函数等待子线程返回结果,否则一直等待(注:只能get一次,多次调用则报异常) 与之类似的.wait()成员函数只等待结果,不获取结果(类似于join())

  • 如果.get().wait()都不用,主程序结束时仍等待子线程

  • future:理解为提供了一种访问异步操作结果的机制,即需要等待一段时间(线程执行完毕) 主线程才能从 子线程 中拿到结果

  • 额外向async()传递一个参数(std::launch枚举类型),实现其它功能

    • std::launch::deffered:表示线程入口函数被延迟到get()或wait()时才执行(但仍是主线程,没有新线程!),若没有get()或wait()则始终不执行子线程

    • std::launch::async:立即创建一个新线程

    • std::launch::async|std::launch::deffered:根据系统资源消耗情况,可能立即创建新线程,也可能延迟

代码示例:

#include<iostream>
#include<fstream>
#include<thread>
#include<string>
#include<mutex>
#include<future>//引入future头文件
​
using namespace std;
​
int factorial(int N)//阶乘函数
{
    cout << "子线程启动>>>>>  线程ID: " << this_thread::get_id() << endl;
    int res = 1;
    for (int i = N; i > 1; i--)
        res *= i;
    chrono::milliseconds dura(3000);//定义一个3秒的时间
    this_thread::sleep_for(dura);//模拟长时间计算
​
    //cout << "Result is:" << res << endl;
    cout << "子线程结束<<<<<  线程ID: " << this_thread::get_id() << endl;
    return res;
}
​
int main()//主线程
{
    cout << "主线程启动>>>>>>>>>>>>>  线程ID: " << this_thread::get_id() << endl;
    int x = 0;
    //future<int> fu = async(launch::async | launch::deferred, factorial, 4);//【两者方式均可能】
    future<int> fu = async(launch::async, factorial, 4);//【强制创建一个线程】
    //future<int> fu = async(launch::deferred, factorial, 4);//【延时执行】
    //future<int> fu = async(factorial, 4);//【不加参数等价于async|deferred】
    cout << "continue..." << endl;
    cout << "continue..." << endl;
    cout << "continue..." << endl;
    x = fu.get();//等待子线程结果
    cout << "Result is:" << x << endl;
    cout << "主线程启动<<<<<<<<<<<<<  线程ID: " << this_thread::get_id() << endl;
    return 0;
}

运行结果:

主线程启动>>>>>>>>>>>>>  线程ID: 2468
continue...子线程启动>>>>>  线程ID: 9980
​
continue...
continue...
子线程结束<<<<<  线程ID: 9980
Result is:24
主线程启动<<<<<<<<<<<<<  线程ID: 2468
请按任意键继续. . .

5 promise

std::promise类模板(事先约定)可以在某个线程中给它赋值,然后在其它线程中,将该值取出

代码示例:

#include<iostream>
#include<fstream>
#include<thread>
#include<string>
#include<mutex>
#include<future>
​
using namespace std;
​
int factorial(future<int>&f)//阶乘函数
{
    int res = 1;
    int N = f.get();
    for (int i = N; i > 1; i--)
        res *= i;
    cout << "Result is:" << res << endl;
    return res;
}
​
int main()//主线程
{
    int x;
    promise<int> p;//主线程中的int变量(“约定型”变量)
    future<int> f = p.get_future();//该变量值的值约定从“将来”future获得
    cout << "pass the promise-future 'p' to factorial()..." << endl;
    future<int> fu = async(launch::async, factorial, ref(f));//按引用传递f(一个未来的约定)
​
    this_thread::sleep_for(chrono::seconds(3));//此线程休息3秒(模拟未来的时间)
    p.set_value(4);//(约定时间到)为其赋值,此时子线程factorial才能获得参数值
​
    x = fu.get();//获得子线程factorial的计算结果
    cout << "Get from child thread:" << x << endl;
​
    return 0;
}

运行结果:

pass the promise-future 'p' to factorial()...
Result is:24
Get from child thread:24
请按任意键继续. . .
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。