0. 引言

本文是多线程技术入门篇,对进程、线程、纤程、并发、并行、线程安全、竞态条件等概念进行了介绍,讨论了多线程技术的实现原理、使用中可能遇到的问题以及如何正确处理。

伴随着硬件和操作系统的进步,现在的计算机能够同时执行多个操作,程序执行更快响应时间缩短。

在软件开发中使用并发既诱人又困难,需要了解计算机底层工作原理。本文是系列的第一篇,从操作系统中线程的基本概念入手,介绍线程背后的魔法。让我们开始吧。

1. 正确认识进程与线程

现在的主流操作系统都支持同时运行多个程序,比如可以一边在浏览器上看这篇文章一边用播放器听歌。运行中的浏览器和播放器程序都是**进程**,操作系统提供了一套机制利用底层硬件保证多个进程同时运行。无论具体采用的是哪种技术,最终让你感觉这些程序是在同时运行。

在操作系统中同时执行多任务,进程不是唯一选项。每个进程都能在内部并发执行子任务,子任务也被称为线程。你可以把线程看成一个进程切片。进程启动时会启动至少一个线程,称为主线程。接下来,根据程序或开发者的需要可以启动新线程或终止线程。多线程即在一个进程中运行多个线程。

例如,播放器会运行多个线程。一个线程显示界面,通常是主线程,另一个线程播放音乐。

可以把操作系统看成进程的容器,而每个进程又是线程的容器。虽然本文只关注线程,但这个主题非常吸引人,值得在未来深入分析。

"1. 操作系统这个盒子里装的是进程,进程又包含了一个或多个线程"

1.1 进程与线程的区别

操作系统会为每个进程分配一块独立的内存。默认情况下,一个进程的内存不能与其他进程共享。例如浏览器不能访问播放器的内存,反之亦然。一个进程启动的多个实例也是如此。启动两次浏览器,操作系统会把每个实例看作一个新进程,并为其分配一块独立的内存。因此,多个进程之间默认是无法共享数据的,除非使用进程间通信(IPC)技术。

与进程不同,线程能够共享父进程的内存。例如播放器中,音乐播放线程可以访问界面线程的数据,反之亦然。因此,线程之间沟通起来更容易。此外,线程占用资源更少,创建速度更快,这就是为什么线程也被称为轻量级进程。

如果没有线程,需要将这些任务作为进程运行并通过操作系统同步。使用 IPC 通信相对困难,而且由于进程比线程更“重”,执行速度也更慢。

1.2 Green Thread 纤程

到目前为止,线程都由操作系统管理,即必须经由操作系统才能启动一个新线程,不过并非所有平台都支持线程。Green Thread 也称为纤程,它模拟线程的工作,可以在不支持本地线程的平台上开发多线程应用。例如,如果操作系统不支持本地线程,虚拟机会实现纤程。

纤程的优点是创建速度快、管理效率高,因为完全绕过了操作系统。但纤程也有缺点,会在下一篇中讨论。

“Green Thread”代指 Sun 公司的 Green Team,他们在20世纪90年代设计了最初的 Java 线程库。今天的 Java 不再使用纤程,早在2000年就已经开始使用本地线程了。一些其他编程语言,比如 Go、Haskell、Rust 等实现了类似纤程的机制代替本地线程。

2. 线程的作用

为什么进程中需要使用多线程?正如之前提到的,并行可以加快处理速度。举个例子,在视频编辑器里渲染一部电影,编辑器会把渲染工作分配给多个线程,每个线程只处理其中一部分工作。假设一个线程需要1小时,那么两个线程只需要30分钟,4个线程缩短到15分钟,以此类推。

真的这么简单?还需要考虑以下三点:

  1. 不是所有程序都要使用多线程。如果程序本身顺序执行或者频繁等待用户输入,这种情况多线程无法发挥作用;
  2. 仅仅增加线程并不会让程序跑得更快,每个子任务都需要经过仔细设计和考虑;
  3. 同样,并不能100%保证并行,一切都依赖于底层硬件。

