一、关于线程和调度的一些基本概念
1.1 什么是线程
线程是操作系统能够进行运算调度的基本单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。和进程类似,一个线程也有就绪、运行、阻塞三种基本转状态。
线程是处理机的独立调度单位,多个线程可以并发执行。每个线程都应有一个唯一的标示符和线程控制块。线程控制块记录了线程执行的寄存器和栈等现场状态。
1.2 时间片
时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间,时间片的大小对系统的性能影响很大。
如果时间片足够大,以至于所有进程都能在一个时间片内执行完毕,则时间片轮转调度算法就退化为先来先服务调度算发。如果时间片很小,那么处理机将在进程间过于频繁切换,使处理机的开销增大,而真正用于处理用户作业的时间将减少,因此时间片的大小应选择适当。
1.3 线程安全
线程不持有资源,但是同一进程中的所有线程可以共享进程的资源,所以说我们遇到的大部分线程的问题基本是因为共享资源引起的。
也就是说,当多个线程同时访问一块共享资源(某一块内存),因为时序性问题,会导致数据错乱,这就是线程不安全。
1.4 同步、异步
- 同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回。
- 异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了。
1.5 并发,并行
并发是同一个时间段内,几个作业都在同一个CPU上运行,但任意一个时刻点上只有一个作业在处理机上运行。
并行是同一个时间段内,几个作业在几个CPU上运行,任意一个时刻点上,有多个作业在同时运行,并且多个作业之间互不干扰。
二、常见的CPU调度算法
2.1 先来先服务算法(FCFS)
这个调度算法是最简单的调度算法,这个算法按照每次在就绪队列中选择最先进入该队列的进程进行调度。属于不可剥夺度算法,有利于CPU繁忙型作业,不利于I/O繁忙性作业。
2.2 短作业优先算法(SJF)
此算法是从后背队列中选择一个或几个估计运行时间短的作业,优先调度到内存中运行。该做法对场作业不利,所以无法保证紧迫性的作业会被及时处理。
2.3 优先级调度算法
分为非剥夺和剥夺式的调度。每个线程有一个优先级,CPU每次去拿优先级高的运行,优先级低的等等,为了避免等太久,每等一定时间,就给线程提高一个优先级。
2.4 高响应比优先调度算法
是对先来先服务算法(FCFS)和短作业优先算法(SJF)的一种综合平衡,克服了饥饿状态也兼顾了场作业。
2.5 时间轮转片调度算法
主要适用于分时系统。根据先来先服务的原则,但是仅能运行一个时间片,用完之后及时没有完成运行任务,也要将资源释放给下一个就绪作业,被剥夺的返回就绪队列直至下一次被运行。
2.5 多级反馈队列调度算法
是时间片轮转调度算法和优先级调度算法的综合和发展。
有多个优先级不同的队列,每个队列里面有多个等待线程。
CPU每次从优先级高的遍历到低的,取队首的线程运行,运行完了放回队尾,优先级越高,时间片越短,即响应越快,时间片就不是固定的了。
队列内部还是用先来先服务的策略。
三、多线程工作的概念和意义
- 多CPU计算机中,各个线程可以占用不同的CPU:因为线程是处理机调度的单位
- 每个线程都有一个线程ID、线程控制块TCB:类比没有引入线程的进程的进程ID和进程控制块PCB
- 线程也有运行、就绪、阻塞三种基本状态
- 线程几乎不拥有系统资源:出了CPU外的系统资源都被分配给了进程,包括一些IO设备、内存地址空间等等
- 同一进程的不同线程共享进程的资源
- 由于共享内存地址空间,同一进程中的线程间的通信甚至无需系统干预
- 同一进程中的线程切换,不会引起进程切换,但是不同进程中的线程切换,则会引起进程切换
- 切换同一进程中的线程,系统的开销小;而切换不同进程中的线程,系统的开销较大
四、iOS中的线程
根据层级从低到高,分别是NSThread < GCD < NSOperation,下面的三部分,分别围绕着这三中多线程方案来讲述。
4.1 NSThread
说到了NSThread就要提一嘴pthreads
,pthread 是一套通用的多线程的 API,可以在Unix / Linux / Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,我们在 iOS 开发中几乎不使用 pthread。
比较典型的有两个例子:
1、用在线程安全上面pthread_mutex_t。
2、用于获取当前的线程,YYKit上有典型用法。
/**
Submits a block for asynchronous execution on a main queue and returns immediately.
*/
static inline void dispatch_async_on_main_queue(void (^block)()) {
if (pthread_main_np()) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
}
NSThread
是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用 [NSThread currentThread] 来显示当前的进程信息。
- (instancetype)init API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
以上是我们最常用的一些NSThread
的API。
4.2 GCD
GCD全称Grand Central Dispatch,是基于 C 实现的一套 API。
照例,先来看API:
#include <dispatch/base.h> //引用dispatch的基本枚举以及宏定义
#include <dispatch/time.h> //dispatch中的时间对象dispatch_time_t
#include <dispatch/object.h>
#include <dispatch/queue.h> //dispatch线程调度队列
#include <dispatch/block.h> //dispatch调度块
#include <dispatch/source.h> //协调处理特定低级系统事件的对象。
#include <dispatch/group.h> //dispatch调度组
#include <dispatch/semaphore.h> //dispatch信号量
#include <dispatch/once.h> //dispatch一次执行
#include <dispatch/data.h> //文件的结构体
#include <dispatch/io.h> //读取文件使用
#include <dispatch/workloop.h>
4.2.1 dispatch_async
GCD通过这个API来进行子线程的切换,并且通过设置具体的dispatch_queue_t来来控制任务队列是并行还是串行,同时也可以切换到dispatch_get_main_queue上,因为主线程和主队列是绑定的,于是此操作也代表切换到主线程。
//异步切换到主线程
dispatch_async(dispatch_get_main_queue(), ^{
//实现代码
});
//同步切换到主线程
dispatch_sync(dispatch_get_main_queue(), ^{
//实现代码
});
//异步切换到子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//实现代码
});
//同步切换到子线程
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
//实现代码
});
假如说,需要自定义执行的队列是并并还是串行,可以自定义队列:
dispatch_queue_t queue = dispatch_queue_create("QueueName", DISPATCH_QUEUE_CONCURRENT);
//DISPATCH_QUEUE_SERIAL 串行
//DISPATCH_QUEUE_CONCURRENT 并行
GCD通过这个方法,将线程这个概念抽象出来,让开发者不再直接与线程交互。开发者要做的是向队列中添加代码块,然后GCD来管理线程池和代码块运行的实际线程。
这缓解了创建过多线程的问题,因为线程现在是GCD集中管理的,并从开发者那里抽象出来。这让开发者更集中关注的是任务队列而不是线程本身。
4.2.2 dispatch_semaphore
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号,这个的逻辑和我们的MRC中频繁使用的引用计数很有相似之处。
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
与dispatch_semaphore相关的共有3个方法,分别是dispatch_semaphore_create,dispatch_semaphore_wait,dispatch_semaphore_signal下面我们逐一了解一下这三个方法。
dispatch_semaphore_create方法用于创建一个带有初始值的信号量dispatch_semaphore_t,创建的时候需要传递一个value值,这个值作为后续判断是否阻塞的关键值。
dispatch_semaphore_wait这个方法主要用于等待或减少信号量,每次调用这个方法,信号量的value都会进行-1,
当value的值小于0的时候,就会阻塞当前线程
。dispatch_semaphore_signal方法用于让信号量的value进行+1操作,然后向下继续执行。如果先前信号量的值小于0,那么这个方法还会唤醒先前等待的线程。
线程同步示例代码:
- (void)dispatch_semaphore {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"任务1开始");
sleep(3);
NSLog(@"任务1结束");
sleep(1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);
NSLog(@"任务2开始");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);
NSLog(@"任务3开始");
});
}
线程安全实例代码:
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
- (void)setSomeValue:(id)value {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
_value = value;
dispatch_semaphore_signal(lock);
}
4.2.3 dispatch_barrier_async
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来。
dispatch_barrier_async函数会将自己要执行的方法加到队列当中,等待前边加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
主要是起到了一个阻隔前后两个任务组的作用。
* dispatch_barrier 是一个类似于dispatch_async()/dispatch_sync()的API,它可以将barrier block提交到队列中,barrier block 只有提交到自定义的并发队列中才能真正的当做一个栅栏,它在这里起到一个承上启下的作用,只有比它(barrier block)先提交到自定义并发队列的block全部执行完成,它才会去执行,等它执行完成,在它之后添加的block才会继续往下执行。
* 当dipatch_barrier block没有被提交到自定义的串行队列中,它与dispatch_async()/dispatch_sync()的作用是一样的。
* barrier_async与barrier_sync的区别在于,barrier_sync会阻塞它之后的任务的入队,必须等到barrier_sync任务执行完毕,才会把后面的异步任务添加到并发队列中,而barrier_async不需要等自身的block执行完成,就可以把后面的任务添加到队列中。
作者:左耳钉zed
链接:https://juejin.cn/post/6844903833831735310
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
示例代码:
- (void)dispatch_barrier {
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"Task 1,%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"Task 2,%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"Task 3,%@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"barrier");
});
dispatch_async(queue, ^{
NSLog(@"Task 4,%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"Task 5,%@",[NSThread currentThread]);
});
}
4.2.4 dispatch_group
有了上面的铺垫,group 是一个非常容易理解的概念,我们先看看如何创建 group:
dispatch_group_t dispatch_group_create(void) {
dispatch_group_t dg = _dispatch_alloc(DISPATCH_VTABLE(group), sizeof(struct dispatch_semaphore_s));
_dispatch_semaphore_init(LONG_MAX, dg);
return dg;
}
没错,group的本质就是一个 value 为 LONG_MAX 的信号量。
enter、leave、notify、wait
dispatch_group_wait :在任务组完成时调用,或者任务组超时是调用(完成指的是enter和leave次数一样多)。
dispatch_group_notify:不管超不超时,只要任务组完成,会调用,不完成不会调用
dispatch_group_wait先于dispatch_group_notify被调用。
notify和wait二者的区别:
- notify是当enter、leave数量一致时候,调用。
- wait是暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("Queue Name", DISPATCH_QUEUE_CONCURRENT);
for (int i=0; i<10 ; i++) {
dispatch_group_enter(group);
dispatch_async(queue, ^{
//任务处理结束
dispatch_group_leave(group);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//回归主线程,处理其他事情
});
4.2.5 dispatch_queue
GCD 为我们提供了两类queue,串行队列和并行队列。两者的区别是:
- 串行队列中,按照 FIFO的顺序执行任务,前面一个任务执行结束,后面一个才开始执行。
- 并行队列中,是按照 FIFO 的顺序开始执行任务,只要前一个被拿去执行,继而后面一个就开始执行,后面的任务无需等到前面的任务执行完再开始执行。
我们主要使用以下三种队列:
- 主队列: dispatch_get_main_queue,一切的UI操作务必在主队列中操作。
- 全局并行队列: dispatch_get_global_queue。分为DISPATCH_QUEUE_PRIORITY_HIGH、DISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW、DISPATCH_QUEUE_PRIORITY_BACKGROUND这四个优先级。
- 创建自定义队列 :自己来定义串行或者并行队列,来执行一些相关的任务。
dispatch_queue_t queue = dispatch_queue_create("QueueName", DISPATCH_QUEUE_CONCURRENT);
//DISPATCH_QUEUE_SERIAL 串行
//DISPATCH_QUEUE_CONCURRENT 并行
全局并行队列与创建的自定义队列的区别:
- 自定义队列可以利用dispatch_queue_create方法来创建名字,便于追踪错误。
- dispatch_queue_create可以设置为串行或者并发队列,而dispatch_get_global_queue默认为并发队列。
需要注意的是,在主线程下,同步切换到主队列,会造成死锁,所以切换之前最好判断下当前的线程,以防出现错误。
下表是同步异步和串行并行之间的关系:
串行 | 并发 | 主队列 | |
---|---|---|---|
dispatch_sync | 不开启新线程 | 不开启新线程 | 死锁 |
dispatch_async | 开启新线程 | 开启新线程 | 主线程运行 |
4.2.6 dispatch_queue与dispatch_group两者关系和区别
这两者,有些同学可能会出现概念上和使用上的混淆。
1、dispatch_queue是一个队列,根据设置的同步和异步,可以是主线程也可以是子线程,同时也可以规定在当前的执行是串行还是并行。粗暴的说,可以说是规定了任务运行的区域以及规则。具体执行顺序可参考:GCD编程中串行、并行、同步、异步的执行顺序
2、对于dispatch_group来说,他是作为一个任务的组(Group),让多个任务之间存在着一种联系,在所有的任务执行完后做一些总结性处理。上面也提到了,dispatch_group本身也需要去挂在某一个dispatch_queue上去执行,但是同一个group可以挂在不同的queue上。粗暴的说,dispatch_group不关心同步还是异步,是否是一个队列,也不关心串行和并行,只要关心的是任务之前的关系。
综上,可以表述为,一群有关系的操作,在某一个线程下,进行串行或者并行操作,在全部结束之后,进行下一步的操作。
4.2.7 dispatch_io与dispatch_data
在看这个名字的时候,大家起始就可以联想到,这是和I/O操作有关的类。
由于大家在实际使用的时候,实在是非常少,我也只是在最近一个项目当中,作为I/O的方案接触过一些,所以在这里就不在赘述了,大家可以参考GCD(Dispatch I/O)这篇文章来接触这个独特的工具。
另外一个典型使用,是一个叫做PeerTalk的框架,他实现了利用数据线和iPhone进行数据沟通的能力。看上去腾讯的一个UI调试软件Lookin也使用作为数据传输框架。
4.2.8 dispatch_object
根据APPLE的文档,dispatch_object继承于OS_dispatch_object。
4.2.9 dispatch_source
字面理解为GCD调度源,它用于处理特定的系统底层事件,即:当一些特定的系统底层事件发生时,调度源会捕捉到这些事件,然后可以做相应的逻辑处理。
Dispatch Source是BSD系统内核惯有功能kqueue的包装,kqueue是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。它的CPU负荷非常小,尽量不占用资源。当事件发生时,Dispatch Source会在指定的Dispatch Queue中执行事件的处理。
4.2.10 dispatch_once的实现
+ (instancetype)sharedInstance {
static Class *class;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
class = [[Class alloc] init];
});
return class;
}
以上就是我们常用的创建代理的方法,那么dispatch_once是如何做到只执行一次的呢?
4.3 NSOperation
NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
在NSOperation上,我们接触最多的应该是NSOperation
和NSOperationQueue
,以及继承自NSOperation
的NSBlockOperation
和NSInvocationOperation
。
使用上,因为NSOperation
是一个抽象类,所以在使用的时候,可以使用继承自NSOperation
的NSBlockOperation
和NSInvocationOperation
,或者我们也可以自行去继承NSOperation
。
相对来说,简化了一些同时也增强了一些,比如弱化了对于串行和并行的逻辑,增强了对于队列的使用,也提供了对于任务之间的依赖逻辑。
NSOperation需要配合NSOperationQueue来实现多线程。因为默认情况下,NSOperation单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue才能实现异步执行。
因为NSOperation是基于GCD的,那么使用起来也和GCD差不多,其中,NSOperation相当于GCD中的任务,而NSOperationQueue则相当于GCD中的队列。NSOperation实现多线程的使用步骤分为三步:
1、创建任务:先将需要执行的操作封装到一个NSOperation对象中。
2、创建队列:创建NSOperationQueue对象。
3、将任务加入到队列中:然后将NSOperation对象添加到NSOperationQueue中。
之后呢,系统就会自动将NSOperationQueue中的NSOperation取出来,在新线程中执行操作。
4.3.1 子类NSInvocationOperation
NSOperation作为一个抽象类,不能直接调用,但是仍然给我们提供了可以调用的子类,NSInvocationOperation是其中之一。
通过以下方法创建:
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
创建结束之后,调用父类方法:
- (void)start;
来执行这个NSOperation,需要注意的是,执行这个方法,不会引发多线程,操作仍然会在当前的线程进行操作。
4.3.2 子类NSBlockOperation
和NSInvocationOperation一样,NSBlockOperation也是的子类,也同样可以调用父类的方法。根据名字可以知道,大部分操作是根据Block来调用的,操作使用和GCD类似。
通过以下方法创建,在block中执行方法:
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
创建结束之后,调用父类方法:
- (void)start;
单独执行这个方法,也不会引发多线程,操作仍然会在当前的线程进行操作。
NSBlockOperation额外还有一个方法:
- (void)addExecutionBlock:(void (^)(void))block;
这个允许大家增加更多的任务到Operation中。
然而!!!当增加的任务达到一定数量之后,机会出现异步并发的现象,时机是系统来控制,这个需要注意。
4.3.3 NSOperationQueue
获取NSOperationQueue有两种方法,一种是
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
这种是获取主队列,和GCD一样主队列和主线程是相互绑定的,除了上面提到的addExecutionBlock方法之外,都是在主线程串行调用。
另一种是
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
这个时候创建的队列,是在子线程,并且包含串行和并行两种队列,具体怎么调用,下面会讲到。
通常情况下,我们会调用
- (void)addOperation:(NSOperation *)op;
来将已经创建好的Operation加入到NSOperationQueue中,加入之后不需要调用start方法,就可以直接开始执行任务。
同时,我们也可以调用
- (void)addOperationWithBlock:(void (^)(void))block;
来直接将block中的任务加入到NSOperationQueue中。
4.3.4 NSOperationQueue的串行和并行
上面两种方法,都是默认打开子线程,并且执行的并发操作,我们需要控制串行的时候,可以通过设置maxConcurrentOperationCount这个属性来控制。
maxConcurrentOperationCount,叫做最大并发操作数,用来控制一个NSOperationQueue中可以有多少个任务可以参与并发执行。
* maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
* maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
* maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
4.3.5 NSOperation 操作依赖
相对于GCD来说,这个可以设置任务之间的依赖关系,从而来控制两个任务之间先后顺序,使用场景类比于GCD的dispatch_semaphore.
一共提供了三个API:
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
4.3.6 线程安全以及线程间沟通
* NSOperation和NSOperationQueue因为是封装于GCD,同样的也是线程不安全的,使用同一块内存或者同一块数据的时候,需要加锁来保证数据安全
* 和GCD一样,可以在任务执行结束之后,可以切换到**mainQueue**来进行操作。
4.3.7 阻塞线程操作
NSOperation:
- (void)waitUntilFinished;//阻塞当前线程,直到该任务结束,可用于线程执行顺序的同步。
NSOperationQueue:
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;//向队列中添加操作数组,wait 标志是否阻塞当前线程直到队列当中所有操作结束。
- (void)waitUntilAllOperationsAreFinished;//阻塞当前线程,直到队列中的所有操作结束。
线程的安全
atomic
OC在定义属性时有nonatomic
和atomic
两种选择.
atomic:原子属性,为setter方法加锁(默认就是atomic)。
nonatomic:非原子属性,不会为setter方法加锁。
atomic加锁原理
@property (assign, atomic) int age;
- (void)setAge:(int)age {
@synchronized(self) {
_age = age;
}
}
但这样的操作也并不能说是完美无缺的,因为这么操作的话,也会造成set的属性和get到的属性并不匹配。
线程锁
NSLock
NSLock遵循NSLocking协议,同时也是互斥锁,提供了lock和unlock方法来进行加锁和解锁。 NSLock内部是封装了pthread_mutext,类型是PTHREAD_MUTEXT_ERRORCHECK,它会损失一定的性能换来错误提示。
- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
===================================================
- (instancetype)init
{
self = [super init];
if (self) {
self.lock = [[NSLock alloc] init];
}
return self;
}
[self.lock lock];
//操作任务
[self.lock unlock];
tryLock和lock方法都会请求加锁,唯一不同的是trylock在没有获得锁的时候可以继续做一些任务和处理。lockBeforeDate:方法也比较简单,就是在limit时间点之前获得锁,没有拿到锁就返回NO。
@synchronized
这其实是一个 OC 层面的锁,防止不同的线程同时执行同一段代码,相比于使用 NSLock ,@synchronized不需要创建对象和持有对象,易用性更高
大体上,想要明白@synchronized,需要知道在@synchronized中 objc_sync_enter 和 objc_sync_exit 的成对调用,而且每个传入的对象,都会为其分配一个递归锁并存储在哈希表中。在objc_sync_enter中加锁,在objc_sync_exit 中解锁。
具体可以参考这篇文章:关于 @synchronized,这儿比你想知道的还要多
@synchronized(self) {
//数据操作
}
dispatch_semaphore
dispatch_semaphore是GCD用来同步的一种方式,作为线程锁使用的时候一共有三个函数,分别是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
dispatch_semaphore_create(long value);//创造信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);//等待信号
dispatch_semaphore_signal(dispatch_semaphore_t dsema);//发送信号
====================================================================
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
- (void)setSomeValue:(id)value {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
_value = value;
dispatch_semaphore_signal(lock);
}
dispatch_semephore_create方法用户创建一个dispatch_semephore_t类型的信号量,初始的参数必须大于0,该参数用来表示该信号量有多少个信号,简单的说也就是同事允许多少个线程访问。
dispatch_semaphore_wait方法是等待一个信号量,该方法会判断signal的信号值是否大于0,如果大于0则不会阻塞线程,消耗点一个信号值,执行后续任务。如果信号值等于0那么就和NSCondition一样,阻塞当前线程进入等待状态,如果等待时间未超过timeout并且dispatch_semaphore_signal释放了了一个信号值,那么就会消耗掉一个信号值并且向下执行。如果期间一直不能获得信号量并且超过超时时间,那么就会自动执行后续语句。
遭遇过的bug
1、多线程异步调用同一块内存
老实说,凡是有了一些线程安全方面的了解的同学,都会去做一些线程安全方面的操作,也就是说,基本不存在多线程异步调用同一块资源的情况,但是这个问题确是很常见的。
我遇到的时候,起始就是同事在没有通知我的时候,自己开辟了一个子线程来异步调用一个数据,导致我在主线调用的时候出现了数据异常的情况。
解决办法也比较简单,就是简单的加一个锁。
2、调用超过了上限
通常存在大量切换线程会造成App会产生“闪退”的现象,但本质上和崩溃还是存在一定的区别。这种就是在".CRASH"文件中能看到的EXC_RESOURCE WAKEUP的Event信息。
我在APPLE官方描述中找到这样的话:
大致表达的意思是,不合理的线程管理会导致每秒内的CPU切换过多,如此会造成大量消耗电池资源和其他资源。
我之前遇到过的一次是去取得音频数据的过程中,在没有取到正确音频数据之后没有做suspend操作,导致一直处于遍历循环当中。因为我们会对音频数据错误的信息记录Log,而记录Log势必存在独立的Quene,所以线程在音频处理线程和Log处理线程当中飞速的切换,一旦而没有从切换的Loop的出来,那么很快切换线程的次数就会达到上限。