前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS底层 之 多线程原理(下)

iOS底层 之 多线程原理(下)

作者头像
CC老师
发布2021-08-25 11:54:30
5260
发布2021-08-25 11:54:30
举报
文章被收录于专栏:HelloCode开发者学习平台

同步

应用程序中存在多个线程会带来有关从多个执行线程安全访问资源的潜在问题。修改同一资源的两个线程可能会以意想不到的方式相互干扰。例如,一个线程可能会覆盖另一个线程的更改或将应用程序置于未知且可能无效的状态。如果幸运的话,损坏的资源可能会导致明显的性能问题或崩溃,这些问题相对容易追踪和修复。但是,如果您不走运,损坏可能会导致微妙的错误,这些错误直到很久以后才会显现出来,或者这些错误可能需要对您的基本编码假设进行重大检查。

谈到线程安全,好的设计就是最好的保护。避免共享资源并最小化线程之间的交互可以降低这些线程相互干扰的可能性。然而,完全无干扰的设计并不总是可能的。在您的线程必须交互的情况下,您需要使用同步工具来确保它们在交互时安全地进行。

OS X 和 iOS 提供了许多同步工具供您使用,从提供互斥访问的工具到在应用程序中正确排序事件的工具。

同步工具

为防止不同线程意外更改数据,您可以将应用程序设计为没有同步问题,也可以使用同步工具。尽管完全避免同步问题是可取的,但这并不总是可能的。

原子操作

原子操作是一种简单的同步形式,适用于简单的数据类型。原子操作的优点是它们不会阻塞竞争线程。对于简单的操作,比如增加一个计数器变量,这可以带来比获取锁更好的性能。

OS X 和 iOS 包括许多操作来对 32 位和 64 位值执行基本的数学和逻辑运算。这些操作包括比较和交换、测试和设置和测试和清除操作的原子版本。有关支持的原子操作列表,请参见/usr/include/libkern/OSAtomic.h头文件或atomic手册页。

内存障碍和易失性变量

为了获得最佳性能,编译器经常对汇编级指令进行重新排序,以保持处理器的指令管道尽可能满。作为此优化的一部分,编译器可能会重新排序访问主内存的指令,因为它认为这样做不会生成不正确的数据。不幸的是,编译器并不总是能够检测到所有与内存相关的操作。如果看似独立的变量实际上相互影响,编译器优化可能会以错误的顺序更新这些变量,从而产生潜在的错误结果。

内存屏障是一种非阻塞同步工具,用于确保内存操作以正确的顺序发生。内存屏障就像一道栅栏,强制处理器完成位于屏障前面的任何加载和存储操作,然后才允许执行位于屏障之后的加载和存储操作。内存屏障通常用于确保一个线程(但对另一个线程可见)的内存操作总是以预期的顺序发生。在这种情况下缺少内存屏障可能会让其他线程看到看似不可能的结果。(例如,请参阅 Wikipedia 中的内存屏障条目。)要使用内存屏障,您只需OSMemoryBarrier在代码中的适当位置调用该函数即可。

易失性变量对单个变量应用另一种类型的内存约束。编译器通常通过将变量的值加载到寄存器中来优化代码。对于局部变量,这通常不是问题。但是,如果该变量对另一个线程可见,则这种优化可能会阻止另一个线程注意到它的任何更改。将volatile关键字应用于变量会强制编译器在每次使用该变量时从内存中加载该变量。您可以声明一个变量,就volatile好像它的值可以随时被编译器可能无法检测到的外部源更改一样。

由于内存屏障和 volatile 变量都会减少编译器可以执行的优化次数,因此应谨慎使用它们,并且仅在需要确保正确性的情况下使用它们。有关使用内存屏障的信息,请参阅 OSMemoryBarrier手册页。

锁是最常用的同步工具之一。您可以使用锁来保护代码的关键部分,这是一次仅允许一个线程访问的一段代码。例如,临界区可能会操作特定的数据结构或使用某种资源,一次最多支持一个客户端。通过在此部分周围放置一个锁,您可以排除其他线程进行可能影响您代码正确性的更改。