最后很关键的一点,如果计算机不支持同时执行多个操作,操作系统会让它们看起来像同时执行,稍后会对此进行介绍。现在,让我们把并发(concurrency)理解为多个任务看起来是并行执行的样子,而并行(parallelism)是真正的同时执行。

"2. 并行是并发的子集"

3. 并发与并行的工作原理

CPU 是程序真正执行的地方,它由几个部分组成,其中最主要的部分被称为核心(core)。CPU 核心同时只能运行一个操作。

这是一个明显的缺点。由于这个原因,操作系统设计了各种机制使图形化环境即使在单核设备上也可以支持多进程(多线程)。其中最重要的技术称为抢占式多任务处理,这里的“抢占”指中断当前任务切换到另一个任务,稍后再恢复前一个任务执行的能力。

如果 CPU 只有一个核心,那么操作系统会将这个核心的计算能力分配给多个进程或线程,它们会在循环中一个接一个地执行。这种设计会造成任务在并行执行或者说程序同时在执行多个任务(多线程)的错觉。**满足了并发,但不是真正的并行**,并没有同时运行多个进程。

如今的 CPU 已经不止一个核心,同一时刻每个核心都能独立执行一次操作,这意味着对于多个核心的核心能够真正做到并行。例如,我的 Intel Core i7 有4个核心,可以同时运行4个不同的进程或线程。

操作系统能够检测 CPU 核心的数量,并为每个核心分配进程或线程。只要操作系统喜欢,线程可以分配到任何核心上执行,这个过程对正在运行的程序而言完全透明。不仅如此,如果所有核心都在忙碌中,仍然可以启用抢占式多任务机制。这样可以运行更多的进程和线程,超过实际设备的核心数量。

在单核 CPU 上使用多线程有意义吗?

"单核无法做到真正的并行,但多线程编程是有意义的"。当进程包含了多个线程时,由于抢占式多任务机制,即使其中某个线程执行缓慢或任务阻塞,也可以保持程序运行。

举个例子,运行的桌面程序正从磁盘读数据,读磁盘的过程非常缓慢。如果程序只有一个线程,那么整个程序会出现“无法响应”直到读取结束。CPU 所有计算资源都分配给了唯一的线程,浪费在等待磁盘IO完成上。当然,操作系统还会运行许多其他进程,但这个应用无法继续响应。

让我们用多线程方式设计这个应用。线程 A 访问磁盘,与此同时线程 B 负责主界面。当 A 由于读磁盘操作进入等待,线程 B 仍然可以保持界面能够作出响应。由于这里有两个线程,操作系统可以在它们之间切换,不会在较慢的线程上卡住。

4. 线程越多,问题多多

正如我们知道的那样,线程会共享父进程的内存,这使得应用中的多个线程可以更好地交换数据。例如,视频编辑器进程的内存中包含了时间轴数据,若干工作线程从内存读取、渲染并把结果存储到影片文件中。它们需要一个内存句柄(例如指针)指向对应的内存区域,从而实现读取并把渲染好的帧存入磁盘。

多个线程只从同一块内存读取没有任何问题,但是当其中某个线程执行写入而其它线程正在读取时,问题来了。这时可能出现两个问题:

  • 数据竞争:读线程正在读内存,写线程还没有写入完成,这时会读到损坏的数据;
  • 竞态条件:读线程应该只在写线程完成操作后读取,如果发生相反的情况会怎么样呢?比数据竞争更微妙的,竞态条件可能是多个线程按照不可预知的顺序执行操作,而实际的要求应该按照指定顺序执行。因此,即使做到了数据保护,还是有可能触发竞态条件。

线程安全指什么

一段代码如果声称自己线程安全,那么应该做到在多线程调用时没有数据竞争并且不会触发竞态条件。你可能会注意到一些开发库声称自己是线程安全的,如果正在编写多线程代码,那么就需要确保所有第三方库都能够跨线程使用而且不会引起并发问题。

5. 数据竞争的根源

