多进程并发:将应用程序分为多个独立的进程,它们在同一时刻运行。如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、。文件、管道等等)。

优点:1.操作系统在进程间提供附附加的保护操作和更高级别的通信机制,意味着可以编写更安全的并发代码。

2. 可以使用远程连接的方式,在不同的机器上运行独立的进程,虽然增加了通信成本,但在设计精良的系统数上,这可能是一个提高并行可用性和性能的低成本方法。

缺点:1. 这种进程间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以免一个进程去修改另一个进程的数据

      2. 运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程

多线程并发:在单个进程中运行多个线程。线程就是轻量级的进程:每个线程独立运行,且线程可以在不同的指令序列中运行。但是进程中的所有线程都共享地址空间,并且所有线程所访问的大部分数据---全局变量仍然是全局的,指针 对象的引用或者数据可以在线程之间传递。

优点:地址空间共此昂,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多线程的开销远远小于使用多进程

缺点:共享内存的灵活性的代价:如果数据要被多个线程访问,那么程序员必须宝成每个线程访问的数据是一致的,这意味着需要对线程间的通信做大量的工作

并发与并行:

并行更加注重性能。在讨论使用当前可用硬件来提高批量数据处理的速度时,我们会讨论程序的并行性;当关注的重点在于任务分离或任务响应时,就会讨论到程序的并发性

std::thread 学习

需要包含的头文件 <thread>

初始化线程(启动线程)

一个简单的例子:

#include <iostream>
#include <thread> using namespace std; void sayHello()
{
cout << "hello" <<endl;
} int main()
{
thread t(sayHello);
t.join();
}

初始化线程(启动线程)就是构造一个std::thread 的实例: std::thread(func)。 func 不简单的指函数,它是一个函数调用符类型,如下例子:

#include <iostream>
#include <thread> using namespace std; class Test
{
public:
void operator()()
{
cout << "hello" <<endl;
}
}; int main()
{
Test test;
thread t(test);
t.join();
}

也可以使用类的成员变量来初始化std::thread:  std::thread(&Class::func, (Class)object)

#include <iostream>
#include <thread> using namespace std; class Test
{
public:
void sayHello()
{
cout << "hello" <<endl;
}
}; int main()
{
Test test;
thread t(&Test::sayHello, &test);
t.join();
}

注意:把函数对象传入到线程构造函数中时,需要避免以下情况: 如果你传递了一个临时变量,而不是一个命名的变量,c++的编译器会将其解释为函数声明,而不是类型对象的定义。例如:

std::thread myThread(func());

这里相当于声明了一个名为myThread的函数,这个函数带一个参数(函数指针指向一个没有参数并且返回func对象的函数),返回一个std::thread对象的函数,而不是启动了一个线程。

要解决这个问题,解决方法:

  • 使用多组括号
std::thread myThread((func()));
  • 使用大括号
std::thread myThread({func()});
  • 使用lambda表达式
std::thread myThread([](){
do_something();
});

启动线程后,需要明确是要等待线程结束(加入式)还是让其自主运行(分离式),如果在对象销毁之前还没做出决定,程序就会终止(std::thread的析构函数会调用std::terminate())。即使有异常情况也要保证线程能够正确的加入(join)或者分离(detached)。

如果不等待线程,就要保证线程结束之前,可访问的数据的有效性。例如主线程往子线程中传了一个变量A的引用,子线程detach,则表示主线程可能在子线程之前结束,这样变量A便会被销毁,这时子线程再使用A的引用就会产生异常。处理这种情况的常规方法:使线程的功能齐全,将数据复制到线程中,而非复制到共享数据中。如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁,但对于对象中包含的指针和引用还需谨慎。最好不要使用一个访问局部变量的函数去创建线程。此外,可以通过join()函数来确保线程在函数完成前结束。

等待线程完成

使用std::thread 方法中的join()来实现等待线程完成

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

后台运行线程

使用std::thread 方法中的detach()来实现等待线程完成

使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有std::thread对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。当std::thread对象使用t.joinable()返回的是true,才可以使用t.detach()

向线程函数传递参数

在初始化的时候可以进行参数传递 std::thread myThread(func, arg0, arg1,...),另外在使用类的成员函数来初始化线程时的参数传递std::thread(&Class::func, (Class)object, arg0, arg1,...)

在这里需要注意两个问题:

1. 在将指向动态变量的指针作为参数传给线程的时候,想要依赖隐式转换将字面值转换为函数期待的对象(1),但是std::thread的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。解决方法是:(2)在传入之前先显示的进行转换

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

2. 期望传入一个引用,但整个对象被复制了。虽然期望传入一个引用类型的参数(1),但std::thread的构造函数并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。

解决方法是使用std::ref()来将参数转换为引用的形式(2)

void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
widget_data data;
//std::thread t(update_data_for_widget,w,data); //
std::thread t(update_data_for_widget,w,std::ref(data)); // 2
display_status();
t.join();
process_widget_data(data);
}

