什么是并发

并发在生活中随处可见,边走路边说话,边听歌边写代码。计算机术语中的"并发",指的是在单个系统里同时执行多个独立的活动,而不是顺序的一个接一个的执行。对于单核CPU来说,在某个时刻只可能处理一个任务,但它却不是完全执行完一个任务再执行一个下一任务,而是一直在任务间切换,每个任务完成一点就去执行下一个任务,看起来就像任务在并行发生,虽然不是严格的同时执行多个任务,但是我们仍然称之为并发(concurrency)。真正的并发是在在多核CPU上,能够真正的同时执行多个任务,称为硬件并发(hardware concurrency)

并发并非没有代价,在单核CPU并发执行两个任务需要付出上下文切换的时间代价。如下图:

双核机器的并行执行与单核机器对比.png

假设A和B两个任务都被分成10个大小相等的块,单核CPU交替的执行两个任务,每次执行其中一块,其花费的时间并不是先完成A任务再玩成B任务所花费时间的两倍,而是要更多。这是因为系统从一个任务切换到另一个任务需要执行一次上下文切换,这是需要时间的(图中的灰色块)。上下文切换需要操作系统为当前运行的任务保存CPU的状态和指令指针,算出要切换到哪个任务,并为要切换的任务重新加载处理器状态。然后将新任务的指令和数据载入到缓存中。

并发的方式

多进程并发

将应用程序分为多个独立的、单线程的进程,他们可以同时运行。进程内部实现原理比较复杂,这里就不多说了。

并发运行的进程之间的通信.png

这些独立的进程可以通过常规的进程间通信机制进行通信,如管道、信号、消息队列、共享内存、存储映射I/O、信号量、套接字等。

缺点:

  • 进程间通信较为复杂,速度相对线程间的通信更慢。
  • 启动进程的开销比线程大,使用的系统资源也更多。

优点:

  • 进程间通信的机制相对于线程更加安全。
  • 能够很容易的将一台机器上的多进程程序部署在不同的机器上(如果通信机制选取的是套接字的话)。

多线程并发

线程很像轻量级的进程,但是一个进程中的所有线程都共享相同的地址空间,线程间的大部分数据都可以共享。线程间的通信一般都通过共享内存来实现。

并发运行的线程之间的通信.png

优点:

  • 由于可以共享数据,多线程间的通信开销比进程小的多。
  • 线程启动的比进程快,占用的资源更少。

缺点:

  • 共享数据太过于灵活,为了维护正确的共享,代码写起来比较复杂。
  • 无法部署在分布式系统上。

为什么使用并发

主要原因有两个:任务拆分和提高性能。

任务拆分

在编写软件的时候,将相关的代码放在一起,将无关的代码分开,这是一个好主意,这样能够让程序更加容易理解和测试。将程序划分成不同的任务,每个线程执行一个任务或者多个任务,可以将整个程序的逻辑变得更加简单。

提高性能

在两种情况下,并发能够提高性能。

  1. 任务并行(task parallelism):将一个单个任务分成若干个部分各自并行运行,从而降低运行时间。虽然听起来很简单,但其实是一个相当复杂的过程,设想假如各个部分之间存在很多以来,一个部分的执行需要使用到另一个任务的执行结果,这个时候并不能很好的并行完成。
  2. 数据并行(data parallelism):每个线程在不同的数据部分上执行相同的操作。

多线程库简介

C++98标准中并没有线程库的存在,而在C++11中终于提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。

多线程库对应的头文件是#include <thread>,类名为std::thread

一个简单的串行程序如下:

#include <iostream>
#include <thread>void function_1() {std::cout << "I'm function_1()" << std::endl;
}int main() {function_1();return 0;
}

这是一个典型的单线程的单进程程序,任何程序都是一个进程,main()函数就是其中的主线程,单个线程都是顺序执行。

将上面的程序改造成多线程程序其实很简单,让function_1()函数在另外的线程中执行:

#include <iostream>
#include <thread>void function_1() {std::cout << "I'm function_1()" << std::endl;
}int main() {std::thread t1(function_1);// do other thingst1.join();return 0;
}

分析:

  1. 首先,构建一个std::thread对象t1,构造的时候传递了一个参数,这个参数是一个函数,这个函数就是这个线程的入口函数,函数执行完了,整个线程也就执行完了。
  2. 线程创建成功后,就会立即启动,并没有一个类似start的函数来显式的启动线程。
  3. 一旦线程开始运行, 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。注意:只需要在std::thread对象被销毁之前做出这个决定。这个例子中,对象t1是栈上变量,在main函数执行结束后就会被销毁,所以需要在main函数结束之前做决定。
  4. 这个例子中选择了使用t1.join(),主线程会一直阻塞着,直到子线程完成,join()函数的另一个任务是回收该线程中使用的资源。

线程对象和对象内部管理的线程的生命周期并不一样,如果线程执行的快,可能内部的线程已经结束了,但是线程对象还活着,也有可能线程对象已经被析构了,内部的线程还在运行。

假设t1线程是一个执行的很慢的线程,主线程并不想等待子线程结束就想结束整个任务,直接删掉t1.join()是不行的,程序会被终止(析构t1的时候会调用std::terminate,程序会打印terminate called without an active exception)。

与之对应,我们可以调用t1.detach(),从而将t1线程放在后台运行,所有权和控制权被转交给C++运行时库,以确保与线程相关联的资源在线程退出后能被正确的回收。参考UNIX守护进程(daemon process)的概念,这种被分离的线程被称为守护线程(daemon threads)。线程被分离之后,即使该线程对象被析构了,线程还是能够在后台运行,只是由于对象被析构了,主线程不能够通过对象名与这个线程进行通信。例如:

