一、关于线程
1.1 简介
计算机线程(Thread)是操作系统能够进行运算调度的最小单位。线程的优势在于提高了程序的效率和响应能力,尤其在处理 I/O 操作或多任务时。多线程编程能够充分利用多核处理器的计算能力,达到更高的性能。
它被包含在进程中,是进程的实际执行单位。一个进程至少包含一个线程,也可以包含多个线程。
每个线程共享进程的资源(如内存和文件),但有自己独立的执行栈和程序计数器。
1.2 发展历程
计算机线程的发展历程可以大致分为以下几个阶段,涵盖了从概念提出到技术应用和优化的演进过程:
-
早期概念阶段:
- 1960s - 1970s:计算机科学家开始研究并发编程的基础概念,早期的操作系统(如UNIX)提供了基本的进程管理机制,但并没有明确的线程概念。
-
线程概念的提出:
- 1980s:随着操作系统和编程语言的发展,研究者开始提出线程的概念。David R. Cheriton和Brian Randell等人在论文中首次提出了线程的概念,并探讨了多线程在操作系统中的潜在优势。
-
POSIX线程标准:
- 1990s:随着多任务操作系统的普及和多核处理器的出现,对多线程编程的需求增加。POSIX(Portable Operating System Interface)制定了一套线程接口标准,促进了跨平台和跨系统的线程编程。
-
操作系统级支持:
- 1990s - 2000s:主流操作系统(如Windows、Linux、macOS)开始提供内置的线程支持和优化。操作系统通过内核级的线程调度器(Scheduler)管理和调度线程,提高了多线程应用程序的性能和稳定性。
-
多核处理器时代:
- 2000s至今:随着多核处理器的普及,多线程编程成为充分利用硬件并行性能的关键。软件开发者在编写应用程序时越来越多地考虑如何利用多线程来提高程序的并发性和性能。
-
并发编程模型的演变:
- 2000s至今:随着云计算、大数据和实时系统等应用的兴起,对并发编程模型的需求更加迫切。新的编程语言和框架(如Java的并发包、Python的多线程库)提供了更高级的抽象和工具,简化了多线程编程的复杂性。
-
现代并发模型的挑战和优化:
- 2010s至今:随着硬件架构的复杂化(如非对称多处理器、GPU并行计算),线程的优化和并发模型的研究成为一个重要的研究方向。包括锁优化、无锁数据结构、并发算法等技术的发展,以提高多线程程序的性能和效率。
总结来说,计算机线程从概念的提出到实际的应用,经历了几十年的发展过程。它从最初的理论探讨逐步演化为操作系统和编程语言中不可或缺的重要组成部分,为现代计算机系统的高效性能和复杂应用提供了基础支持。
1.3 出现背景
计算机线程的出现背景可以追溯到操作系统和计算机体系结构的发展过程中。以下是主要背景因素:
-
多任务操作系统的发展:
- 随着计算机系统的发展,操作系统逐渐支持多任务(multitasking),即同时处理多个任务或进程。为了更高效地管理和调度这些任务,线程作为更细粒度的执行单元被引入。
-
提高系统资源利用率:
- 单一进程模型下,进程是程序的基本执行单位,但是创建、撤销和切换进程都需要消耗大量的系统资源。线程作为进程内的执行单元,可以更高效地利用系统资源,如内存和CPU时间片。
-
响应性和并发需求:
- 随着计算机应用程序的复杂性增加,对系统的响应速度和并发处理能力提出了更高要求。线程使得程序可以并行执行多个任务,提高了系统的响应速度和并发处理能力。
-
硬件技术的进步:
- 计算机硬件的发展(如多核处理器)为多线程提供了更好的支持。多核处理器可以同时执行多个线程,有效利用多个处理单元的能力,提高系统的整体性能。
-
编程模型的演变:
- 传统的单线程编程模型难以满足复杂应用程序的需求,多线程编程模型的出现使得程序员可以更灵活地处理并发任务和异步操作,提高程序的可维护性和扩展性。
综上所述,计算机线程的出现是操作系统和硬件技术发展的产物,它为提高系统的资源利用率、响应速度和并发处理能力提供了重要的技术基础。
线程带来的好处
一直以来,硬件的发展极其迅速,其中有一个很著名的"摩尔定律"。
摩尔定律并不是一种自然法则或者是物理定律,它只是基于一些观测数据后,对未来的一种预测。按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力。2004 年,Intel 宣布 4GHz 芯片的计划推迟到 2005 年,然后在 2004 年秋季,Intel 宣布彻底取消 4GHz 的计划,也就是说摩尔定律的有效性超过半个世纪后戛然而止。但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,不再追求单独的计算单元,而是将多个计算单元整合到了一起,于是就出现了多核 CPU。
短短十几年的时间,家用型 CPU,比如 Intel i7 就可以达到 4 核心甚至 8 核心。而专业服务器通常可以达到几个独立的 CPU,每一个 CPU 甚至拥有多达 8 个以上的内核。因此,摩尔定律似乎在 CPU 核心扩展上继续得以验证。因此,多核 CPU 的背景下,并发编程变得越来越受重视,因为通过并发编程的形式可以将多核 CPU 的计算能力发挥到极致。
顶级计算机科学家 Donald Ervin Knuth 如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施导致的,他们将摩尔定律的责任推给了开发者。
另外,有些特殊的业务场景下,先天就适合于并发编程。比如在图像处理领域,一张 1024X768 像素的图片,包含达到 78 万 6 千多个像素。要在短时间内将所有的像素遍历一边需要很长的时间,面对如此复杂的计算量就需要充分利用多核计算的能力。
又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以利用多线程的技术来完成。面对复杂业务模型,并行程序会比串行程序更适用于业务需求 。正是因为这些优点,使得多线程技术得到了进一步的重视,Java 开发者也应该掌握并发编程,以便:
- 充分利用多核 CPU 的计算能力;
- 方便进行业务拆分,提升应用性能
1.4 特点功能
- 轻量级:线程是轻量级的执行单元,相比进程消耗的资源更少,可以更快速地创建和销毁。
- 共享资源:线程通常与其他线程共享进程的资源,如内存空间、文件句柄等。这也是多线程编程中需要考虑同步和互斥的原因。
- 并发执行:多个线程可以在同一时间内并发执行,利用多核处理器和多处理器系统的性能优势。
- 独立调度:线程独立于其它线程进行调度,操作系统可以在不同的处理器核心上调度不同的线程,以提高并行度和系统的响应速度。
1.5 线程的类型
- 用户线程(User Thread) :由用户自行创建和管理的线程,运行在用户空间中。
- 守护线程(Daemon Thread) :在后台提供服务的线程,它们不会阻止进程的终止,当所有的非守护线程结束时,守护线程会自动结束。
1.6 线程的状态(生命周期)
- 新建(New) :线程对象创建但未启动。
- 运行(Runnable) :线程正在执行任务。
- 阻塞(Blocked) :线程因为等待某个条件而暂停执行。
- 等待(Waiting) :线程因为等待其他线程的通知或者特定条件而暂停。
- 超时等待(Timed Waiting) :线程等待一段指定的时间后自动恢复。
- 终止(Terminated) :线程执行完成或者出现异常导致终止。
1.7 多线程编程的优点及挑战
优点
- 提高程序效率:可以利用多核处理器并行处理任务,加快程序执行速度。
- 改善程序结构:将任务分解成多个线程可以使程序结构更清晰,提高代码的模块化和可维护性。
- 提升用户体验:能够处理异步操作,增加程序的响应性,提升用户体验。
挑战
- 同步与互斥:多线程共享资源时,需要考虑线程间的同步与互斥,避免竞态条件和数据不一致问题。
- 死锁:多个线程因为互相等待对方持有的资源而无法继续执行。
- 线程安全:保证多线程环境下的数据操作安全,避免出现数据错乱和意外的行为。
二、其它概念
2.1 进程与线程
2.1.1 关于进程
讲到线程,又不得不提进程了~
进程的定义:
进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
- 进程是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源
在windows下打开任务管理器,可以发现我们在操作系统上运行的程序都是进程:
2.1.2 为什么要线程
那系统有了进程这么一个概念了,进程已经是可以进行资源分配和调度了,为什么还要线程呢?
为使程序能并发执行,系统必须进行以下的一系列操作:
- (1)创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB(Process Control Block,进程控制块);
- (2)撤消进程,系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB;
- (3)进程切换,对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。
可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销
引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。 使OS具有更好的并发性,简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能 【调度】 )。
那么线程在哪呢??举个例子:
也就是说:在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
- 所以说:一个进程会有1个或多个线程的!
2.1.3 两者概念
- 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
- 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。
很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):
- 进程可以比作是你开的这一把游戏
- 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。
1、线程在进程下进行
(单独的英雄角色、野怪、小兵肯定不能运行)
2、进程之间不会相互影响,主线程结束将会导致整个进程结束
(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)
3、不同的进程数据很难共享
(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)
4、同进程下的不同线程之间数据很容易共享
(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)
5、进程使用内存地址可以限定使用量
(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)
2.1.4 两者区别
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如 I/O) :
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即 CPU 分配时间的单位 。
再用一些图来表达一下,感官会更清晰一些。
1. 计算机的核心是 CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个 CPU 一次只能运行一个任务。
**2.**进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。
一个车间里,可以有很多工人。他们协同完成一个任务。
**3.**线程就好比车间里的工人。一个进程可以包括多个线程。
车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
还有些房间,可以同时容纳 n 个人,比如厨房。也就是说,如果人数大于 n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
这时的解决方法,就是在门口挂 n 把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。
不难看出,mutex 是 semaphore 的一种特殊情况(n=1 时)。也就是说,完全可以用后者替代前者。但是,因为 mutex 较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
操作系统的设计,因此可以归结为三点:
- 以多进程形式,允许多个任务同时运行;
- 以多线程形式,允许单个任务分成不同的部分运行;
- 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
2.2 并发与并行
计算机中的并发(Concurrency)和并行(Parallelism)是两个密切相关但又有所区别的概念,它们都与程序的执行方式有关。
2.2.1 并发(Concurrency)
并发是指在一段时间内,多个任务看起来像是同时进行的。它强调的是多个任务在宏观上“同时”发生,但实际上它们可能在微观上是交替执行的。并发的实现通常依赖于操作系统的调度算法,使得多个任务能够共享CPU资源,从而在单个处理器上实现多任务处理。
并发的特点:
- 任务切换:操作系统通过时间片轮转或其他调度算法,使得多个任务在单个CPU上交替执行。
- 资源共享:并发任务通常共享相同的硬件和软件资源。
- 上下文切换:为了实现并发,操作系统需要在任务之间进行上下文切换,这会带来一定的开销。
2.2.2 并行(Parallelism)
并行是指多个任务在物理上同时进行。它强调的是多个任务在多个处理器上同时执行,每个处理器可以独立处理一个任务。并行计算可以显著提高计算效率,特别是在处理大量数据或复杂计算时。
并行的特点:
- 多处理器:并行计算需要多个处理器或核心,每个处理器可以同时执行不同的任务。
- 任务分配:并行任务通常需要在多个处理器之间分配,以实现真正的同时执行。
- 通信开销:在并行计算中,不同处理器之间可能需要交换数据,这会带来通信开销。
2.2.3 并发与并行的关系
- 并发可以导致并行:在多核处理器上,操作系统可以将并发任务分配到不同的核心上,从而实现并行执行。
- 并行是并发的一种特殊情况:并行是并发的一种特例,它要求物理上同时执行多个任务。
2.2.4 应用场景
- 并发:适用于需要处理多个任务的场景,如服务器处理多个客户端请求、多线程Web服务器等。
- 并行:适用于需要大量计算资源的场景,如科学计算、大数据处理、图形渲染等。
2.2.5 编程模型
- 并发编程:涉及到线程的创建、同步、通信等,需要处理线程安全、死锁等问题。
- 并行编程:涉及到任务的分配、数据的并行处理、结果的合并等,需要考虑数据分割、负载均衡等问题。
并发和并行是提高程序性能和资源利用率的重要手段,但同时也带来了编程复杂性。开发者需要根据具体需求选择合适的并发或并行策略。