图1-1列出了一些程序员常用的锁。OS X 和 iOS 为大多数这些锁类型提供了实现,但不是全部。对于不受支持的锁类型,描述列解释了为什么这些锁没有直接在平台上实现的原因。

图1-1 锁类型

注意: 大多数类型的锁还包含内存屏障,以确保在进入临界区之前完成任何前面的加载和存储指令。

线程安全和信号

当谈到线程应用程序时,没有什么比处理信号的问题更令人恐惧或困惑了。信号是一种低级 BSD 机制,可用于向进程传递信息或以某种方式对其进行操作。一些程序使用信号来检测某些事件,例如子进程的死亡。该系统使用信号来终止失控的进程并传达其他类型的信息。

信号的问题不在于它们做什么,而在于当您的应用程序有多个线程时它们的行为。在单线程应用程序中,所有信号处理程序都在主线程上运行。在多线程应用程序中,与特定硬件错误(例如非法指令)无关的信号被传递给当时正在运行的线程。如果多个线程同时运行,则信号被传递给系统碰巧选择的任何一个。换句话说,信号可以传递到应用程序的任何线程。

在应用程序中实现信号处理程序的第一条规则是避免假设哪个线程正在处理信号。如果一个特定的线程想要处理一个给定的信号,你需要想办法在信号到达时通知该线程。您不能仅仅假设从该线程安装信号处理程序将导致信号被传递到同一个线程。

有关信号和安装信号处理程序,看到更多的信息signal和sigaction手册页。

总结:进程

进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内,通过活动监视器可以查看 Mac 系统中所开启的进程。

线程

线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;进程要想执行任务,必须得有线程,进程至少要有一条线程;程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程。

进程与线程的关系

地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。

资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

1. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

2. 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程同样如果要求同时进行并且又要共享某些变量的并发操作只能用线程不能用进程 。

3. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

4. 线程是处理器调度的基本单位,但是进程不是。

5. 线程没有地址空间,线程包含在进程地址空间中。

多线程的意义

优点:

1. 能适当提高程序的执行效率

2. 能适当提高资源的利用率(CPU、内存)

3. 线程中的任务执行完毕后,线程可以自动销毁

缺点:

1. 开启线程需要占用一定的内存空间(默认情况下占512K)

2. 如果开启大量的线程,会占用大量的内存空间,降低程序的性能

3. 线程越多,CPU 在调用线程上的开销就越大

4. 程序设计更加复杂,比如线程间的通信、多线程的数据共享

多线程原理

时间片

1. CPU在多个任务之间进行快速的切换,这个时间间隔就是时间片

2. (单核CPU)同一时间,CPU 只能处理 1 个线程

3. 换言之,同一时间只有 1 个线程在执行

多线程同时执行

1. 是 CPU 快速的在多个线程之间的切换

2. CPU 调度线程的时间足够快,就造成了多线程的“同时”执行的效果

如果线程数非常多

1. CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源

2. 每个线程被调度的次数会降低,线程的执行效率降低

线程生命周期

线程被创建出来之后,我们就可使用它来完成我们指定给他的任务。我们作为开发者,自然是希望其能按照我们的业务逻辑,快速而且高质量的完成。

但是,具体的任务内容是交给系统去做处理,再往底层是交给CPU来执行代码。那么,一个线程从创建到最后被回收,会经历什么呢?我简单整理了下图:

线程大致有 就绪、运行、阻塞、死亡这几种状态。其生命周期也是和可调度线程池以及CPU的调度有直接的关系,线程创建出来之后,就会等待被CPU调度,此时是就绪状态,CPU调度到我们的线程之后,线程进入运行状态来执行任务,过程中,如果我们有调用sleep或者在等候同步锁或者从可调度线程池中移出后,则进入阻塞状态,带sleep结束,获取到同步锁,被添加到可调度线程池之后,我们的线程又回到就绪状态,等待被调度,直到最后被系统回收掉,也就是死亡状态。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 HelloCoder全栈小集 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档