1.1 何谓并发

最简单和最基本的并发,是指两个或更多独立的活动同时发生。  (注意区别于计算机中的并发情况!!!!!!!!!!见下面)

1.1.1 计算机系统中的并发:是指在单个系统里同时执行多个独立的任务,而非顺序的进行一些活动。

通过这个任务做一会儿,在切换到别的任务,再做一会儿的方式 ,让任务看起来是并行执行的,这种方式称为“任务的切换”,如今仍然叫做   并发。

应为 任务切换的太快 以至于感觉不到任务在何时会被挂起。 任务切换会给用户造成一种“并发的假象”。因为这种假象,当应用在任务切换的环境下和真

正并发环境下执行相比,行为还是有着微妙的不同。特别是对内存模型不正确的假设,在多线程环境中可能不会出现。

图1.1显示了一个计算机处理两个任务时的理想情景,每个任务被分为10个相等大小的块。在一个双核机器上,每个任务可以在各自的处理上执行。                  在单核机器上做任务切换时,每个任务的块交织进行。但中间有一小段分隔; 为实现交织进行,每次从一个任务切换到另一个时都需要切换一次上下文,切换有时间开销。切换时,操作系统必须为当前运行的任务保存CPU的状态和指令指针,计算出要切换到哪个任务,并为即将切换到的任务重新加载处理器状态。           然后,CPU可能要将新任务的指令和数据的内存载入缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟

图 1.1 并发的两种方式:双核机器的真正并行 Vs. 单核机器的任务切换

图 1.2 四个任务在两个核心之间的切换

图 1.3 一对并发运行的进程之间的通信

图 1.4 同一进程中的一对并发运行的线程之间的通信

清单 1.1 一个简单的Hello, Concurrent World程序:

#include <iostream>
#include <thread> //①
void hello() //②
{
std::cout << "Hello Concurrent World\n";
}
int main()
{
std::thread t(hello); //③
t.join(); //④
}

