iOS 并发编程

iOS设备有两或三个CPU核心。即使应用的主线程正忙于更新屏幕,应用仍然可以在后台进行更多计算,而无需任何上下文切换。

* 创建和管理线程

* 多线程优化技术概述

* 操作和队列

1、线程

线程是运行时执行的一组指令序列。

每个进程至少应包含一个线程。在iOS中,进程启动时的主要线程通常被称主线程。

所有的UI元素都需要在主线程中创建和管理。与用户交互相关的所有中断最终都会分发到UI线程,处理代码会在这些地方执行-IBAction方法的代码都会在主线程中执行。

Cocoa编程不允许其他线程更新UI元素。这意味着,无论何时应用在线程执行了耗时操作,比如网络或其他处理,代码都必须将上下文切换到主线程再更新UI。

2、线程开销

每个线程都有一定的开销,从而影响到应用的性能。线程不仅仅有创建时的时间开销,还会消耗内核内存,即影响应用内存性能。

2.1 内核数据结构

每个线程大约消耗1KB的内核内存空间。这块内存用于存储与线程有关的数据结构和属性。这块内存是联动内存,无法被分页。

2.2 栈空间

主线程的栈空间大小为1M,而且无法修改。

所有二级线程默认分配512KB的栈空间。

完整的栈并不会立即被创建出来。实际的栈空间空间大小会随着使用而减少。因此,即主线程1M的栈空间,某个时间点的实际栈空间可能要小很多。

在线程开启前,栈空间的大小可以被改变。栈空间的最小值是16KB,而且其数值必须是4KB的倍数。

2.3 创建耗时

创建线程后启动线程的耗时区间为5-100毫秒,平均大约在29毫秒。这是很大的时间开销。若在应用启动时开启多线程,则尤为明显。

线程的启动时间之所以如此之长,是因为多次的上下切换所带来的开销。

3 GCD

GCD API 由核心语言特性、运行时库以及对执行并行代码的系统增强所组成。

GCD提供的功能列表:

* 任务或分发队列,允许主线程的执行、并行执行和串行执行。

* 分发组,实现对一组任务执行情况的跟踪,而与这些任务所基于的队列无关。

* 信号量

* 屏障,允许在并行分发队列中创建同步的点。

* 分发对象和管理源,实现更为底层的管理和监控。

* 异步I/O,使用文件描述符或管道

GCD同样佳绝了线程的创建与管理。它帮助我们跟中应用中线程的总数,且不会造成任何的泄漏。

大多数的情况下,应用单独使用GCD就可以很好地工作,但仍有特定的情况需要考虑使用NSThread或NSOperationQueue。当应用中有多个长耗时的任务需要并行执行。最好,对线程的创建过程加以控制。如果代码执行的时间长,很可能达到线程的限制64个,即GCD的线程池上限。应该避免浪费地使用dispatch_async 和 dispatch_sync,因为那会导致应用崩溃。虽然64个线程对移动应用来说是个很高的合理值,但不加控制的应用迟早超出这个限制。

4 操作与队列

操作和操作队列是iOS编程中和任务管理有关的又一个重要概念。

NS封装了一个任务以及和任务相关的数据代码,可以用maxConcurrentOperationCount属性控制队列个数,也可以控制每个队列的线程的个数。

对NSThread、NSOperationQueue和GCD API 的一个快速比较。

(1)GCD

* 抽象程度最高。

* 两种队列开箱即用:main和globe

* 可以创建更多的队列(使用dispatch_queue_create)

* 可以请求独占访问(使用dispatch_bassier_sync和dispatch_barrier_async)。

* 基于线程管理。

* 硬性限制创建64个线程

(2)NSOperationQueue

* 无默认队列

* 应用管理自己创建和队列

* 队列是优先级队列

* 操作可以有不同的优先级(使用queuePriority属性)

* 使用cancel 消息可以取消操作。注意,cancel 仅仅是个标记。如果操作已经开始执行,则可能会继续执行下去。

* 可以等待某个操作执行完毕(使用waitUntilFinished)。

