多线程基础知识了解一下

(一) 前言

作为一名优秀的攻城师,了解多线程的知识非常有必要,尤其在人工智能和机器学习的热潮下,如何提高程序或者算法的运行效率是非常有价值的一件事情。

在当代大多数的操作系统,都有能力同时的运行多个程序或者app,比如在windows上你可以同时打开多个QQ,多个不同的浏览器,多个不同的视频播放器。或者在你的苹果或者安卓手机上边听歌边使用地图功能,这背后其实用的就是用的是多线程的技术。

image

(二)多任务处理

在同一时刻运行多个程序也叫做多任务处理,每个程序会由一个单独的task来执行,每个task运行在一个单独的处理器中(可以理解为是一个进程或者一个CPU)。在早期的计算机操作系统中,往往都是单处理器的,这时候你也能同时运行多个程序,这种情况我们称为并发而不是并行,因为这个时候多个程序其实是共用的一个CPU时钟,由于计算机的时间片切换非常快,所以大多数时候你是感觉不到这种差异的,但其实是一种假象。随着社会的进步,现在的电脑基本上都是多个CPU的,所以在多个CPU的情况下,程序才能够真正的并行起来。

(三)线程与多线程

每个处理器可以创建多个子任务,这里的每一个子任务都是一个线程。一个线程执行的其实就是一段代码指令序列。在一个处理器内的多个线程是可以通过处理器的共享内存进行交互的,关键词并发(concurrency)指的就是在一个处理器内同时执行多个线程。

多线程通常是通过把大的任务切分成多个子任务运行,以此来提高程序运行效率的。比较典型的例子就是现实中修一段高速公路时,最快的方法就是,把这条公路切分成多段,然后每个段由一个工程队负责,这样同时工作就能大大提高效率。

(四)并发与线程上下文切换

上面说过,单个处理器内的多线程任务其实是一种假象,其实是通过切换CPU时钟实现的,这个时候,在切到另一个线程之前,CPU必须保存当前线程的状态,这被称为上下文切换,这也是单核处理器能同时执行多个任务的秘密。

线程切换是一个比较昂贵的操作,调度器需要花费额外的CPU时间来临时暂停当前活跃的线程为了让另外一个线程运行,然后保存当前线程状态,在需要的时候,还得恢复当前挂起的线程状态。

image

(五)线程调度

线程调度主要负责线程的上下文切换,它决定了接下来要选择哪个挂起的线程执行。线程调度是操作系统的一部分。

(六)互斥

互斥的目的是保证在两个线程之间不能同时执行同一个代码片段。通俗点来说就是我们在大街上看到的红绿灯,任何时候只能有一种颜色的灯在亮。 互斥的资源通常是需要被共享的,比如卫生间的马桶,任何时候只能有一个人用,如果同时有多个人用那么就会出现问题,这也叫竞争,反映到程序中,可能是一种数据解构,一个外部设备如打印机,或者一个网络连接。

竞争通常会带来问题,所以在程序中通常使用锁机制(lock)来达到互斥的目的,互斥也可以称为线程同步(synchronization)

同步带来的缺点是,在一个线程没有释放锁之前,另外一个线程需要一直等待。它强制调度是串行操作的,即使这里有多个空闲的CPU资源,所以在日常开发中要合理使用。

(七)并发与并行

并行:

并行指的是多线程运行在不同的CPU或者处理器上,从而避免了在同一个CPU或者处理器中的上下文切换的操作。当然这里是多个线程之间不需要通信或者有共享资源需要访问。这种情况就可以独立的执行和计算。当然前提是硬件有多个CPU或者处理器。

并发:

并发指的是多个线程有通信或者需要访问共享的数据,这个时候需要考虑加锁,否则有可能安全问题。通常情况下并发是指运行在同一个CPU或者core内,但这并不是十分准确,多个线程也可以运行在多个CPU内但是他们有合理的同步策略。

image

(八)多处理器 vs 多core vs 超线程

多处理器是指在单台电脑上有多个CPU单元,每一个处理器可以有多个core,每个core可以运行一个任务,多线程程序每个线程都可以并行的运行在一个core中。

注意单个core也有可能运行两个并行的线程,这种能力被称为超线程。

超线程(HT, Hyper-Threading)[1]是英特尔研发的一种技术,于2002年发布。超线程技术原先只应用于Xeon 处理器中,当时称为“Super-Threading”。之后陆续应用在Pentium 4 HT中。早期代号为Jackson。
通过此技术,英特尔实现在一个实体CPU中,提供两个逻辑线程。之后的Pentium D纵使不支持超线程技术,但就集成了两个实体核心,所以仍会见到两个线程。超线程的未来发展,是提升处理器的逻辑线程。英特尔于2016年发布的Core i7-6950X便是将10核心的处理器,加上超线程技术,使之成为20个逻辑线程的产品。

超线程其实是一个CPU单元内,提供了两个逻辑线程,依赖于底层操作系统,如果操作系统不支持,也可以禁用掉。因此在一个4 core 处理器系统中可能有8个逻辑处理器。

(九)线程 与 CPU缓存

依赖于CPU的类型,当前的操作系统基本都支持三级缓存,CPU缓存的目的是为了CPU访问CPU缓存数据更快,这种快是相对于CPU读取内存数据而言(RAM),通常情况下一般高出几个数量级。

L1 级别缓存 在cpu的芯片中,体积一般是8-64kb

L2 级别缓存 通常位于CPU和RAM之间,体积一般是2-4MB

L3 级别缓存 如果存在一般都位于主板上,体积一般是8-16MB (注:跟CPU类型有关,一些CPU类型可能直接用L2替代L3了)

image

下面通过表格看下不同的介质,访问的耗时情况,其中L1缓存属于core级别的,所以每个运行在core里面的线程都可以拥有自己的local cache。

从CPU到 大约需要的CPU周期 大约需要的时间(单位ns)
寄存器 1 cycle 可以忽略
L1 Cache ~3-4 cycles ~0.5-1 ns
L2 Cache ~3-4 cycles ~0.5-1 ns
L3 Cache ~3-4 cycles ~0.5-1 ns
跨槽 ~30-40 cycles ~20 ns
内存 ~120-240 cycles ~60-120ns

(十) 总结

本篇主要介绍了多线程有关的一些基础概念以及CPU的cache模型,在一个多线程的程序中,为了提高处理性能,每个线程都有自己的CPU缓存,而同时如果多个线程想要访问一块共享的区域(位于主内存中),需要考虑同步和可见性的问题,,所以一些编程语言如C,C++,C#和Java都会有确保变量在修改之后对其他线程可见的语义,如Java里面的volatile关键词会强制flush线程的local cache的数据到主存中,除此之外一些锁机制也会触发,如lock和unlock指令,这些知识点会在后面的文章中一一介绍。

参考文章:

https://www.logicbig.com/quick-info/programming/multi-threading.html#processor-cache

https://medium.com/@bkodirov/threading-in-java-55ec2e184fe7

image

Top