#include <iostream>
#include <thread>void function_1() {//延时500ms 为了保证test()运行结束之后才打印std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cout << "I'm function_1()" << std::endl;
}void test() {std::thread t1(function_1);t1.detach();// t1.join();std::cout << "test() finished" << std::endl;
}int main() {test();//让主线程晚于子线程结束std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //延时1sreturn 0;
}// 使用 t1.detach()时
// test() finished
// I'm function_1()// 使用 t1.join()时
// I'm function_1()
// test() finished

分析:

  1. 由于线程入口函数内部有个500ms的延时,所以在还没有打印的时候,test()已经执行完成了,t1已经被析构了,但是它负责的那个线程还是能够运行,这就是detach()的作用。
  2. 如果去掉main函数中的1s延时,会发现什么都没有打印,因为主线程执行的太快,整个程序已经结束了,那个后台线程被C++运行时库回收了。
  3. 如果将t1.detach()换成t1.join()test函数会在t1线程执行结束之后,才会执行结束。

一旦一个线程被分离了,就不能够再被join了。如果非要调用,程序就会崩溃,可以使用joinable()函数判断一个线程对象能否调用join()

void test() {std::thread t1(function_1);t1.detach();if(t1.joinable())t1.join();assert(!t1.joinable());
}

参考

  1. C++并发编程实战
  2. C++ Threading #1: Introduction

c++11 多线程编程(一)------初始相关推荐

  1. Linux与C++11多线程编程(学习笔记)

    多线程编程与资源同步 在Windows下,主线程退出后,子线程也会被关闭; 在Linux下,主线程退出后,系统不会关闭子线程,这样就产生了僵尸进程 3.2.1创建线程 Linux 线程的创建 #inc ...

  2. c++11多线程编程同步——使用条件变量condition variable

    简述 在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量. c++11提供的 condition_variable 类是一个同步原语, ...

  3. c++11 多线程编程(六)------条件变量(Condition Variable)

    互斥锁std::mutex是一种最常见的线程间同步的手段,但是在有些情况下不太高效. 假设想实现一个简单的消费者生产者模型,一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中 ...

  4. c++11 多线程编程(三)------ 竞争和互斥锁

    竞争条件 并发代码中最常见的错误之一就是竞争条件(race condition).而其中最常见的就是数据竞争(data race),从整体上来看,所有线程之间共享数据的问题,都是修改数据导致的,如果所 ...

  5. c++11 多线程编程(二)------ 线程类构造函数深入理解

    构造函数的参数 std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数. 第一参数的类型并不是c语言中的 ...

  6. c++11多线程编程(一):创建线程的三种方法

    c++11线程库 原始的c++标准仅支持单线程编程,新的c++标准(c++11或c++0x)于2011年发布,引入了新的线程库. 编译器要求 Linux: gcc 4.8.1 (完全并发支持) Win ...

  7. C++11多线程编程-两个进程轮流打印1~100

    这是经典的同步互斥问题, 遵循原则: 1.条件变量需要锁的保护: 2.锁需要条件变量成立后,后重新上锁: 参考代码: //notify_one()(随机唤醒一个等待的线程) //notify_all( ...

  8. c++11 多线程编程(五)------unique_lock

    互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁. 这一点lock_guard做的不好,不够灵活,lock_guar ...

  9. c++11 多线程编程(四)------ 死锁(Dead Lock)

    死锁 如果你将某个mutex上锁了,却一直不释放,另一个线程访问该锁保护的资源的时候,就会发生死锁,这种情况下使用lock_guard可以保证析构的时候能够释放锁,然而,当一个操作需要使用两个互斥元的 ...

最新文章

  1. 网站转化率做不好多半是这三大因素产生干扰
  2. 简单灵活的 PHP页面跳转函数
  3. bashrc与profile的相关知识
  4. python docx 合并文档 图片_Python+pymupdf处理PDF文档案例6则
  5. MySQL checkpoint机制详解
  6. 【原创】cocos2d-x3.9蓝牙开发之蓝牙开启
  7. 2021-01-08随感
  8. Hibernate→ORM、简介、第一个Hibernate实现、核心XML配置、Hibernate执行流程、操作数据库对象session、事务、映射XML配置、单例CRUD、get与load
  9. 可任意设置时间的ppt倒计时软件
  10. 微信公众号 菜单 { “errcode“: 47001, “errmsg“: “data format error rid: 61b36b ...“}
  11. 【css】各个字号大小对照表
  12. 【建议收藏】手把手教你画一个项目的技术架构图
  13. 针对商品标题冗长问题,阿里工程师怎么解决?
  14. 继电器控制电路原理解析说明
  15. U3D 场景快速搭建教程
  16. Linux基础(维护基本存储空间)
  17. ppt保存类型中无html,ppt保存类型里没有pdf怎么办?这样做即可!
  18. 匿名管道、命名管道--Linux
  19. 2022-2028年中国嵌入式系统行业市场专项调研及竞争战略分析报告
  20. 10万字C语言入门手册,历时三个月,详细的C语言教程终于出炉了,给你全新C语言入门体验

热门文章

  1. [C++] Vector对象的合法定义
  2. FAILED Execution Error, return code 2 from org
  3. 爬虫项目——BS练手(2)
  4. 大数据分析技术趋势有哪些
  5. Python内置四大数据结构之字典的介绍及实践案例
  6. Flutter 环境搭建 与 Android 应用的调试
  7. Python高级编专题 - 类的创建与销毁
  8. javaweb 获取服务器整个文件夹下的文件_详细得不要不要的 JavaWeb快速入门,值得收藏!...
  9. go和python对比的优缺点_Python与Golang对比
  10. AcWing 4243. 传递信息(单源最短路)