④这里调用join()的原因 这会导致调用线程(在main()中等待与std::thread对象相关联的线程,即这个例子中的t。

第2章 线程管理

主要内容

  • 启动新线程
  • 等待线程与分离线程
  • 线程唯一标识符

2.1.1 启动线程

1 最简单的情况下,任务也会很简单,通常是无参数无返回(void-returning)的函数。这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。

void do_some_work();
std::thread my_thread(do_some_work);

2 将带有函数调用符类型的实例传入std::thread类中,替换默认的构造函数。

 class background_task{
public:
void operator()() const{ //这个类型重载了运算符()
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);

注意 :

当把函数对象传入到线程构造函数中时,需要避免“最令人头痛的语法解析”(C++’s most vexing parse中文简介)。

如果你传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。

例如:

std::thread my_thread(  background_task()  );

这里相当与声明了一个名为my_thread的函数,这函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数而非启动了一个线程

使用在前面命名函数对象的方式,或使用多组括号①, 或使用新统一的初始化语法② lambda表达式也能避免这个问题  也可以避免这个问题。

如下所示:

std::thread my_thread(  ( background_task() )   );  // 1
std::thread my_thread{ background_task() }; // 2

==============================================================

启动了线程,你需要明确是要等待线程结束(join),还是让其自主运行(detach分离式)。

如果std::thread对象销毁之前还没有做出决定,程序就会终止(std::thread的析构函数会调用std::terminate())。

因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。

清单2.1 函数已经结束,线程依旧访问局部变量

struct func{
int& i;
func( int& i_ ) : i(i_) {}
void operator() (){
for (unsigned j=0 ; j<1000000 ; ++j){
do_something(i); // 1. 潜在访问隐患:悬空引用
}
}
};
void oops(){
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行

例子中,已经决定不等待线程结束(使用了detach()②),所以当oops()函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量如同一个单线程程序——允许在函数完成后继续持有局部变量的指针或引用;这种情况发生时,错误并不明显,会使多线程更容易出错。

2.1.2 等待线程完成

如果需要等待线程,相关的std::thread实例需要使用join()。清单2.1中,将my_thread.detach()替换为my_thread.join(),就可以确保局部变量在线程完成后,才被销毁。

调用join()的行为,还清理了线程相关的存储部分,这样std::thread对象将不再与已经完成的线程有任何关联。这意味着,只能对一个线程使用一次join();一旦已经使用过join(),std::thread对象就不能再次加入了,当对其使用joinable()时,将返回否(false)。

2.1.3 特殊情况下的等待

清单 2.2 等待线程完成

struct func; // 定义在清单2.1中
void f(){
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try{
do_something_in_current_thread();
}
catch(...){
t.join(); // 1
throw;
}
t.join(); // 2
}

清单2.2中  当函数正常退出时,会执行到②处;当函数执行过程中抛出异常,程序会执行到①处。try/catch块能轻易的捕获轻量级错误,所以这种情况,并非放之四海而皆准。如需确保线程在函数之前结束——查看是否因为线程函数使用了局部变量的引用,以及其他原因——而后再确定一下程序可能会退出的途径,无论正常与否,可以提供一个简洁的机制,来做解决这个问题。

一种方式是使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),并且提供一个类,在析构函数中使用join(),如同下面清单中的代码。看它如何简化f()函数。

清单 2.3 使用RAII等待线程完成

class thread_guard
{
std::thread& t; //想想为什么是引用 thread_guard类设计时候!!!!
public:
explicit thread_guard( std::thread& t_ ):t(t_){ }
~thread_guard(){
if(t.joinable()) { //1
t.join(); // 2
}
}
thread_guard(thread_guard const&)=delete; // 3
thread_guard& operator=(thread_guard const&)=delete;
};
struct func; // 定义在清单2.1中
void f(){
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
} // 4

当线程执行到④处时,局部对象就要被逆序销毁了。因此,thread_guard对象g是第一个被销毁的,这时线程在析构函数中被加入到②原始线程中。即使do_something_in_current_thread抛出一个异常,这个销毁依旧会发生。

在thread_guard的析构函数的测试中,首先判断线程是否已加入①,如果没有会调用**join()②进行加入。这很重要,因为join()**只能对给定的对象调用一次,所以对给已加入的线程再次进行加入操作时,将会导致错误。

拷贝构造函数和拷贝赋值操作被标记为=delete③,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。

如果不想等待线程结束,可以分离(detaching)线程,从而避免异常安全(exception-safety)问题。不过,就打破了线程与std::thread对象的联系,即使线程仍然在后台运行着,分离操作也能确保std::terminate()std::thread对象销毁才被调用。

2.2 向线程函数传递参数

清单2.4中,向std::thread构造函数中传递一个参数很简单。需要注意的是,默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。再来看一个例子:

void f(int i, std::string const& s);
std::thread t(f, 3, "hello");

代码创建了一个调用f(3, "hello")的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型。之后,在线程的上下文中完成字面值向std::string对象的转化。需要特别要注意,当指向动态变量的指针作为参数传递给线程的情况,代码如下:

void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024]; // 1
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer); // 2
t.detach();
}

这种情况下,buffer②是一个指针变量,指向本地变量,然后本地变量通过buffer传递到新线程中②。并且,函数有很有可能会在字面值转化成std::string对象之前崩溃(oops),从而导致一些未定义的行为。并且想要依赖隐式转换将字面值转换为函数期待的std::string对象,但因std::thread的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。

解决方案就是在传递到std::thread构造函数之前就将字面值转化为std::string对象:

void f(int i,std::string const& s);
void not_oops(int some_param){
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer) ); // 使用std::string,避免悬垂指针
t.detach();
}

不过,也有成功的情况:复制一个引用。在线程更新数据结构时,会成功的传递一个引用:

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