我们知道一个 CPU 核心一次只能执行一条机器指令,这种指令因为不可分割所以被称为原子操作,即不能拆分成更小的操作。希腊单词“atom”(ἄτομος; atomos)表示不可切分。

不可分割的特性让原子操作天然线程安全。当一个线程写入共享数据时,其他线程在修改完成前无法读取。反过来,当一个线程读共享数据操作具有原子性时,一定能够读到某个时刻的完整数据,不会出现某个线程进入原子操作中的情况,因此不会发生数据竞争。

坏消息是绝大多数的操作都不是原子操作,即使 `x = 1` 这样简单的赋值语句在硬件上也可能由多个机器指令组成,所以说赋值语句本身不是线程安全的。因此如果一个线程读取 `x`,而另一个线程执行赋值操作,就会产生数据竞争。

6. 竞态条件的根源

抢占式多任务机制让操作系统能完全控掌控线程管理:按照调度算法启动、停止和暂停线程执行,程序员无法控制线程执行时间或执行顺序。事实上,并不能保证下面这段简单的代码按照指定顺序启动:

```javawriter_thread.start()reader_thread.start()```

多运行几次就能注意到,每次的运行的结果可能不一样,有时写线程先启动,有时读线程先启动。如果要求程序确保在读取之前进行写入,那么肯定会触发竞态条件。

这种行为被称不非确定性:每次结果都会改变,无法预测。调试带有竞态条件的程序非常麻烦,因为不能以可控的方式复现问题。

7. 教线程和谐相处:并发控制

数据竞争和竞态条件都是现实中会出现的问题,有人甚至因此丧生。并发控制是一种多线程并发的艺术,操作系统和编程语言为此提供了一些解决方案,其中最重要的几项有:

  • 同步:确保一次仅有一个线程使用资源。同步是指将代码的特定部分标记为“protected”,这样多个并发线程就不会同时执行这段代码,也不会让共享数据变得混乱;
  • 原子操作:由操作系统提供特殊指令,一些非原子操作,比如前面提到的赋值操作,可以转化为原子操作。这样,无论其他线程如何访问,共享数据始终保持有效;
  • 不可变数据:当共享数据被标记为不可变时,只允许线程从中读取数据,没有任何方式可以改变它的内容,从而解决了竞态条件的根本原因。众所周知,只要不修改内存地址,线程就可以安全地从相同的位置读取数据。这也是函数式编程背后的哲学。

在系列的下一篇中,我们将讨论这些有趣的并发话题,敬请期待!

举两个例子,怎么样写好代码

最经典的算法,献给正在面试道路上的你

