无门槛理解线程

现代计算机在硬件和操作系统的加持下具备了同时执行多个操作的能力,这种同时执行多个操作的能力可以让程序员写出运行更加快速同时响应也更加及时的程序。

计算机提供的这种能力用软件术语描述就是线程,但真正用好这样能力并不容易,需要程序员对计算机有充分的理解。

进程与线程

现代操作系统可以使用运行多个程序,这就是为什么你能在使用浏览器的同时还可以听音乐,这里的两个程序其实指的就是进程。

然而运行多个进程并不是唯一一种获得“同时运行多个操作”的方法,其实,在每个进程中也可以有多个任务同时执行,这里的多个任务就是指的线程。

你可以将线程简单的理解为进程自身的一部分,一个进程可以根据需要划分为多个部分,每一部分可以同时被执行起来。

用一个简单的例子来说明一下,看电影,媒体播放器至少有这样几个线程:一个线程用来渲染UI,可以让让用户暂停、快进等等;还有一个播放声音以及画面的线程。

进程与线程的不同点

操作系统会为每个进程分配一块独有的内存,你的浏览器进程无法访问到音乐播放器进程内存,反之亦然。

不像进程,进程内部的线程共享所在进程的内存,还是以上一节中媒体播放器进程为例,UI线程可以轻松访问到播放声音以及画面线程的数据,反之亦然。此外线程也更加轻量级,这就是为什么线程又被经常叫做轻量级进程( lightweight processes)。

线程到底有什么用?

假设我们需要下载10个文件,下载每个文件需要6分钟,那么如果不使用线程的话全部下载需要一个小时,但是如果我们使用两个线程,每个线程下载5个文件,那么我们就能在半小时内下载完全部文件;使用四个线程就能将程序运行时间缩短到15分钟,这里不考虑网络限制。

但是,真的会有这么简单吗?

实际上并不是所有的进程都需要使用多线程,如果你的程序就是一堆串行操作无法并行的话那么多线程在这种情况下没有用武之地。

要想使用多线程你需要仔细的将整个程序逻辑划分为子任务

同时操作系统并不保证线程就是100%并行执行的,决定因素其实是底层硬件而不是软件。

最后这一点非常重要,如果你的硬件不支持真正的并行操作(true parallelism)的话,那么操作系统只能刷一点花招,让你的多线程程序看起来是在并行运行,但其实不是,这就是程序的并发执行(concurrency).

假设系统中有两个线程A和B,那真正的并行操作指的是A和B在同时运行,也就是同一时刻有两个线程在运行。但并发不是这样,并发是指同一时刻只有一个线程在运行,但是A运行一会后就停止然后运行B,这样看起来像是A和B在同时运行,但这不是真正的并行。

并发和并行的底层实现

计算机系统中真正干活的是CPU,CPU其实是由几部分组成的,最主要的部分就是我们所说的核,core,这是机器指令被执行的地方,一个core一次只能执行一个操作。

鉴于CPU核心的这种特性(一次只能执行一个操作),对于只有一个core的CPU来说要想实现并发(并行)就只能借助操作系统的帮助了,操作系统是怎样在单核上实现并发(并行)呢?

原来操作系统借助了一种叫做preemptive multitasking的技术,在这种技术的帮助下,操作系统可以随时暂停某个任务转而去执行另外一个任务。

因此如果你的计算机中只有一个core,那么操作系统的任务就是把这个core的计算资源合理分配给各个任务,这些任务一个接一个的运行在一个循环中,这就使得各个任务看起来是在同时运行。

当然,现代CPU都已经不止一个core了,也就是说运行在多核CPU上的操作系统支持真正的并发。

同时操作系统也能否检测到CPU中核心的个数,将线程分配给哪个core执行是由操作系统决定的,这一切对于程序员来说是透明的。

单核上运行多线程程序

现在我们知道了在单核上真正的并行是不可能实现的,然而即便在这种情况下多线程程序依然有用。

假设你正在编写一个读取磁盘的桌面程序,磁盘由于年老失修读起来特别慢,在这种情况下如果你的程序只有一个线程,那么在读取磁盘时整个程序都不会响应用户的任何操作直到磁盘读取操作执行完成,当然在这个过程中操作系统会把CPU的计算资源分配给其它线程,但在磁盘操作读取完成之前操作系统都不会把CPU计算资源分配给你的程序了。

让我们从多线程角度来重新设计这个程序,我们创建两个线程,线程A负责读取磁盘,线程B负责用户可以看到的UI界面。这样当线程A因读取磁盘而被卡住时操作系统依然可以执行B线程,也就是说用户界面依然可以响应用户操作。这就是多线程的好处。

多线程,多问题

线程的一大好处就是同一个进程中的线程之间共享内存,这就使得线程之间共享数据极为简单,硬币的另一个面就是出错也极其容易。

两个线程同时读取一块内存时不会有问题,问题出现在当一个线程试图修改数据时。

data race

当一个写线程修改一块内存时,一个读线程可能正在读这块内存,如果这是写线程还没有写完,那么读线程读取到的将是一块被破坏的内存数据

race condition

比data race更难解决的是race condition问题,假设任务之间是有先后顺序的,先执行A后执行B,但我们也知道操作系统调度线程其实是对程序员透明的,那么你该怎样确保操作系统按照某种顺序来执行你的线程。

线程安全的概念

当一段代码即使是在多线程环境下也能正确运行,没有data race问题也没有race condition问题时我们才称之为线程安全,thread-safe。

data race问题的根源

我们已经知道了CPU一次只能执行一条指令,我们认为这样的一条机器指令是原子的(atomic),因为其不可再分割:机器指令不可以再被分割为更小的操作。希腊语“atom”其原意就是uncuttable(不可再分割)。

因此我们可以看到机器指令的执行是原子的,执行单个机器指令时不存在data race问题。

然而程序员写的程序实际上是由大量的机器指令组成的(编译器将程序员写的程序翻译为机器指令),当大量的机器指令被执行时就会有data race问题了。

即使像a = 1;这样简单的赋值语句实际上可能会由多条机器指令组成,这就使得a = 1;这样的程序就存在data race问题。

race condition问题的根源

从“并发和并行的底层实现”一节中我们知道,操作系统借助该技术可以随时启动、暂停、终止一个线程。而作为程序员是不能控制线程是以怎样的顺序来执行的,实际上即使像这样简单的两句程序:

threadA.start();
threadB.start();

程序员都无法控制这两个线程的启动顺序,如果你多次运行该程序的话实际上你会发现有时A先运行,而有时B先运行,在这种情况下如果你的程序需要B线程先运行的话那么显然你会遇到race condition问题。

上述现象就称为不确定性 non-deterministic,也就是说你无法确定该该程序的运行结果,运行该程序有时会得到X而有时又会得到Y。

作为程序员,调试这样的bug会让人抓狂,因为你根本无法稳定的复现这个问题。

多线程编程好帮手:并发控制

要想编写好多线程程序你需要掌握正确的方法,这就是并发控制。

操作系统和编程语言提供了很多方法来帮助程序员实现并发控制,限于篇幅,今天先挖个坑,后续将为大家介绍。

总结

希望这篇文章能给大家一个关于线程的基本认知,只有从底层真正理解线程,这样不管是c++语言提供的线程还是Java、Python提供的线程你都能游刃有余的用起来,而且能正确用起来。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值