6、并发 & 并行
上面提到一个名词 并发
(Concurrency),指的是:
同一时刻只有一条指令执行,但多个进程指令被快速地 轮换执行,使得在宏观上有同时执行的效果,微观上并不是同时执行,只是把CPU时间分成若干段,使得多个进程快速交替地执行,存在于单核或多核CPU系统中。
而另一个容易混淆的名词 并行
(Parallel) 则是:
同一时刻,有多条指令在多个处理器上同时执行,从微观和宏观上看,都是一起执行的,存在于多核CPU系统中。
7、协作式 & 抢夺式
单核CPU,同一时刻只有一个进程在执行,这么多进程,CPU的时间片该如何分配呢?
协作式多任务
早期的操作系统采用的就是协作时多任务,即:
由进程主动让出执行权,如当前进程需等待IO操作,主动让出CPU,由系统调度下一个进程。
每个进程都循规蹈矩,该让出CPU就让出CPU,是挺和谐的,但也存在一个隐患:
单个进程可以完全霸占CPU
计算机中的进程良莠不齐,先不说那种居心叵测的进程了,如果是健壮性比较差的进程,运行中途发生了死循环、死锁等,会导致整个系统陷入瘫痪!在这种鱼龙混杂的大环境下,把执行权托付给进程自身,肯定是不符合基础国情,由操作系统扛大旗的 抢占式多任务 横空出世~
抢占式多任务
由操作系统决定执行权,操作系统具有从任何一个进程取走控制权和使另一个进程获得控制权的能力。
系统公平合理地为每个进程分配时间片,进程用完就休眠,甚至时间片没用完,但有更紧急的事件要优先执行,也会强制让进程休眠。有了进程设计的经验,线程也做成了抢占式多任务,但也带来了新的——线程安全问题。
8、线程安全问题
进程在执行过程中拥有独立的内存单元,而多个线程共享这个内存,可能存在这样一种情况:
假设有一个变量a = 10,它可以被线程t1和t2共享访问,两个线程都会对i值进行写入,假设在单核CPU上运行此程序,系统需要给两个线程都分配CPU时间片:
- 1.t1从内存中读取了a的值为10,它把a的值+1,准备把11这个新值写入内存中,此时时间片耗尽;
- 2.系统执行了线程调度,t1的执行现场被保存,t2获得执行,它也去读a的值,此时a的值仍为10,+1,然后把11写入内存中;
- 3.t1再次被调度,此时它也把11写入内存中。
程序的执行结果和我们的预期不符,a的值应该为12而不是11,这就是线程调度不可预测性引起的 线程同步安全问题。
解决方法:
系列化访问临界资源,同一时刻,只能有一个线程访问临界资源,也称
同步互斥访问
,通常的操作就是加锁(同步锁)
,当线程访问临界资源时需要获得这个锁,其他线程无法访问,只能 等待(堵塞),等这个线程使用完释放锁,供其他线程继续访问。
前置概念相关的东西就说这么多,相信会对你接下来学习Kotlin协程大有裨益。
0x2、单线程的Android GUI系统
是的,Android GUI 被设计成单线程了,你可能会问:为啥不采用性能更高的多线程?
答:如果设计成多线程,多个线程同时对一个UI控件进行更新,容易发生 线程同步安全问题;最简单的解决方式:加锁,但这意味着更多的耗时和UI更新效率的降低,而且还有死锁等诸多问题要解决;多线程模型带来的复杂度成本,远远超出它能提供的性能优势成本。这也是大部分GUI系统都是单线程模型的原因。
Android要求:在主线程(UI线程)更新UI,注意是 → 要求建议,不是规定,规定底线是:
只有创建这个view的线程才能操作这个view
所以,你在子线程中更新子线程创建的UI也是可以的,不过不建议这么做,建议:
在子线程中完成耗时操作,然后通过Handler发送消息,通知UI线程更新UI。
Tips:关于Handler更多的内容可移步至:《换个姿势,带着问题看Handler》
接着说下,Android异步更新UI的写法都有哪些~
1、Handler
主线程中实例化一个Handler对象,在子线程需要更新UI的地方,通过Handler对象的post(runnable)或其他函数,往主线程的消息队列发送消息,等待调度器调度,分发给对应的Handler完成UI更新,写法示例如下:
利用 lambda表达式 + Kotlin语法糖thread { } 可对上述代码进行简化:
还有另外一种常见的写法:自定义一个静态内部类Handler,把UI更新操作统一放到这里,根据msg.what进行区分。
2、AsyncTask
AsyncTask是Android提供的一个轻量级的用于处理异步任务的类(封装Handler+Thread),使用代码示例如下:
相比起手写Handler简单了一些,只需要继承AsyncTask,然后就是填空题(按需重写函数):
- onPreExecute():异步操作开始,可以做一些UI的初始化操作;
- doInBackground():执行异步操作,可调用publishProgress()触发onProgressUpdate()进度更新;
- onProgressUpdate():根据进度更新UI;
- onPostExecute():异步操作完成,更新UI;
但也存在以下局限性:
① AsyncTask类需在主线程中加载;
② AsyncTask对象需在主线程中创建;
③ execute()必须在主线程中调用,且一个AsyncTask对象只能调用一次此方法;
④ 需要为每一种任务类型创建一个特定子类,同时为了访问UI方便,经常定义为Activity的内部类,耦合严重。
可以通过 函数转换为回调的方式 来解耦,抽取后的自定义AsyncTask类如下:
调用也很简单,按需重写对应函数即可:
解耦后灵活多了,外部调用逻辑 与 内部异步逻辑 的分离开来了,但依旧存在问题,如异常处理、任务取消等。