并发与多线程【一】——多线程基础知识

引言

操作系统知识对于服务问题的排查定位十分重要,下面重点说一下进程与线程

概念

进程

进程是计算机科学中最重要和最成功的的概念之一,是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中个,需要运行的进程数是多余可以运行他们的CPU个数的。传统系统在一个时刻只能执行一个程序,而现今的多核处理器同时能够执行多个程序。无论是在单核还是在多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制成为上下文切换。为简化讨论,只考虑包含一个CPU的单处理器系统的情况。在任何时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从他上次停止的地方开始。从一个进程到另一个进程的转换是由操作系统内核管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,他就执行一条特殊的系统调用指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。实现进程这个抽象概念需要地基硬件和操作系统软件的秘密合作。至于应用程序是如何创建和控制它们进程的此处不作展开,仅仅对进程这一概念有一个宏观的认识先。

线程

尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器中对并行处理的需求,线程成为越来越重要的编程模型,因为多个线程之间比多个进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当多个处理器可用的时候,多线程也是一种使得程序可以运行的更快的方法,这又涉及到另一个概念,并发。

数字计算机整个历史中,有两个需求是驱动进步的持续动力:一个是我们想要计算机做得更多,另一个是我们想要计算机运行的更快。当处理器能够同时做更多的事情时,这两个因素都会改进。

并发(concurrency)

并发是一个通用的概念,是指一个同时具有多个活动的系统

并行(parallelism)

并行是指用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用。按照系统层次结构中由高到低的顺序重点强调三个层次:线程级并发、指令级并行、及单指令、多数据并行,同样不作扩展。

下图是简书转载的,便于直观感受。(https://www.jianshu.com/p/cbf9588b2afb)

 

高并发和多线程

高并发和多线程”总是被一起提起,给人感觉两者好像相等,实则 高并发 ≠ 多线程。

多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。

 

进程与线程

了解了概念后,下面看一下关于进程与线程的区别与联系线程调度线程切换进程间通信协程

区别与联系

  1. 进程是系统资源分配的最小单位,线程是程序执行的最小单位。
  2. 进程使用独立的数据空间,线程共享进程的数据空间。

线程调度

这里只简单罗列线程的几种调度算法,不作展开介绍。比如时间片轮转调度、先来先服务调度、优先级调度、多级反馈队列调度、高响应比优先调度。

线程切换

上面介绍线程概念的时候提到,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。什么是线程切换呢?

此处主要了解线程的上下文切换、线程切换的代价以及如何减少上下文切换。

1.线程上下文切换:CPU 通过分配时间片来执行任务,当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态。任务从保存 → 再加载的过程就是一次上下文切换。

这里涉及两个概念,切入和切出。

切出:是指一个线程被剥夺处理器的使用权而被暂停运行。

切入:是指一个线程被系统选中占用处理器开始或继续运行。

在切出时,线程的进度信息会被保存到内存当中,切入时,会从内存中加载线程的进度信息。这里的“进度信息”就指的是“上下文”,在线程上下文切换过程中会记录程序计数器CPU寄存器状态等数据。

在理解这个之前需要理解程序计数器的作用,程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。由于 java 虚拟机的多线程是通过线程轮流切换并分配时间片的方式实现的,任何时刻,一个处理器(对于多核处理器是一个内核)都只会执行一个线程中的指令,因此为了线程切入后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,我们称这类区域为“线程私有”的内存。(程序计数器是线程私有的。)

另外,当线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道被挂起时变量的值是多少,因此需要记录 CPU 寄存器的状态。

所以,一般来说上下文切换过程中会记录程序计数器、CPU 寄存器状态等数据。

2.上下文切换代价:我们当然是希望在线程执行过程中一直执行直到完毕,但凡有线程切换必然是需要代价的,或者说是有开销的,上下文切换的开销包括直接开销和间接开销。

直接开销如操作系统保存恢复上下文所需的开销、线程调度器调度线程的开销。

间接开销如处理器高速缓存重新加载的开销、上下文切换可能导致整个一级高速缓存中的内容被冲刷,即被写入到下一级高速缓存或内存。

3.如何减少上下文切换?

既然上下文切换会导致额外的开销,因此减少上下文切换次数便可以提高多线程程序的运行效率。减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 取模分段,不同的线程处理不同段的数据。
  • CAS 算法。JUC 中的 Atomic 包使用 CAS 算法来更新数据,而不需要锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

进程间通信

进程间通信,也就是 IPC,是中间件研发的相关职位面试中经常考察的点。在 Linux 下的进程间通信有6种方式,Pipe、MessageQueue、共享内存、UnixSocket、Signal、Semaphore。需要了解的是这几种进程通信方式的原理和适用场景,例如进程间数据共享的场景可以使用共享内存,进程间数据交换可以使用 Unix Socket 或者消息队列,这里不过多介绍。

协程

协程更轻量化,是在用户态进行调度,切换的代价比上下文切换要低很多。Java 中第三方协程框架如 Kilim、Quasar等,这里不过多介绍,可自行了解。

 

感谢

https://www.cnblogs.com/zuxiaoyuan/p/9115861.html

https://blog.csdn.net/dh554112075/article/details/90696768

https://blog.csdn.net/JinXYan/article/details/89915046

 

posted @ 2020-02-16 15:05  习惯沉淀  阅读(511)  评论(0编辑  收藏  举报