提供的参数可以"移动"(move),但不能"拷贝"(copy)。"移动"是指:原始对象中的数据转移给另一对象,而转移的这些数据就不再在原始对象中保存了std::unique_ptr就是这样一种类型,这种类型为动态分配的对象提供内存自动管理机制移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象在多个std::unique_ptr实现中传递。使用"移动"转移原对象后,就会留下一个空指针(NULL)。移动操作可以将对象转换成可接受的类型,例如:函数参数或函数返回的类型。当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么转移的时候就需要使用std::move()进行显示移动。下面展示std::move是如何转移一个动态对象到一个线程中去的:

void process_big_object(std::unique_ptr<big_object>);

std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));

std::thread的构造函数中指定std::move(p),big_object对象的所有权就被首先转移到新创建线程的的内部存储中,之后传递给process_big_object函数。

标准线程库中和std::unique_ptr在所属权上有相似语义类型的类有好几种,std::thread为其中之一。虽然,std::thread实例不像std::unique_ptr那样能占有一个动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个执行线程。执行线程的所有权可以在多个std::thread实例中互相转移,这是依赖于std::thread实例的可移动(movable)且不可复制(aren't copyable)性。不可复制保性证了在同一时间点,一个std::thread实例只能关联一个执行线程;可移动性使得程序员可以自己决定,哪个实例拥有实际执行线程的所有权。

2.3 转移线程所有权

假设要写一个在后台启动线程的函数,想通过新线程返回的所有权去调用这个函数,而不是等待线程结束再去调用;或完全与之相反的想法:创建一个线程,并在函数中转移所有权,都必须要等待线程结束。总之,新线程的所有权都需要转移。

这就是移动引入std::thread的原因,C++标准库中有很多资源占有(resource-owning)类型,比如std::ifstream,std::unique_ptr还有std::thread都是可移动(movable),但不可拷贝(not cpoyable)。

void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); //
t1=std::thread(some_other_function);//③
为什么不显式调用std::move()转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用明白了吧!!!!
std::thread t3; // ④ t3使用默认构造方式创建 ④,与任何执行线程都没有关联
t3=std::move(t2); // ⑤ ⑤完成后,t1与执行some_other_function的线程相关联,t2与任何线程都无关联,t3与执行some_function的线程相关联。
t1=std::move(t3); // 6 赋值操作将使程序崩溃

当显式使用std::move()创建t2后  ②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了;执行some_function的函数现在与t2关联。

调用std::move()将与t2关联线程的所有权转移到t3中⑤。因为t2是一个命名对象,需要显式的调用std::move()

最后一个移动操作,将some_function线程的所有权转移⑥给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()终止程序继续运行。终止操作将调用std::thread的析构函数,销毁所有对象。需要在线程对象被析构前,显式的等待线程完成,或者分离它;进行复制时也需要满足这些条件(说明:不能通过赋一个新值给std::thread对象的方式来"丢弃"一个线程)。

