内容概况:
- 锁:互斥锁、自旋锁、同步锁
- 信号量:(Semaphore)
- 原子操作:atomic 不会被线程调度机制打断的操作
(非原子操作 nonatomic) - 同步:共享数据在同一时刻只被一个线程使用
- 进程间的通信:URL Scheme、钥匙串、粘贴板、DocumentVC、UIActivityVC、TCP通信、隔空传送、APP Groups
- 多线程:pthread、NSThread、GCD、NSOperation
- 线程的生命周期:创建、就绪、运行、阻塞、死亡
- GCD:(Grand Central Dispatch)多核并行运算的C语言框架。会自动利用更多的CPU内核,系统管理线程生命周期
- 队列Queue:
以下4个GCD方法的区别:
dispatch_async 异步执行Block
dispatch_sync 同步执行Block
dispatch_barrier_async 为异步执行调度队列:提交一个路障
dispatch_barrier_sync 为同步执行调度队列:提交一个路障
dispatch_get_main_queue 主队列(特殊的 串行队列)
dispatch_get_global_queue 全局并行队列:
dispatch_queue_t (队列组-泛型:可以使用以下3个标志符创建)
1.dispatch_queue_serial: 串行队列:先进先出顺序,连续调用块调
2.dispatch_queue_concurrent: 并行队列:可并发 调用块和支持
3.dispatch_queue_priority_default: 全局并行队列:并发调用
- 同步执行:不会到线程池里面去获取子线程(排队)
- 异步执行:只要有任务,就会去线程池取子线程(主队列除外)
一、基础概念
锁的原理:必须先得到锁,在访问完共享资源后,必须释放锁。
互斥锁:标记时刻,只能有一个线程访问该对象
如果资源已经被占用,资源申请者只能进入睡眠状态。
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
锁操作
:主要包括三个方式,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。
加锁 pthread_mutex_lock()
解锁 pthread_mutex_unlock()
测试加锁 pthread_mutex_trylock()
,
锁机制
:同时也不是异步信号安全的,也就是说,不应该在信号处理过程中使用互斥锁,否则容易造成死锁。
自旋锁:循环查看线程是否可用
不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看
该自旋锁的使用者是否已经释放了锁,"自旋"一词就是因此而得名。
适合于保持时间非常短的情况,它可以在任何上下文使用。
同步锁:保证线程执行原子操作
同步锁是为了保证每个线程都能正常执行原子不可更改操作,同步监听对象/同步锁/同步监听器/互斥锁的一个标记锁。(即:监听标记锁)
它一般至少会包含两个功能:
1. 给资源加锁;
2. 给资源解锁;
另外,它一般还有 等待/通知 功能, 即 wait/notify。
同步的基本思想:
目的:为了保证共享数据在同一时刻只被一个线程使用,我们有一种很简单的实现思想,就是
1.在共享数据里保存一个锁 ,当没有线程访问时,锁是空的。
2.当有第一个线程访问时,就 在锁里保存这个线程的标识 并允许这个线程访问共享数据。
3.在当前线程释放共享数据之前,如果再有其他线程想要访问共享数据,就要 等待锁释放 。
jvm中有以下三种锁(由上到下越来越“重量级”):
- 偏向锁
- 轻量级锁
- 重量级锁
信号量
信号量(Semaphore
),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
(信号量就相当于,一个宾馆房间的钥匙,每个线程就相当于一个人,当一个人不用了,另一个人才能用)
原子操作:atomic(非原子操作 nonatomic)
是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)
1、acomic就是线程安全,但是一般使用nonacomic,因为acomic的线程安全开销太大
,影响性能,即使需要保证线程安全,我们也可以通过自已的代码控制,而不用acomic。
2、atomic 会各在getter和setter方法中生成锁,确保线程安全。
atomic 只能保证getter和setter方法单独是线程安全的,不能保证两者同时调用时是线程安全的。(为了保证线程安全,要给getter和setter 加上一个锁,当setter 才能执行getter,防止数据污染)
比如:线程A调用了getter,线程B和C调用setter,此时线程A收到的返回值可能是初始值,可能是线程B设置的值,也可能是线程C设置的值。
3、由于nonatomic不会生成锁,所以执行速度会更快。
synchronized和Lock的区别是什么?
1、lock是一个接口,而synchronized是关键字。
2、synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。”
谈谈iOS多线程的锁
二、多线程、GCD
1、进程:是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内。
引用:iOS进程间的通信
1、URL scheme:openURL联检通信
2、Keychain:钥匙串,即使APP被删除之后,keychain里面的信息依然存在
3、UIPasteBoard:粘贴板
4、UIDocumentInteractionController:同设备上APP之间的共享文档,以及文档预览、打印、发邮件和复制等功能。
5、UIActivityViewController:系统分享功能
6、Local socket:TCP通信(即时通讯)
7、AirDrop:隔空传送
8、APP Groups:同一个开发团队开发的App之间数据共享
2、线程:线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程至少要有一条线程。程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程。
多线程的原理:
1、单核CPU:同一时间,CPU 只能处理 1 个线程,只有 1 个线程在执行。
2、多核CPU:同一时间,能处理 多个线程。
多线程同时执行(单核CPU):
- 是 CPU 快速的在多个线程之间的切换
- CPU 调度线程的
时间足够快
,就造成了多线程的“同时”执行的效果 - 如果线程数非常多
- CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源
- 每个线程被调度的次数会降低,线程的执行效率降低
多线程优/缺点:
- 优点
1.能适当提高程序的执行效率
2.能适当提高资源的利用率(CPU,内存)
3.线程上的任务执行完成后,线程会自动销毁 - 缺点
- 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
结束这个线程:
a)break;
b)[thread cancel]
基础概念
1、pthread:
跨平台,适用于多种操作系统,可移植性强,是一套纯C语言的通用API,且线程的生命周期需要程序员自己管理
,使用难度较大,所以在实际开发中通常不使用。
2、NSThread
基于OC语言的API,使得其简单易用,面向对象操作。线程的生命周期由程序员管理
,在实际开发中偶尔使用。
[self performSelectorInBackground:@selector(multithread) withObject:nil];
3、GCD
基于C语言的API,充分利用设备的多核,旨在替换NSThread等线程技术。线程的生命周期由系统自动管理
,在实际开发中经常使用。
4、NSOperation
基于OC语言API,底层是GCD,增加了一些更加简单易用的功能,使用更加面向对象。线程生命周期由系统自动管理
,在实际开发中经常使用。
2、NSThread
线程的生命周期:创建、就绪、运行、阻塞、死亡
1、创建:实例化对象
2、就绪:向线程对象发送start消息,线程对象被加入“可调度线程池”等待CPU调度;detach方法
和performSelectorInBackground方法
会直接实例化一个线程对象并加入“可调度线程池”
3、运行:CPU负责调度“可调度线程池”中线程的执行,线程执行完成之前,状态可能会在“就绪
”和“运行
”之间来回切换,“就绪”和“运行”之间的状态变化由CPU负责,程序员不能干预。
4、阻塞:当满足某个预定条件时,可以使用休眠或锁阻塞线程执行,影响的方法有:sleepForTimeInterval
,sleepUntilDate
,@synchronized(self) 线程锁
;线程对象进入阻塞状态后,会被从“可调度线程池”中移出,CPU 不再调度。
sleepForTimeInterval
:睡眠时间间隔(休息一段时间。)
sleepUntilDate
: 睡眠直到日期
@synchronized(self)
: 线程锁
5、死亡(两种死亡方式)
• 正常死亡:线程执行完毕。
• 非正常死亡:线程内死亡-->[NSThread exit]: 强行中止后,后续代码都不会在执行线程外死亡:[threadObj cancel]-->通知线程对象取消,在线程执行方法中需要增加isCancelled
判断,如果isCancelled == YES
,直接返回。
死亡后线程对象的isFinished属性为YES;如果是发送cancle消息,线程对象的isCancelled属性为YES;死亡后stackSize == 0,内存空间被释放。
NSThread 线程间通信
在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新UI。这就涉及到了子线程和主线程之间的通信。看一下官方关于NSThread的线程间通信的方法。
经典案例:下载图片来展示线程之间的通信。具体步骤如下:
1、开启一个子线程,在子线程中下载图片。
2、回到主线程刷新UI,将图片展示在UIImageView中。
NSThread线程安全
线程安全:也可以被称为线程同步
,主要是解决多线程争抢操作资源的问题(不会出现数据污染),就比如火车票,全国各地多个售票窗口同事去售卖同一列火车票。怎么保证,多地售票的票池保持一致,就需要用到多线程同步的技术去实现了。
NSThread线程安全和GCD、NSOperation线程安全都是一样的,实现方法无非就是加锁(各种锁的实现)、信号量、GCD栅栏等。
实现互斥锁
案例(多线程抢票):
总共有100张火车票,开启两个线程,北京和上海两个窗口同时卖票,卖一张票就减去库存,使用锁,保证北京和上海卖票的库存是一致的。实现如下。
GCD
全称:Grand Central Dispatch)是苹果为多核并行运算
提出的C语言并发技术框架。
特点:
1、GCD会自动利用更多的CPU内核;
2、会自动管理线程的生命周期(创建线程,调度任务,销毁线程等);程序员只需要告诉GCD想要如何执行什么任务,不需要编写任何线程管理代码。
3、繁杂耗时较长的计算任务可以通过GCD分配给其他线程来完成任务.
4、如果没有特殊需求,不应引入线程增加程序复杂度
5、谨慎对待线程阻塞。
GCD底层实现:
我们使用的GCD的API是C语言函数,全部包含在LIBdispatch库
中,DispatchQueue
通过 结构体和链表被实现为FIFO的队列;而FIFO
(First In First Out)的队列是由dispatch_async
等函数追加的Block来管理的;Block不是直接加入FIFO队列,而是先加入Dispatch Continuation
结构体,然后在加入FIFO队列,Dispatch Continuation用于记忆Block所属的Dispatch Group和其他一些信息(相当于上下文)。
Dispatch Queue 可通过 dispatch_set_target_queue()设定,可以设定执行该Dispatch Queue处理的Dispatch Queue为目标。该目标可像串珠子一样,设定多个连接在一起的Dispatch Queue,但是在连接串的最后必须设定 Main Dispatch Queue,或各种优先级的 Global Dispatch Queue,或是准备用于Serial Dispatch Queue
的Global Dispatch Queue
。
Global Dispatch Queue的8种优先级:
1. High priority
2. Default Priority
3. Low Priority
4. Background Priority
5. High Overcommit Priority
6. Default Overcommit Priority
7. Low Overcommit Priority
8. Background Overcommit Priority
全局队列:dispatch_get_global_queue
参数1:涉及系统适配
iOS 8.0 服务质量
QOS_CLASS_USER_INTERACTIVE 用户交互(希望线程快速执行,不要放一些耗时操作)
QOS_CLASS_USER_INITIATED 用户需要的(不要放一些耗时操作)
QOS_CLASS_DEFAULT 默认
QOS_CLASS_UTILITY 使用工具(用来耗时操作)
QOS_CLASS_BACKGROUND 后台
QOS_CLASS_UNSPECIFIED 没有指定优先级
iOS 7.0 调度优先级
DISPATCH_QUEUE_PRIORITY_HIGH 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级
DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台优先级
参数2:为未来的保留参数
提示:尤其不要选择BACKGROUND,线程执行会慢到令人发指!
以下4个GCD方法的区别:
dispatch_async
异步执行Block
dispatch_sync
同步执行Block
dispatch_barrier_async
为异步执行调度队列:提交一个路障
dispatch_barrier_sync
为同步执行调度队列:提交一个路障
dispatch_get_main_queue
主队列(特殊的 串行队列)
dispatch_get_global_queue
全局并行队列:
dispatch_queue_t (队列组-泛型:可以使用以下3个标志符创建)
1.DISPATCH_QUEUE_SERIAL
: 串行队列:先进先出顺序,连续调用块调
2.DISPATCH_QUEUE_CONCURRENT
: 并行队列:可并发 调用块和支持
3.DISPATCH_QUEUE_PRIORITY_DEFAULT
: 全局并行队列:并发调用
dispatch_queue_serial
dispatch_queue_concurrent
dispatch_queue_priority_default
建立栅栏
它的作用可以用一个词概括 -承上启下,它保证此前的任务都先于自己执行,此后的任务也迟于自己执行。当然它的作用导致它只有在并行队列中有意义。
# 建立栅栏_执行任务
- dispatch_barrier_async:不会阻塞Block的当前线程(异步提交)
- dispatch_barrier_sync:会阻塞提交Block的当前线程
dispatch_barrier_async(queue, ^{
NSLog(@"--------- dispatch_barrier_async 异步栅栏任务 ");
});
- dispatch_sync:并不能阻塞并行队列
例五:例如我们在一个读写操作中,我们就可以如下使用:
//一个读写操作中:我们要知道一个数据,读与读
之间是可以用线程并行的,但是 写与写
、写与读
之间,就必须串行同步或者使用线程锁来保证线程安全。我们可以用 dispatch_barrier_async 保证线程安全。
- 读与读: 线程并行
- 写与写: 必须串行同步或者使用线程锁
- 写与读: 必须串行同步或者使用线程锁
- (void)case5 {
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"读操作_1");
});
dispatch_async(queue, ^{
NSLog(@"读操作_2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"建立栅栏:写操作_1");
});
dispatch_barrier_async(queue, ^{
NSLog(@"建立栅栏:写操作_2");
});
dispatch_async(queue, ^{
NSLog(@"读操作");
});
}
结论:这样写操作的时候,始终只有它这一条线程在进行。而读操作一直是并行的。这么做充分利用了多线程的优势,还不需要加锁,减少了相当一部分的性能开销。实现了读写操作的线程安全。
例六: 4)dispatch_sync 和 dispatch_barrier_sync的区别。
二者因为是sync提交,所以都是阻塞当前提交Block主线程。
//而它俩唯一的区别是:dispatch_sync 不能阻塞并行队列。案例如下:
- (void)case6 {
dispatch_queue_t queue = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
dispatch_async(queue, ^{
NSLog(@"任务二");
});
dispatch_async(queue, ^{
NSLog(@"任务三");
});
//睡眠2秒
//[NSThread sleepForTimeInterval:2];
NSLog(@"任务一 ");
});
NSLog(@"-------------- \n ");
//输出结果: 任务三 任务二 任务一 或:2、1、3 或:3、1、2 或:2、3、1 或3、2、1
//(一、二、三 顺序不固定,很显然,并行队列没有被sync所阻塞。)
}
例七: dispatch_barrier_sync
、dispatch_barrier_sync
可以阻塞并行队列,栅栏任务先执行(栅栏作用的体现):
- (void)case7 {
dispatch_queue_t queue = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_sync(queue, ^{
NSLog(@"-------------- \n ");
dispatch_async(queue, ^{
NSLog(@"任务二:异步任务,放在并行队列中执行");
});
dispatch_async(queue, ^{
NSLog(@"任务三:异步任务,放在并行队列中执行");
});
//睡眠2秒
//[NSThread sleepForTimeInterval:2];
NSLog(@"任务一:同步栅栏");
});
//输出结果: 任务一 任务二 任务三 (一首先,二、三顺序不固定)
}
总结:
相同点:dispatch_sync
、dispatch_barrier_sync
都能阻塞 主队列
不同点:dispatch_sync
不能阻塞并行队列
dispatch_barrier_sync
可以阻塞并行队列
GCD 核心概念:将任务添加到队列,指定任务执行的方法。
任务
- 使用block 封装
- block 就是一个提前准备好的代码块,需要的时候执行
-任务执行函数(任务都需要在线程中执行)
- 同步执行:不会到线程池里面去获取子线程(排队)
- 异步执行:只要有任务,就会去线程池取子线程(主队列除外)
小结:
-- 开不开线程,取决于执行任务的函数,同步不开,异步就开
-- 开几条线程,取决于队列,串行开一条,并发开多条(异步)
有两种队列(串行队列/并发队列),两种任务执行方式(同步执行/异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:
1、同步执行 + 串行队列
3、同步执行 + 并发队列
2、异步执行 + 串行队列
4、异步执行 + 并发队列
系统还提供了两种特殊队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是主队列因为有点特殊,所以我们就又多了两种组合方式。这样就有六种不同的组合方式了。
5、同步执行 + 主队列
6、异步执行 + 主队列
例七:信号量 dispatch_semaphore_wait
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
这句话什么意思?
判断信号量是否为0,是就执行,不是就等待(原始信号为1)
调用此函数,原子性减1后,进入等待状态直到有信号唤醒。
- (void)case7 {
dispatch_queue_t myQueue = dispatch_queue_create("com.ds.creatqueue", DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t sema = dispatch_semaphore_create(1); # 创建信号量
for (NSInteger count = 0; count < 10; count ++) {
dispatch_async(myQueue, ^{
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
sleep(5);
dispatch_semaphore_signal(sema);
});
NSLog(@"---count= %ld ",(long)count);
});
}
}
# dispatch_async 不是立即执行的 只是提交到队列。
dispatch_async(dispatch_get_main_queue(), ^{
sleep(5);
dispatch_semaphore_signal(sema);
});
主线程等待,然后又把启动信号放到之后的主线程队列执行,那就是卡死了;
信号量创建时候赋值0,等待的函数放在 dispatch_async 后是不是一样效果?
一样效果,都会卡死,主线程要慎用 此类函数。
阻塞当前线程
在主队列 添加 同步Block任务: 阻塞当前线程(会崩溃)
- (void)chokeThread {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"111");
});
//同上
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
NSLog(@"222");
});
}
同步Block任务 添加到 异步Block任务中, 总结:
1、调用 串行队列、主队列 都会线程卡死(主队列造成死锁,任务相互等待)
2、调用 并行队列;不会线程卡死
-(void)testGCD2{
//示例二: 以下代码会产生什么结果?
//会奔溃
dispatch_queue_t queue11 = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
//会奔溃
//dispatch_queue_t queue11 = dispatch_get_main_queue();
//不会奔溃
//dispatch_queue_t queue11 = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"异步async之前 - %@", NSThread.currentThread );
# 1.往这个queue中添加一个 异步线程: async
dispatch_async(queue11, ^{ // 异步执行 + 串行队列
dispatch_sync(queue11, ^{// 同步执行 + 串行队列(当前)
NSLog(@"同步sync时 - %@", NSThread.currentThread );
});
});
NSLog(@"异步async之后 - %@", NSThread.currentThread );
}
队列组(dispatch_group_T)
dispatch_group 是基于 dispatch_semaphore 实现的。
1、dispatch_group_notify
:可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了。
2、dispatch_group_async
:多次使用队列组的方法执行任务, 只有异步block方法。监听一组任务,配合 dispatch_group_notify 使用。
-(void)grouQueue{
//1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t _mainQueue = dispatch_get_main_queue();
//2.创建队列组
dispatch_group_t group = dispatch_group_create();
//3.1、global_queue 分派组异步:执行任务
dispatch_group_async(group, queue, ^{
//[NSThread sleepForTimeInterval:1];
NSLog(@"-------------group_task 3.1 - %@ ", [NSThread currentThread]);
});
//3.2、global_queue
dispatch_group_async(group, queue, ^{
//[NSThread sleepForTimeInterval:1];
NSLog(@"-------------group_task 3.2 - %@ ", [NSThread currentThread]);
});
//3.3、main_queue 主队列中执行的任务,在其他队列中最后执行,
dispatch_group_async(group, _mainQueue, ^{
//[NSThread sleepForTimeInterval:1];
NSLog(@"-------------group_task 3.3 _mainQueue - %@ ", [NSThread currentThread]);
});
//4.完成通知: 全完成后会自动通知,不管放在前、还是后,都在所有队列任务完成以后通知
dispatch_group_notify(group, _mainQueue, ^{
NSLog(@"---notify:任务完成 通知队列 - %@ \n\n_", [NSThread currentThread]);
});
//3.4、global_queue
dispatch_group_async(group, queue, ^{
//[NSThread sleepForTimeInterval:1];
NSLog(@"-------------group_task 3.4 - %@ ", [NSThread currentThread]);
});
//3.5、global_queue
dispatch_group_async(group, queue, ^{
//[NSThread sleepForTimeInterval:1];
NSLog(@"-------------group_task 3.5 - %@ ", [NSThread currentThread]);
});
// dispatch_queue_t queue5 = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);
//
// dispatch_barrier_async(queue5, ^{ // 为异步执行调度队列:提交一个路障
// NSLog(@"---> barrier_async ");
// });
// dispatch_barrier_sync(queue5, ^{ // 为同步执行调度队列:提交一个路障
// NSLog(@"---> barrier_sync ");
// });
// NSLog(@"---dispatch_barrier_sync ");
// for (NSInteger i = 0; i < 3; i++) {
// NSLog(@"group-11 - %@", [NSThread currentThread]);
// }
}
点击返回按钮以后,取消GCD子线程
dispatch_suspend(downloadNewsQueue);
dispatch_release(downloadNewsQueue);
downloadNewsQueue = nil;
注意
:GCD 在使用时,也要判断一下downloadNewsQueue是否为nil
未完待续。。。
参考文章:
1、同步锁Synchronized及其实现原理