文章目录

  • 创建线程
    • std::thread 类
    • 使用join()
    • 使用 detach()
  • 警惕作用域
  • 线程不能复制
  • 给线程传参
    • 传递指针
    • 传递引用
  • 以类成员函数为线程函数
  • 以容器存放线程对象
  • 互斥量
    • std::mutex
    • std::lock_guard
  • 条件变量
  • call_once

创建线程

C++11 增加了线程以及线程相关的类, 而之前并没有对并发编程提供语言级别的支持

std::thread 类

使用 std::thread 类来创建线程, 我们需要提供的只是线程函数, 或者线程对象, 同时提供必要的参数
std::thread 表示单个执行的线程, 使用thread 类首先会构造一个线程对象, 然后开始执行线程函数,

#include <iostream>
#include <thread> //需要包含的头using namespace std;void func(int a, double b)  //有参数, 参数数量不限
{cout << a << ' ' << b << endl;
}void func2() //无参数
{cout << "hello!\n";
}int main()
{thread t1(func, 1, 2); //提供参数thread t2(func2);//可以使用 lambda表达式thread t3([](int a, double b){cout << a << ' ' << b << endl;}, 3, 4);cout << t1.get_id()  << "****" << endl;  //可以使用 get_id() 获取线程 idt1.join();t2.join();t3.join();return 0;
}

使用join()

我们知道, 上例中如果主线程 (main) 先退出, 那些还未完成任务的线程将得不到执行机会, 因为 main 会在执行完调用 exit(), 然后整个进程就结束了, 那它的"子线程" (我们知道线程是平级的, 这里只是, 形象一点) 自然也就 over 了
所以就像上例中, 线程对象调用 join() 函数, join() 会阻塞当前线程, 直到线程函数执行结束, 如果线程有返回值, 会被忽略

使用 detach()

对比于 join(), 我们肯定有不想阻塞当前线程的时候, 这时可以调用 detach(), 这个函数会分离线程对象和线程函数, 让线程作为后台线程去执行, 当前线程也不会被阻塞了, 但是分离之后, 也不能再和线程发生联系了, 例如不能再调用 get_id() 来获取线程 id 了, 或者调用 join() 都是不行的, 同时也无法控制线程何时结束

#include <thread>
void func()
{//...
}int main()
{std::thread t(func);t.detach();// 可以做其他事了, 并不会被阻塞return 0;
}

程序终止后, 不会等待在后台执行的其余分离线程, 而是将他们挂起, 并且本地对象被破坏

警惕作用域

std::thread 出了作用域之后就会被析构, 这时如果线程函数还没有执行完就会发生错误, 因此, 要注意保证线程函数的生命周期在线程变量 std::thread 之内

线程不能复制

std::thread 不能复制, 但是可以移动
也就是说, 不能对线程进行复制构造, 复制赋值, 但是可以移动构造, 移动赋值

#include <iostream>
#include <thread>void func()
{std::cout << "here is func" << std::endl;
}int main()
{std::thread t1(func);std::thread t2;t2 = t1; //errort2 = std::move(t1); //right, 将 t1 的线程控制权转移给 t2std::cout << t1.get_id() << std::endl;  //error,t1已经失去了线程控制权t1 = std::thread(func); //right, 直接构造, 创建的是临时对象,所以隐式调用movet1 = std::move(t2); //error, 不能通过赋值一个新值来放弃一个已有线程, 这样会直接导致程序崩溃
}

std::thread= 重载了, 调用 operator= 是移动构造函数, 复制被禁用了,

给线程传参

传递指针

#include <iostream>
#include <thread>void func(int* a){//这里是直接修改指针指向的地址中的值,并不是修改形参指针的指向,所以传指针可以改变实参的值*a += 10;
}int main()
{int x = 10;std::thread t1(func, &x);t1.join();std::cout << x << std::endl;return 0;
}

上例代码, 可以如愿改变 x 的值, 但是看下面的代码, 当我们传递引用时, 却好像并不能如我们所想:

传递引用

#include <iostream>
#include <thread>void func(int& a)
{a += 10;
}int main()
{int x = 10;std::thread t1(func, x);  //编译会报错// std::thread t1(func, std::ref(x)); //正确的写法t1.join();std::cout << x << std::endl;return 0;
}