转移线程所有权 

首先要明确的是对于std::thread,不能将一个对象赋值给另一个对象,即赋值构造函数是被删除的。

thread(thread&) = delete;

但是我们有时需要转移线程的所有权,这时候就需要使用std::move()来实现

void some_function();
void some_other_function();
std::thread t1(some_function); //
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3 隐式移动操作
std::thread t3; //
t3=std::move(t2); //
t1=std::move(t3); // 6 赋值操作将使程序崩溃

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

std::thread 支持移动操作,意味着它可以当做函数的返回值和参数

//作为函数返回值
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,);
return t;
} //作为函数的参数
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}

运行时决定线程数量

std::thread::hardware_concurrency() 这个函数会返回能并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。

标识线程

线程标识类型为std::thread::id,获取方法有两种:

1. 调用std::thread对象的成员函数get_id()来直接获取,如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值,这个值表示“无线程”

2. 当前线程中调用std::this_thread::get_id()(这个函数定义在<thread>头文件中)也可以获得线程标识。

如果两个对象的std::thread::id相等,那它们就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。

std::thread::id 有丰富的比较方法,因此它可以当做容器的键值,做排序等等比较。标准库也提供std::hash<std::thread::id>容器,所以std::thread::id也可以作为无序容器的键值。

参考资料:

https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/

c++多线程并发学习笔记(0)的更多相关文章

  1. c++多线程并发学习笔记(1)

    共享数据带来的问题:条件竞争 避免恶性条件竞争的方法: 1. 对数据结构采用某种保护机制,确保只有进行修改的线程才能看到修改时的中间状态.从其他访问线程的角度来看,修改不是已经完成了,就是还没开始. ...

  2. c++多线程并发学习笔记(2)

    等待一个时间或其他条件 在一个线程等待完成任务时,会有很多选择: 1. 它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一个线程完成工作时对这个标志进行重设.缺点:资源浪费,开销大 2. ...

  3. 多线程编程学习笔记——使用异步IO(一)

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  4. 多线程编程学习笔记——使用异步IO

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  5. Java多线程技术学习笔记(二)

    目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...

  6. 多线程编程学习笔记——async和await(一)

    接上文 多线程编程学习笔记——任务并行库(一) 接上文 多线程编程学习笔记——任务并行库(二) 接上文 多线程编程学习笔记——任务并行库(三) 接上文 多线程编程学习笔记——任务并行库(四) 通过前面 ...

  7. 多线程编程学习笔记——async和await(二)

    接上文 多线程编程学习笔记——async和await(一) 三.   对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...

  8. 多线程编程学习笔记——async和await(三)

    接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五.   处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...

  9. 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端

    接上文 多线程编程学习笔记——使用异步IO 二.   编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...

随机推荐

  1. kafka消费者示范代码(Java)

    1.将kafka里lib目录下(除jar包外还有别的东西)所有的jar包导入工程中. 2.代码 public static void main(String[] args) { //声明连接属性 Pr ...

  2. Nginx模块开发实验

    工作原理: 当nginx接到 一个http请求之后,会找通过查找配置文件,并在配置文件中找到相应的地址映射,该地址也叫location block,而location中配置的文件会启动 相应的bloc ...

  3. linux-文件系统-5

    cat /proc/partions cat /proc/mounts mount [options] -o [option] -t 文件类型 设备 挂载目录 设备: (1)设备文件:例如/dev/s ...

  4. win10 搜索框输入没提示

    1.点击win, 手动在应用里找到Cortana(小娜) 2. 点右键->更多->应用设置,进入到下面的界面 3. 下拉到最下面,找到“重置”即可

  5. html applet标签 语法

    html applet标签 语法 作用:定义嵌入的 applet. 说明:某些浏览器中依然存在对 <applet> 但是需要额外的插件和安装过程才能起作用.大理石机械构件 注释:HTML5 ...

  6. 关于win7系统下gitbook的安装与gitbook editor的配合使用

    1.安装nodejs 2.node -v,可查看node版本: npm -v,可查看npm版本 3.npm install gitbook-cli -g,安装gitbook 此过程经常报错,如果报错, ...

  7. Linux基本命令使用(三)

    1.压缩解压命令:gzip,   .gz格式的 gzip 文件名     就压缩了. Linux压缩的放到Windows下可以解压,但是Windows下压缩到Linux解压就不一定可以. (1)只能压 ...

  8. HDU 2147--HDU 2147(博弈)--我可能做了假题。。。

    kiki's game Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 40000/1000 K (Java/Others) Total Su ...

  9. java jdk原生的http请求工具类

    package com.base; import java.io.IOException; import java.io.InputStream; import java.io.InputStream ...

  10. express中redirect传递数据

    redirect中无法跟render一样传递数据 在index中,可以通过session重定向到login 在login.js 中获取req.session,渲染到login.ejs中,最后js获取