iOS知识梳理 - 多线程(1)杂谈

发布于 2019-10-14  约 7 分钟

进程和线程

首先,通常所说的进程和线程都是系统内核层面的概念。

进程(Process),直观来讲就是运行中的程序,它是系统进行资源分配的最小单位。即,系统给每个进程分配虚拟地址空间等资源,进程间默认是不共享内存等资源的。

线程(thread)本身理解起来其实更简单,它是个依附于进程的更细粒度的调用单位,它有自己的栈等数据,但通常内存是共享的(在一个进程的多个线程间)。然而实际讨论起来会更复杂一些。

线程分为内核级线程用户级线程两种,分类的标准主要是线程的调度者在核内还是核外。说白了,内核级线程就是系统提供的线程。而用户级线程就是程序自身实现的线程。理论上他们的本质区别只是这些,但实际上确是天差地别。

线程的调度策略大体上分两种:抢占式调度和协同式调度。抢占式指线程的切换时机由系统控制,每个线程都会获得或多或少的CPU时间片,单个线程卡死并不会卡住其它线程;协同式调度是指某一线程执行完或执行到一定阶段后,主动让出CPU,切换到另一个线程执行,一个线程卡死了,别的线程也就得不到CPU资源,整个进程就全完了。

我们一般讨论的线程,都默认了调度方式为抢占式调度。而讨论协同式调度的线程时,通常使用一个更高大上一点的词:协程

从另一个维度来看一下,我们使用的线程API,和内核级线程的对应关系,通常分为一对一、多对一和多对多。一对一时其实就是内核级线程;多对一时是用户级线程,实际实现时都是协同式调度的,即协程。而多对多时,即多个上层线程会被分派到多个内核级线程执行,这种被成为混合型线程。

我们一般讨论的线程都是这种一对一模型下的线程,也是目前最广泛的线程实现。无论是Android还是iOS,以及Java服务端,默认地,都是这种。我们后面的讨论也主要在这种模型下展开。

多对一模型的用户级线程已经几乎看不到了,这里不再讨论。

多对多模型还是不少的,这两年很火的golang就是用的多对多模型,另外有一些jvm也有实现但默认选项仍是一对一。以golang的goroutine为例,实际实现可以理解为线程池+协程的结合。goroutine其实比较接近线程池中的task概念,排队进入线程池执行。而它优于task的地方在于,goroutine是带上下文的,即执行到一半时,可以停下来带着上下文重新排队,排到它时继续执行,这又是协程的特性了。这种底层设计结合优秀的上层语法,得到的golang是个非常有吸引力的语言。

参考:

线程和进程的区别是什么?

内核态是指一个特殊的进程,还是指进程的一种特殊状态?

用户级线程和内核级线程的区别

Linux下调用pthread库创建的线程是属于用户级线程还是内核级线程

线程到底是什么?

发散地探讨了一些,下面还是重点讨论内核级线程。

线程池

多线程编程给我们带来了诸多便利,但仍有一些不方便的地方。比如出于性能考虑,线程数量不能太多。

并行是指物理层面上的同时处理,即CPU多个核心各自处理不同的事情。这很nice。

并发是指逻辑上的同时处理多个事情,实际上CPU可能是在同一个核上分了时间片处理的,也就是前面说的抢占式调度。线程数量越多,线程间切换的开销也就越大。这就导致我们不能随意创建线程。

另外,如果有比较多的创建、销毁线程操作,开销也是比较大的。

为了解决这一问题,最常见的方案是线程池模式。一开始就设定好池子里线程的数量,把要执行的任务往池子里丢,有线程空闲就处理,没有就排队。

GCD就是iOS平台下基于线程池的方案,它暴露Queue、Task这样的接口,通常只会使用CPU核心数那么多的线程数量(参考Number of threads created by GCD?),保证了性能。相比直接调用线程接口,降低了心智负担,并且有效地提高了多线程程序的下限。

线程冲突

线程冲突是多线程编程中最大的问题,容易遇到,又比较难搞。比如两个线程都想要操作一个数组,一个想往里塞数据,一个想要删数据,就很容易冲突。没做异常捕获的话程序就直接挂了。

常规方案就是加锁。但是加锁是个,咋说呢,很有学问的事情。加得不好会特别影响性能(→_→ 参考python的GIL)。一些复杂的业务场景下,锁的问题可能会非常非常非常非常复杂。

这个问题的本质在于,线程间是通过共享内存来通信的,同时读写同一块内存时必然遇到冲突问题。于是,通过事件驱动/消息机制进行线程间通信的方式逐渐受到人们关注。直白点来说,你需要这块数据的时候我复制一份发给你,你就从消息通道读数据,不要跟我共享别的变量之类的了。

一般场景下,这种方式带来的内存和CPU开销并不会太多,逻辑上却比锁要简单太多了,因此这些年受到了广泛的关注。

以下几个都是这种思想的实现:

有的语言(go)是把这作为可选方案,而有的语言(dart)直接把这作为唯一方案。当消息机制成为线程间通信的唯一方案时,线程已经不再是线程了(共享内存算是线程的比较核心的特征了),因此dart自称单线程语言,其isolate是区别于多线程的一种并发编程机制。

用通信代替共享内存是个大的思路,实际上衍生出了多重并发编程模型如Actor、CSP等。这里就不再更细地分析了,可以参考erlang/go的实现。

参考:

并发编程模型:事件驱动 vs 线程

如何理解 Golang 中“不要通过共享内存来通信,而应该通过通信来共享内存”?

多线程和异步

比较早的时候,是很少用异步调用的。要发网络请求的时候,就开个子线程让子线程进行同步调用,阻塞等待调用结果。

这种方式称为同步,期间这个线程是阻塞的。

显然,这是对线程资源极大的浪费。因此现在这样做的少了,通常发出网络请求后当前线程会继续往下执行,请求回包后会通过某种事件处理机制触发回调函数进行处理。

这种方式称为异步,期间这个线程是非阻塞的。

除了网络请求,IO读写等场景也是类似的。这些不占用CPU的场景应当优先使用异步的手段,而不是开子线程处理。(iOS平台下好像基本上没有这样做的,以前写Java的时候见到的比较多)

iOS下,通常我们用block或delegate进行异步编程,写起来会比同步代码麻烦一些。协程的一大好处就是能够以类似同步代码的方式写异步代码。对应的async/await是swift最受人期待的新特性,之前一度传言swift5会上,可惜并没有...

阅读 296发布于 2019-10-14

推荐阅读
二师兄的博客
用户专栏

个人博客,希望这次能多写点

3 人关注
41 篇文章
专栏主页
目录