(3)NSThread

* 低级别结构,最大化控制。

* 应用创建并管理线程

* 应用创建并管理线程池

* 应用启动线程。

* 线程可以拥有优先级,操作系统会根据优先级调度它们的执行。

* 无直接API用于等待线程完成。需要使用互斥量和自定义代码。

NSOperationQueue 是多核安全的。你可以放心地使用分享队列,从不同线程中提交任务,而无需担心损坏队列。

5 线程安全的代码

两大类技术可以实现这一点:

* 不要使用可修改的共享状态

* 如果无法避免使用可修改的共享状态,则确保你的代码是线程安全的。

在代码中保留不变量。

5.1 原子属性

原子属性是实现应用状态线程安全的一个良好开始。如果一个属性是atomic ,则修改和读取肯定都是原子。因为这样可以阻止两个线程同时更新一个值,反之则有可能导致错误的状态。正在修改的属性的线程处理完毕后,其他线程才能开始处理。

因为原子属性存在开销,所以过度使用它们并不明智。如果能够保证某个属性在任何时刻都不会被多线程访问,那最好还是将器标记为nonatomic

5.2 同步块

即使属性被标记为atomic,最终使用它们的代码仍然可能是线程不安全的。原子属性只能阻止并行修改。如果在某个时间点可能会有多个响应同时尝试更新用户配置文件,可能通过两个CPU核心或通过不同的时间片,所以并不能保证代码一定是线程安全的。使用@synchronized指令可以创建一个信号量,并进入临界区,临界区在任何时刻都只能被一个线程执行。

注意,过度使用@synchronized指令会拖慢应用的运行速度,因为任何时间都只有一个线程在临界区内执行。

5.3 锁

锁是进入临界去的基础构件。atomic属性和@synchronized块是为了实现便捷实用的高级别抽象。

以下是三种可能的锁:

* NSLock 这是一种低级别的锁。一旦获取了锁,执行则进入临界区,且不会允许超过一个线程并行执行。释放锁则标记着临界区的结束。(在临界区,任何时刻最多只允许一个线程执行。NSLock 必须在锁定的线程中进行解锁)

* NSRecursiveLock 在调用 lock 之前,NSLock 必须先调用unlock。但正如名字所暗示的那样,NSRecursiveLock允许在被解锁前锁定的次数相匹配,则认为锁被释放,其他线程可以获取锁。

* NSCondition 有些情况需要协调线程之间的执行。例如,一个线程可能需要等待其他线程返回结果。NSCondition 可以原子性地释放锁,从而使得其他等待的线程可以获取锁,而初始的线程继续等待。使用NSCondition 解决标准的生产-消费者问题。

5.4 将读写锁应用于并发读写

学了atomic 属性可以用于防护不一致性更新的问题,但有些过于谨慎。如果有多个线程试图读取一个属性,同步的代码在同一时刻只允许单个线程进行访问。因此,使用atomic属性会拖慢应用的性能。

这可能是个严重的瓶颈,尤其是当某个状态需要在多个线程间的共享,需要被多个线程访问时。cookie或登陆后的访问令牌就是这样的例子。它可以周期性地变化,但会被所有访问服务器的网络请求所调用。

另外一个使用案例是缓存。每条缓存的条目可以被应用内的任何地方所访问,并且会因为用户特定的操作而更新。

本质上,我们需要允许并行读取、却与写入互斥的一种机制。这将我们带到了读写锁的话题。它们通常有多个读者、单一写者锁和多个读者、多个写者锁。

读写锁允许并行访问读写操作,而写操作需要互斥访问。这意味着多个线程可以并行地读取数据,但是修改数据时需要一个互斥锁。

GCD屏障允许在并行分发队列上创建一个同步点。当遇到屏障时,GCD会延迟执行提交的代码块,直到队列中所有在屏障之前提交的代码块都执行完毕。随后,通过屏障提交的代码块会单独地执行。我们将这个代码块称为屏障块。待其完成后,队列按照原有行为继续执行。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(iOS)