c++ 多线程 0的更多相关文章

  1. JAVA思维导图系列:多线程0基础

    感觉自己JAVA基础太差了,又一次看一遍,已思维导图的方式记录下来 多线程0基础 进程 独立性 拥有独立资源 独立的地址 无授权其它进程无法訪问 动态性 与程序的差别是:进程是动态的指令集合,而程序是 ...

  2. Java多线程0:核心理论

    并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系 ...

  3. 【C/C++学院】0724-堆栈简单介绍/静态区/内存完毕篇/多线程

    [送给在路上的程序猿] 对于一个开发人员而言,可以胜任系统中随意一个模块的开发是其核心价值的体现. 对于一个架构师而言,掌握各种语言的优势并能够运用到系统中.由此简化系统的开发.是其架构生涯的第一步. ...

  4. 【多线程】创建线程方式二:实现Runnable接口

    创建线程方式二:实现Runnable接口 代码示例: /** * @Description 实现Runnable接口,重写run方法,执行线程需要丢入Runnable接口实现类,调用start方法 * ...

  5. 【多线程】创建线程方式一:继承Thread类

    创建线程方式一:继承Thread类 代码示例: /** * @Description 继承Thread类,重写run方法,调用start开启线程 * @Author hzx * @Date 2022- ...

  6. Node.js模块 加载笔记

    //核心模块就是Node.js标准API种提供的模块,如fs,http,net.vm等.官方提供,编译成二进制代码//核心模块拥有最高的加载优先级 //文件模块则是存储为单独的文件(或文件夹)的模块, ...

  7. C语言阐述进程和线程的区别

    [cpp]view plain copy /* 每一个程序相当于一个进程,而一个进程之中可以有多个线程 */ #define _CRT_SECURE_NO_WARNINGS #include<s ...

  8. 实用向—总结一些唯一ID生成方式

    在日常的项目开发中,我们经常会遇到需要生成唯一ID的业务场景,不同的业务对唯一ID的生成方式与要求都会不尽相同,一是生成方式多种多样,如UUID.雪花算法.数据库递增等:其次业务要求上也各有不同,有的 ...

  9. python学习-Day37

    目录 今日内容详细 GIL全局解释器锁 GIL与普通互斥锁区别 GIL对程序的影响 验证多线程作用 两个大前提 关于CPU的个数 关于任务的类型 死锁现象 避免死锁的解决: 添加超时释放锁 信号量 自 ...

随机推荐

  1. Apache Thrift with Java Quickstart(thrift入门及Java实例)

    thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发.它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl ...

  2. 10.model/view实例(1)

    1.如图显示一个2x3的表格: 思考: 1.QTableView显示这个表 2.QAbstractTableModel作为模型类. 3.文档中找到subclass的描述 When subclassin ...

  3. 数据结构与算法(Java版)_堆

    完全二叉树叫做堆. 完全二叉树就是最后一个节点之前不允许有不满的节点,就是不允许有空洞. 可以使用数组来做完全二叉树(堆). 堆分为大顶堆和小顶堆.大顶堆就是根节点上的数字是最大的,小顶堆就是根节点上 ...

  4. Python--详解TKinter类库

    为了学习python3.5的tkinter,于是我去官网找了找相关部件的一些文档,读起来有点绕口,觉得还是自己来实践实践,看看视频感觉用处会更大,然后就有了下面的一部分常用的总结, 查看tkinter ...

  5. appium自动化安装(一)

    1.首先去  https://github.com/  了解一下appium一些相关信息 2.安装node.js:node.js官方网站:https://nodejs.org/ 安装完成,打开Wind ...

  6. Django会话,用户和注册之用户认证

    通过session,我们可以在多次浏览器请求中保持数据, 接下来的部分就是用session来处理用户登录了. 当然,不能仅凭用户的一面之词,我们就相信,所以我们需要认证. 当然了,Django 也提供 ...

  7. 【图灵学院01】Java程序员开发效率工具IntelliJ IDEA使用

    1. 什么是IDEA? IDEA, Java智能IDE. 2. 为什么要使用? IDEA的优点: 1)智能选取 2)导航模式 3)历史记录 4)重构 5)编码辅助 6)智能排版,控制 7)智能代码,查 ...

  8. loj #2037. 「SHOI2015」脑洞治疗仪

    #2037. 「SHOI2015」脑洞治疗仪   题目描述 曾经发明了自动刷题机的发明家 SHTSC 又公开了他的新发明:脑洞治疗仪——一种可以治疗他因为发明而日益增大的脑洞的神秘装置. 为了简单起见 ...

  9. s5pv210移植Minigui3.0.12

    移植平台:ubuntu:14.04 开发板:s5pv210(A8) Minigui版本:3.0.12-------------------------------------------------- ...

  10. Could not instantiate bean class [org.springframework.data.mongodb.core.MongoTemplate]

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'repositoryDa ...