我们想让 func 函数对 x 进行更新, 但是实际上给线程传参会以拷贝的形式复制到线程空间, 所以即使是引用, 引用的实际上是新线程堆栈中的临时值, 为了解决这个问题, 我们需要使用引用包装器 std::ref()
改成:
std::thread t1(func, std::ref(x));

实际上, 我的编译器对于std::thread t1(func, x);这段代码直接给出了编译错误,改成std::thread t1(func, std::ref(x));后就没问题了…


以类成员函数为线程函数

因为类内成员涉及 this 指针, 就和所需的线程函数参数不同了

#include <iostream>
#include <thread>using namespace std;class A
{public:void func1()  {cout << "here is class A`s func 1" << endl;}static void func2() {cout << "here is class A`s func 2" << endl;}void func3() {thread t1(&A::func1, this);  //非静态成员函数thread t2(A::func2);       //静态成员函数t1.join();t2.join();}
};int main()
{A a;thread t1(&A::func1, &a);  //非静态成员函数thread t2(A::func2);       //静态成员函数t1.join();t2.join();a.func3();
}

注意的是, 如果我们选择将成员函数变成静态的使用, 那我们就不能使用非静态的成员变量了, 解决办法也很简单, 给静态成员函数传递该对象的 this 指针就好了。

  • 非静态成员函数需要加上& 符号,并且应该加上类名和::作用符,写在类外就是std::thread t1(&A::func1, &a); ,写在类中就需要用this,即thread t1(&A::func1, this);;
  • 静态成员函数不需要加上&,也没有this。因为静态成员函数属于类,不属于实例对象。即thread t2(A::func2);

可以参考一下我犯过的这个错误


以容器存放线程对象

我们可以用容器保存创建的多个线程对象, 而当我们像其中插入元素时, 建议使用 emplace_bcak() 而不是 push_back()

我们知道 push_back() 会创建一个临时对象然后拷贝, 当然自从有了移动语意这里出发都是移动, 如下例:

#include <iostream>
#include <thread>
#include <vector>using namespace std;class A
{public:void func1() {cout << "here is class A`s func 1" << endl;}void func3() {tmpThread.push_back(thread(&A::func1, this));     //(1)tmpThread.emplace_back(&A::func1, this);   //(2)}vector<thread> tmpThread;
};

比较上例中 (1) (2)两处, 明显发现emplace_back()push_back() 调用形式更加简洁, 他会自动推导直接根据你给出的参数初始化临时对象
emplace_back 不会触发复制构造和移动构造, 他会直接原地构造一个元素
所以使用 emplace_back 更加简洁效率也更加高


互斥量

std::mutex

mutex 类是保护共享数据, 避免多线程同时访问的同步原语;
mutex 也不能复制, 他的operator=被禁用。

  • lock
    上锁, 若失败则阻塞
  • try_lock
    尝试上锁, 失败则返回
  • unlock
    解锁

使用时注意死锁

std::lock_guard

通常不直接使用 mutex, lock_guard 更加安全, 更加方便。
他简化了 lock/unlock 的写法, lock_guard 在构造时自动锁定互斥量, 而在退出作用域时会析构自动解锁, 保证了上锁解锁的正确操作, 正是典型的 RAII 机制

#include <thread>
#include <mutex>std::mutex myLock;
void func()
{{std::lock_guard<std::mutex> locker(myLock);   //出作用域自动解锁//do some things...}myLock.lock();myLock.unlock();
}int main()
{std::thread t(func);t.join();
}

@zhz: 疑问: std::lock_guard<std::mutex> locker(myLock);这句话是锁定myLock这个互斥量,避免其他线程获取这个互斥量吗? 还是说,只锁住这句话之后的代码块,避免别的线程访问该代码块???

  • 答:是锁住这个互斥量。因为一旦有一个线程的某段代码锁住了这个互斥量,其他线程就获取不了这个锁的权限了。使用同一个互斥量在不同的地方锁住,是因为这几个地方代码肯定会访问同一个变量(或者说共享内存区域),不然不需要使用锁。只有在多线程下才需要使用锁。哪怕只有一个地方使用锁,还是必要的,因为不同线程都在同一片代码块进行写操作时,也需要加锁防止同时在这个地方写造成写数据混乱。

还有一些其他互斥量, 如std::recursive::mutex 是递归型互斥量, 可以让同一线程重复申请等等, 就不一一介绍了


条件变量

条件变量是C++11 提供的一种用于等待的同步机制, 可以阻塞一到多个线程, 直到收到另一个线程发出的通知或者超时, 才会唤醒当前阻塞的线程, 条件变量需要和互斥量配合起来使用

  • std::condition_variable
    该条件变量必须配合 std::unique_lock 使用
  • std::condition_variable_any
    可以和任何带 lock, unlock 的 mutex 配合使用. 他更加通用, 更加灵活, 但是效率比前者差一些, 使用时会有一些额外的开销

这两者具有相同的成员函数

通知

  • notify_one
    唤醒一个阻塞于该条件变量的线程。 如果有多个等待的线程, 并没有会优先唤醒谁的说法。即, 没有唤醒顺序, 是随机的.

  • notify_all
    唤醒所有阻塞于该条件变量的线程

等待

  • wait
    让当前线程阻塞直至条件变量被通知唤醒
  • wait_for
    导致当前线程阻塞直至通知条件变量、超过指定时间长度
  • 下面示例中, wait_for()最后一个参数是预制条件,调用 wait_for的时候,首先就会判断这个条件,
    • 如果这个条件返回false,那么会继续等待;
    • 如果在超时之前,收到了一个notify,那么他会再次执行这个预制条件来进行判断,超时的时候也还会再次执行这个条件,这种可以用在处理队列事件:
    // wait_for的第一个参数是锁,第二个参数是时间长度,第三个参数是lambda表达式
    cond_var.wait_for(lck, std::chrono::seconds(20), [] {std::this_thread::sleep_for(std::chrono::seconds(1));return false;});
    

    原文链接:https://blog.csdn.net/najiutan/article/details/110817106

  • wait_until
    导致当前线程阻塞直至通知条件变量、抵达指定时间点
  • wait_for需要相对时间(“等待长达10秒”),而wait_until需要绝对时间(“等到2012年10月30日12:00”)。
    比较时间参数的声明:

    // wait_for:
    const std::chrono::duration<Rep, Period>& rel_time// wait_until:
    const std::chrono::time_point<Clock, Duration>& abs_time
    

    关于条件变量的详细以及wait_forwait_until用法可参考

    因为虚假唤醒的存在 和 为了避免丢失信号量 (避免丢失信号量就是在调用wait的时候, 在其之前发出的唤醒都不会对wait生效, 而系统不会保存这些条件变量, 调用完就丢掉了),
    我们必须使用循环判断条件变量,所以我们使用条件变量必须结合 mutex ,并且将判断条件放入 while 循环, 而不是使用 if


    std::call_once

    <mutex>中还提供了std::call_once函数,保证某个函数即使在多个线程中同时调用时,也只被调用一次。使用 std::call_once 需要同时使用其帮助结构体 once_flag

    template< class Callable, class... Args >
    void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
    
    • 如果调用call_onceflag已经被设置,说明函数f已经被调用过了,这种情况下call_once直接返回;
    • 如果flag未被设置,则调用call_once时会直接调用std​::​forward<Callable>(f),并向其传递std​::​forward<Args>(args)...参数。如果此时f内抛出了异常,则异常会传递给call_once的调用者,并且不会设置flag,这样可以使得后续使用同一标志调用call_once时能继续调用f函数。
    #include <iostream>
    #include <thread>
    #include <mutex>using namespace std;once_flag onlyOnce;
    mutex myMutex;void func() //线程函数
    {myMutex.lock();cout << "here is func" << endl;myMutex.unlock();call_once(onlyOnce, []{       //仅仅调用一次cout << "hello world!" << endl;});
    }int main()
    {thread t1(func);thread t2(func);thread t3(func);t1.join();t2.join();t3.join();return 0;
    }
    

    这篇博客算是拖了好几个月才写的了, 写一半还没了, 以后写博客记得好好保存…

【多线程】C++11进行多线程开发 (std::thread)相关推荐

  1. C++11 新特性之std::thread

    C++11 新特性之std::thread 原文:https://blog.csdn.net/oyoung_2012/article/details/78958274 从C++11开始,C++标准库已 ...

  2. C++11新特性以及std::thread多线程编程

    一 .C++11新特性 1. auto 类型推导 1.1 当=号右边的表达式是一个引用类型时,auto会把引用抛弃,直接推导出原始类型: 1.2 当=号右边的表达式带有const属性时,auto不会使 ...

  3. 【C/C++开发】C++11 并发指南二(std::thread 详解)

    上一篇博客<C++11 并发指南一(C++11 多线程初探)>中只是提到了 std::thread 的基本用法,并给出了一个最简单的例子,本文将稍微详细地介绍 std::thread 的用 ...

  4. C++11 并发指南二(std::thread 详解)

    上一篇博客<C++11 并发指南一(C++11 多线程初探)>中只是提到了 std::thread 的基本用法,并给出了一个最简单的例子,本文将稍微详细地介绍 std::thread 的用 ...

  5. [C++11 std::thread] 使用C++11 编写 Linux 多线程程序

    From: http://www.ibm.com/developerworks/cn/linux/1412_zhupx_thread/index.html 本文讲述了如何使用 C++11 编写 Lin ...

  6. C++11学习笔记-----线程库std::thread

    在以前,要想在C++程序中使用线程,需要调用操作系统提供的线程库,比如linux下的<pthread.h>.但毕竟是底层的C函数库,没有什么抽象封装可言,仅仅透露着一种简单,暴力美 C++ ...

  7. 【转】C++11 并发指南五(std::condition_variable 详解)

    http://www.cnblogs.com/haippy/p/3252041.html 前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三 ...

  8. C++11 并发指南五(std::condition_variable 详解)

    前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三(std::mutex 详解)>分别介绍了 std::thread,std::mut ...

  9. std::thread与pthread

    线程std::thread与pthread对比 多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序.一般情况下,两种类型的多任务处理:基于进程和基于线程. 基于进程的多 ...

最新文章

  1. 清华博士接亲被要求现场写代码,网友:真是面向对象编程!
  2. 使用Git,显示一个分支中的所有提交,但不显示其他分支中的所有提交
  3. 【最新】2020年4月学术会议变动汇总
  4. 路由器的基本配置--荣新IT培训带给我的......(三)
  5. 重磅!阿里巴巴工程师获得 containerd 社区席位,与社区共建云时代容器标准 1
  6. 我是如何获取新知识的?
  7. apiCloud实现微信分享功能
  8. 学写网页 #05# CSS Mastery 笔记 1~3
  9. 计算机安全群,大开眼界||斯坦福大学信息安全课程群
  10. U-SEM体验模型——让游戏交互设计的维度更加清晰
  11. 数据库原理基本SQL语句练习题及答案1
  12. 微信抢票开发实践总结
  13. Wulihub设计文档类在线托管分享平台
  14. 3词法分析 - 有穷自动机
  15. 修改热血传奇服务器地址,传奇私服如何更改上线地点
  16. uni-app 小程序获取dom信息
  17. linux下的云桌面,云桌面eyeOS之现状
  18. 华硕Z77老主板加持NVMe SSD 成功!
  19. IPV4 跨网段扫描获取IP地址 python
  20. 小米android7.0,华为还是小米?Android7.0国产手机推荐

热门文章

  1. 【Java设计模式】策略模式
  2. 【android-tips】如何在view中取得activity对象
  3. how to query for a listString in jdbctemplate?--转载
  4. Nginx vs Apache--reference
  5. Lesson 13.2 模型拟合度概念介绍与欠拟合模型的结构调整策略
  6. 在conda环境中pip使用清华源秒速安装skimage、opencv、tensorflow、pytorch1.2.0等p
  7. Loss Function view
  8. 让餐厅放心的云服务-雅座CRM技术解密
  9. 每日一博 - 常见的Spring事务失效事务不回滚案例集锦
  10. Spring Cloud【Finchley】-07Feign构造多参数GET/POST请求