winform 线程 句柄不断增加_多线程讲解相关推荐

  1. java 线程 单例_多线程单例模式

    多线程单例模式 原文:https://blog.csdn.net/u011726005/article/details/82356538 1. 饿汉模式 使用饿汉模式实现单例是十分简单的,并且有效避免 ...

  2. java线程条件变量_多线程同步条件变量(转载)

    最近看<UNIX环境高级编程>多线程同步,看到他举例说条件变量pthread_cond_t怎么用,愣是没有看懂,只好在网上找了份代码,跑了跑,才弄明白 #include #include ...

  3. java线程锁机制_多线程之锁机制

    前言 在Java并发编程实战,会经常遇到多个线程访问同一个资源的情况,这个时候就需要维护数据的一致性,否则会出现各种数据错误,其中一种同步方式就是利用Synchronized关键字执行锁机制,锁机制是 ...

  4. burp爆破线程设置多少_多线程到底需要设置多少个线程?

    我们在使用线程池的时候,会有两个疑问点: 线程池的线程数量设置过多会导致线程竞争激烈 如果线程数量设置过少的话,还会导致系统无法充分利用计算机资源 那么如何设置才不会影响系统性能呢?其实线程池的设置是 ...

  5. 两个子线程不冲突_多线程操作可见性

    可见性:让一个线程对共享变量的修改,能够及时的被其他线程看到,java中首先想到的就是volatile volatile关键字 Java内存模型中规定: 1.对某个volatile 字段的写操作hap ...

  6. java线程顺序输出_多线程按顺序输出ABC

    /** * @author mengwen E-mail: meng_wen@126.com * @date 创建时间:2016年8月30日 下午3:10:32 * @version 1.0 * @p ...

  7. python以追加方式打开文件 线程安全吗_多线程追加文件,不加锁,会出现什么情况 ?...

    无定义. 顺便用这个问题来回答一下这个问题(linux/sem.h和sys/sem.h有什么区别? - in nek 的回答)下有人问我的问题:什么叫基于语义来进行编程? 其实这句话完整说应该是:要基 ...

  8. python 串行线程终止后还会执行下一个吗_多线程笔记

    1.线程的状态 狂神说多线程讲解 休眠方法: public static void sleep(long millis) 放弃方法:放弃对时间片的使用 public static void yield ...

  9. python爬虫进程和线程的区别_熬了两个通宵写的!终于把多线程和多进程彻底讲明白了!...

    我们知道,在一台计算机中,我们可以同时打开许多软件,比如同时浏览网页.听音乐.打字等等,看似非常正常.但仔细想想,为什么计算机可以做到这么多软件同时运行呢?这就涉及到计算机中的两个重要概念:多进程和多 ...

  10. call线程起名字_多线程面试题总结

    1.什么是线程,什么是进程,它们有什么区别和联系,一个进程里面是否必须有个线程 进程本质是一个正在执行的程序,一个进程可以有多个线程.线程是进程的最小执行单位,一个进程至少有一个线程 区别:1:多进程 ...

最新文章

  1. cheatengine找不到数值_找商网:百度爱采购与其他B2B平台有何不同,为何能够后来居上?...
  2. rfc垮端口 sap_SAP扫盲系列之二:SAP ABAP应用服务器的组成部分
  3. Docker容器相关命令
  4. SPI初始化C语言编程,SD卡spi模式读写,初始化和复位都成功了
  5. 爱情八十二课,爱情三国杀
  6. 安卓app开发工具_怎么开发app软件需要多少钱?主流app开发工具盘点
  7. python 用元类 type 实现对数据库的ORM 映射
  8. 实验 使用 vivado zedboard GPIO 开关 开控制 LED
  9. T-SQL with关键字
  10. NameNode之文件系统目录树
  11. 华为智慧屏鸿蒙系统怎么样,鸿蒙系统初体验,华为智慧屏V65到底值不值得入手?...
  12. 2019.11.27 阵列信号处理
  13. cosx的麦克劳林级数是多少_余弦函数的泰勒级数
  14. chm文档制作 java_自己动手制作chm格式开源文档
  15. 动感校园行17951长途ip电话卡
  16. 骆昊python100天 github_GitHub - zsfz/Python-100-Days: Python - 100天从新手到大师
  17. python九宫格拼图_Python制作九宫格图片
  18. 第三方软件测试z5x电池,5000mAh的vivo Z5x能用多久?三大续航测试摧残,表现出乎意料...
  19. 水星UD6S网卡Linux驱动,水星UD6S驱动|水星UD6S无线网卡驱动下载 v1.0 官方版 - 比克尔下载...
  20. 怎么储存才能让CPU永久性使用导热硅脂

热门文章

  1. JAVA Eclipse创建Android程序界面不显示怎么办
  2. java对象创建、对象内存布局、对象的访问定位、句柄池、直接指针
  3. svg转换pdf用php实现,如何使用javascript在JSPDF中将SVG文件转换为PDF
  4. SpringCloud常见面试题总结一
  5. 面向对象编程设计练习题(2)
  6. Flink中task之间的数据交换机制
  7. mysql 5.6 在线DDL
  8. mybatis动态sql片段与分页,排序,传参的使用与一对多映射与resultMap使用
  9. 《Adobe Audition CC经典教程》——1.5 使用外部连接器
  10. EF Code First学习笔记 初识Code First