文章目录
- 前言
- 1. 你理解的多线程
- 优点
- 缺点
- 2. atomic 和 nonatomic 的区别及其作用
- 3. GCD的队列类型 - 三种队列类型
- 4. GCD的死锁问题
- 线程死锁的四个必要条件
- 5. 多线程之间的区别和联系
- 6. 进程和线程?
- 进程间的通信方式
- 线程间的通信方式
- 6. iOS的线程安全手段如何保证
前言
iOS 锁和多线程的总结
1. 你理解的多线程
多线程是同时执行多个线程(子任务)的能力,用于提高程序性能和响应性。它允许在一个程序中并发地处理多个任务。
- 并发:在一个时间段多个线程同时进行,计算机通过切换不同的线程实现多线程任务。
优点
- 大大提高了程序的运行速度。
- 使用线程可以把占据时间较长的任务放到后面去处理,从而提升用户的体验。
缺点
- 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
- 更多的线程需要更多的内存空间。
2. atomic 和 nonatomic 的区别及其作用
- atomic原子操作:加锁,保证setter和getter存取方法的线程安全(仅仅对setter和getter方法加锁)。因为线程加锁,别的线程访问当前属性的时候会先执行完属性当前的操作。
- 对同一对象的set和get的操作是顺序执行的。
- 速度不快,因为要保证操作整体完成。
- 线程安全,需要消耗大量系统资源为属性加锁。
- 使用
atomic
并不能保证绝对的线程安全,因为atomic
仅仅是对系统生成的的setter
和getter
方法加锁, 对于绝对保证线程安全的操作,需要使用更高级的方式处理,NSSpinLock
, @syncronized
锁保证线程安全。
- nanatomic非原子操作,不加锁,线程执行快,但是多个线程访问同一个属性可能产生crash。
- 不是默认的
- 速度更快,如果有两个线程访问同一个属性可能造成crash。
- 非线程安全
- atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样
atomic
系统自动生成的getter
/setter
方法会进行加锁操作。nonatomic
系统自动生成的getter
/setter
方法不会进行加锁操作。
⚠️:atomic
修饰的属性,系统生成的 getter/setter
会保证 get
、set
操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter
:那么线程 A 的 getter
还是能得到一个完好无损的对象。
3. GCD的队列类型 - 三种队列类型
- The main queue(主线程串行队列)与主线程功能相同,提交到main queue的任务会在主线程中执行。
dispatch_get_main_queue() 来获取
- Global queue(全局并发队列) 全局并发队列由整个进程共享,有 高 中(默认是中) 低和后台四个优先级别。
dispatch_get_global_queue() 可以设置优先级
- Custom queue(自定义队列) 可以串行,也可以并发。
dispatch_queue_create()
4. GCD的死锁问题
线程死锁的四个必要条件
- 互斥: 一个资源每次都只能被一个进程占用。
- 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
- 不可抢占:别人已经占有了某项资源,你不能因为自己需要资源而去抢占其他资源。
- 循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需要的至少一种资源。
概念:所谓死锁,通常是两个线程A和B都卡住了,A在等B,B在等A,互相等待到值死锁。
- .主线程串行队列同步执行任务,在主线程运行时,会产生死锁
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3
分析:
dispatch_sync
表示是一个同步线程;dispatch_get_main_queue
表示运行在主线程中的主队列;- 任务2是同步线程的任务。
- 任务3需要等待任务2结束之后再执行.
为什么造成死锁?
- 首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是主队列,是一个特殊的串行队列,有任务来,当然会将任务加到队尾,然后遵循
FIFO
原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面
任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。【既然这样,那干脆就卡在这里吧】这就是死锁。
- 同步异步互相嵌套
// 同步 + 异步 互相嵌套产生死锁
- (void)sync_async {dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);NSLog(@"1"); // 任务1dispatch_async(queue, ^{NSLog(@"2"); // 任务2dispatch_sync(queue, ^{NSLog(@"3"); // 任务3});NSLog(@"4"); // 任务4});NSLog(@"5"); // 任务5
}
分析:首先通过自定义队列创建了dispatch_queue_create函数创建了一个DISPATCH_QUEUE_SERIAL的串行队列。
- 执行任务1.
- 遇到异步线程,将【任务2、同步线程、任务4】加入串行队列中。因为是异步线程,所以在主线程中的任务5不必等待异步线程中的所有任务完成;
- 因为任务5不必等待,所以2和5的输出顺序不能确定;
- 任务2执行完以后,遇到同步线程,这时,将任务3加入串行队列;
- 又因为任务4比任务3早加入串行队列,所以,任务3要等待任务4完成以后,才能执行。但是任务3所在的同步线程会阻塞,所以任务4必须等任务3执行完以后再执行。这就又陷入了无限的等待中,造成死锁。
主线程无限循环
- (void)async_loop {dispatch_async(dispatch_get_global_queue(0, 0), ^{NSLog(@"1"); // 任务1dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@"2"); // 任务2});NSLog(@"3"); // 任务3});NSLog(@"4"); // 任务4while (1) {}NSLog(@"5"); // 任务5// a打印 4 1 / 1 4 顺序不定
}
打印结果: 4 1 / 1 4 顺序不定
分析:
- 先来看看都有哪些任务加入了Main Queue:【异步线程、任务4、死循环、任务5】。
- 在加入到Global Queue异步线程中的任务有:【任务1、同步线程、任务3】。
- 第一个就是异步线程,任务4不用等待,所以结果任务1和任务4顺序不一定。
- 任务4完成后,程序进入死循环,Main Queue阻塞。但是加入到Global Queue的异步线程不受影响,继续执行任务1后面的同步线程。
- 同步线程中,将任务2加入到了主线程,并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。
最终,只能得到1和4顺序不定的结果。
5. 多线程之间的区别和联系
GCD和NSOperation
GCD
的执行效率更高,执行的是由Block构成的任务,是一个轻量级的数据结构,写起来更加方便GCD
只支持FIFO
队列,NSOperationQueue
可以通过设置最大并发数、设置优先级、添加依赖关系来调整执行顺序NSOperation
可以跨越队列设置依赖关系,GCD
仅仅能通过栅栏等方法才能控制执行顺序NSOperation
更加面向对象,支持KVO
,也可以通过继承等关系添加子类。- 所以如果我们需要考虑异步操作之间的顺序行、依赖关系,比如多线程并发下载等等,就使用
NSOperation
GCD 与 NSThread 的区别
NSThread
通过@selector
指定要执行的方法,代码分散, 依靠的是NSObject
的分类实现的线程之间的通讯,如果要开线程必须创建多个线程对象。经常只用的是[NSTread current]
查看当前的线程。NSThread
是一个控制线程执行的对象,它不如NSOperation
抽象,通过它我们可以方便的得到一个线程,并控制它。但NSThread
的线程之间的并发控制,是需要我们自己来控制的,可以通过NSCondition
实现。GCD
通过block
指定要执行的代码,代码集中, 所有的代码写在一起的,让代码更加简单,易于阅读和维护,不需要管理线程的创建/销毁/复用的过程!程序员不用关心线程的生命周期
6. 进程和线程?
参考:进程和线程的概念、区别及进程线程间通信
1. 基本概念:
- 进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
- 线程是进程的子任务,是CPU调度和分配的基本单位,用于保障程序执行的实时性,实现进程内部的并发。线程是操作系统可以识别的最小执行和调度单位。每个线程都肚子占用一个虚拟处理器等,每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
2. 区别:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
- 进程间不会相互影响 ;线程:一个线程挂掉将导致整个进程挂掉
- 线程之间的通信更加方便,同一个进程下线程共享全局变量,静态变量等数据。
3. 通信方式:
进程间的通信方式
- 进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
- 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
线程间的通信方式
- 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
- 互斥量
Synchronized
/Lock
:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问 - 信号量
Semphare
:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。 - 事件(信号),
Wait
/Notify
:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作进程间通信的方式:
6. iOS的线程安全手段如何保证
参考:iOS中有哪些技术可以保证线程安全?
问:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。此时,我们需要用线程锁来解决。
线程数据安全的方法:
- natomic原子操作:使用
atomic
多线程原子性控制,atomic
的原理给setter
加上锁,getter
不会加锁。 - 使用GCD实现atomic操作:给某字段的setter方法和getter方法加上同步队列;
- (void)setCount:(NSInteger)newcount
{dispatch_sync(_synQueue, ^{count = newcount;});
}
- (NSInteger)count
{__block NSInteger localCount;dispatch_sync(_synQueue, ^{localCount = count;});return localCount;
}
- 互斥锁能够有效的防止因多线程抢夺资源造成的数据安全问题,但是需要消耗大量的CPU资源。
- 互斥锁: 使用互斥锁可以确保同一时间只有一个线程访问共享资源。例如@
synchronized
创建互斥锁
@synchronized (self) {// 访问共享资源的代码
}
- 自旋锁:自旋锁
(Spin Lock)
:自旋锁一种忙等待的锁,它会不断地尝试获取锁,直到成功为止。在Objective-C中,可以使用os_unfair_lock
来创建自旋锁。 - 信号量(
Semaphore
):信量是一种数器,用于控制同时访问某个资源的线程数量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源的代码
dispatch_semaphore_signal(semaphore);
- 串行队列:串行队列
(Serial Queue)
:使用串行队列可以确保任务按顺序执行,从而避多个线程同时访问共享资源。可以使用GCD(Grand Central Dispatch)
来创建串行队列。
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{// 访问共享资源的代码
});