1、多线程:NSThread/GCD/NSOpeation
NSThread、NSOperation和Grand Central Dispatch (GCD)是三种常用的多线程技术。它们各有特点,适用于不同的场景。
-
NSThread使用更加面向对象,提供了最基本的线程操作( 简单易用 ),适用于需要直接控制线程的场景。缺点是需要手动管理生命周期。 -
NSOperation和NSOperationQueue提供了面向对象的并发操作,适用于需要执行复杂任务、设置任务依赖的场景。可操作依赖关系,优先级,以及最大并发数,自动管理生命周期。 -
GCD提供了强大的并发编程能力,旨在替换 NSTread 等线程技术,是目前推荐的多线程解决方案,适用于几乎所有需要并发执行任务的场景。
可灵活操作线程和队列,附有其它强大功能,自动管理生命周期
1.1 NSThread
NSThread提供了面向对象的线程操作,允许你直接管理线程的创建、启动和停止。用的比较多的是[NSThread currentThread]
参考:www.jianshu.com/p/686dbf4bb…
NSThread给予了开发者较高的控制权,但相应地,也需要开发者自己管理线程的生命周期和资源。
// 方法一:initWithBlock + start
- (void)nsthreadDemo
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
// 打印当前线程
NSLog(@"%@",[NSThread currentThread]);
}];
[thread start];
}
// 方法二、创建并启动线程,不需要start
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
[NSThread detachNewThreadSelector:@selector(doBackgroundWork) toTarget:self withObject:nil];
// 被执行的后台任务方法
- (void)doBackgroundWork {
@autoreleasepool {
// 在这里执行后台任务
NSLog(@"NSThread: 执行后台任务");
}
}
- (void)nsthreadDemo
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
// 打印当前线程
NSLog(@"%@",[NSThread currentThread]);
//让当前线程睡2秒
[NSThread sleepForTimeInterval:2.0];
// 回到主线程
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
}];
[thread start];
}
- (void)updateUI
{
NSLog(@"%@",[NSThread currentThread]);
self.view.backgroundColor = [UIColor systemRedColor];
}
# 线程睡眠
+ (void)sleepUntilDate:(NSDate *)date; // sleepUntilDate方法是指睡眠当某个date
+ (void)sleepForTimeInterval:(NSTimeInterval)ti; // sleepForTimeInterval是指让线程睡眠几秒。
# 获取当前线程
[NSThread currentThread]
1.2 NSOpeation 使用中的一些问题??TODO
NSOperation和NSOperationQueue基于 GCD 更高一层的封装**,使用更加面向对象,**提供了更高层次的抽象,允许你定义操作(任务)并将它们添加到队列中**。**系统会自动管理线程的创建和执行(** 自动管理生命周期 **)** 。比 GCD 多了一些更简单的功能。NSOperation`提供了更多的灵活性,如设置操作之间的依赖、取消操作等,并且可以很容易地实现并发或串行执行**。
1.2.1 操作(Operation)与操作队列(** NSOperationQueue )
执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系), 然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
NSOperationQueue:NSInvocationOperation与NSBlockOperation
操作NSOperation:NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作
NSBlockOperation
NSBlockOperation**通过一个或多个块来定义操作,提供了更大的灵活性,** 特别是当操作可以被分解为多个并发执行的任务时。使用NSOperationQueue可以管理这些操作的执行,包括并发执行、设置操作依赖、取消操作等。
// NSBlockOperation是另一个NSOperation的子类,它允许你使用一个或多个块来定义操作。如果NSBlockOperation对象包含多个块,那么这些块可以并发执行。
// 创建NSBlockOperation对象
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// 在这里执行任务
NSLog(@"执行第一个块的任务");
}];
// 添加更多的块
[blockOperation addExecutionBlock:^{
NSLog(@"执行第二个块的任务");
}];
// 创建操作队列并将操作添加到队列中
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blockOperation];
// NSBlockOperation可以包含多个执行块,如果添加的执行块数量超过1,那么这些块将并发执行,前提是NSOperationQueue的maxConcurrentOperationCount属性允许并发执行。
NSInvocationOperation
NSInvocationOperation通过指定目标对象和方法来定义操作,适用于操作可以被封装为单个方法调用的场景。
/**
* 使用子类 NSInvocationOperation
*/
- (void)useInvocationOperation {
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
}
/**
* 任务1
*/
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}
2、使用自定义继承自 NSOperation 的子类
如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了。
先定义一个继承自 NSOperation 的子类,重写main方法。
// YSCOperation.h 文件
#import <Foundation/Foundation.h>
@interface YSCOperation : NSOperation
@end
// YSCOperation.m 文件
#import "YSCOperation.h"
@implementation YSCOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}
@end
/**
* 使用自定义继承自 NSOperation 的子类
*/
- (void)useCustomOperation {
// 1.创建 YSCOperation 对象
YSCOperation *op = [[YSCOperation alloc] init];
// 2.调用 start 方法开始执行操作
[op start];
}
如何延长生命周期 TODO: sleep+wait
NSOperation 操作依赖:能添加操作之间的依赖关系。
通过**操作依赖,我们可以很方便的控制操作之间的执行先后顺序**。NSOperation 提供了3个接口供我们管理和查看依赖。
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
@property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。
#### **queuePriority优先级**:设定操作执行的优先级。queuePriority属性适用于**同一操作队列中的操作,不适用于不同操作队列中的操作**。
* 默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
状态切换:
对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。TODO:验证 --juejin.cn/post/684490…
如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。
使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。-可以很方便的取消一个操作的执行。
(双向链表)任务在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类。
自定义队列(非主队列)
添加到这种队列中的操作,就会自动放到子线程中执行。
同时包含了:串行、并发功能。
自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSOperation: 执行后台任务");
}];
// 创建队列并添加操作
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
2、串行队列
NSOperationQueue *serialQueue = [[NSOperationQueue alloc] init];
serialQueue.maxConcurrentOperationCount = 1;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation1---%d: %@",4,[NSThread currentThread]);
}];
[serialQueue addOperation:op1];
[serialQueue addOperationWithBlock:^{
NSOperationQueue *concurrentQueue = [[NSOperationQueue alloc] init];
concurrentQueue.maxConcurrentOperationCount = 2;
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation2---%d: %@",4,[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation3---%d: %@",4,[NSThread currentThread]);
}];
[concurrentQueue addOperation:op2];
[concurrentQueue addOperation:op3];
[concurrentQueue waitUntilAllOperationsAreFinished];
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation4---%d: %@",4,[NSThread currentThread]);
}];
[serialQueue addOperation:op4];
3.设置操作之间的依赖没有依赖的任务就可以进入到ready。--任务之间可以相互依赖,没有依赖关系,那就看谁的优先级高,就先执行谁。
- (void)operationdemo1
{
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//创建并发队列
queue.maxConcurrentOperationCount = 2;
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",1,[NSThread currentThread]);
}];
//设置普通优先级
[operation1 setQueuePriority:NSOperationQueuePriorityNormal];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",2,[NSThread currentThread]);
}];
//2依赖1
[operation2 addDependency:operation1];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",3,[NSThread currentThread]);
}];
//设置高的优先级
[operation3 setQueuePriority:NSOperationQueuePriorityHigh];
NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",4,[NSThread currentThread]);
}];
//4依赖3
[operation4 addDependency:operation3];
// 队列添加操作
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
[queue addOperation:operation4];
}
// 优先级并不是决定任务执行优先顺序的,甚至在源码里没有看到在执行任务过程中对优先级的判断??
1.3 实战:NSBlockOperation+addExecutionBlock-可添加完成的代码块,在操作完成后执行。--TODO
在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。 NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。如果添加的操作多的话, blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。(可以使用 addExecutionBlock: 多添加几个操作试试)。
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.调用 start 方法开始执行操作
[op start];
1.4 操作队列NSOperationQueue
操作队列通过设置 最大并发操作数(maxConcurrentOperationCount) 来控制并发、串行。
用来控制一个特定队列中可以有多少个操作同时参与并发执行。(默认情况下为-1,表示不进行限制,可进行并发执行。maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。--TODO:
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 自定义队列(非主队列):添加到这种队列中的操作,就会自动放到子线程中执行。同时包含了:串行、并发功能。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
/**
* 使用 addOperation: 将操作加入到操作队列中
*/
- (void)addOperationToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
// 使用 NSInvocationOperation 创建操作1
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 使用 NSInvocationOperation 创建操作2
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
// 使用 NSBlockOperation 创建操作3
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op3 addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.使用 addOperation: 添加所有操作到队列中
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
[queue addOperation:op3]; // [op3 start]
}
* NSOperation 常用属性和方法
1、取消操作方法
- (void)cancel; 可取消操作,实质是标记 isCancelled 状态。
2、判断操作状态方法
- (BOOL)isFinished; 判断操作是否已经结束。
- (BOOL)isCancelled; 判断操作是否已经标记为取消。
- (BOOL)isExecuting; 判断操作是否正在在运行。
- (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
3、操作同步
- (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。
- (void)setCompletionBlock:(void (^)(void))block; completionBlock 会在当前操作执行完毕时执行 completionBlock。
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
@property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。
NSOperationQueue 常用属性和方法
1、取消/暂停/恢复操作
- (void)cancelAllOperations; 可以取消队列的所有操作。
- (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。
- (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
2、操作同步
- (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。
3、添加/获取操作
- (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象。
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
- (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。
- (NSUInteger)operationCount; 当前队列中的操作数。
4、获取队列
+ (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。
+ (id)mainQueue; 获取主队列。
注意:
这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。
2.3 GCD:Grand Central Dispatch 大中央调度:多线程的一种手段, 处理异步流程 ---任务、队列、函数
参考: juejin.cn/post/684490… demo:github.com/itcharge/YS…
`GCD是基于C语言的底层API,目前iOS开发中推荐的多线程解决方案,它提供了简单的API来执行任务,并自动优化线程的使用。它自动管理线程池,简化了多线程编程的复杂性。
2.3.1 GCD的优势:GCD将任务添加到队列,并指定执行任务的函数。
- GCD 是苹果公司为多核的并⾏运算提出的解决⽅案
- GCD 会⾃动利⽤更多的CPU内核(⽐如双核、四核)
- GCD 会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉 GCD 想要执⾏什么任务,不需要编写任何线程管理代码
2.3.2 队列、函数、任务(任务封装成了无参数无返回值的block)
队列:存放 &&执行任务的等待队列,即用来存放任务的队列
- 队列是什么:先进先出的数据结构FIFO--是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
- 队列与线程的关系:1、队列是负责存储任务与调度任务的,线程是用来执行任务的主体。2、主队列与主线程存在绑定关系,其他队列与线程并不是依赖关系。
- 任务先添加先执行完毕么?线程执行任务。先添加先执行,但不代表先添加先执行完毕。原因如下:五点
1、当前任务的复杂度:任务耗时不同,并行任务呢 TODO
2、优先级:比如放在后台的下载任务
3、时间切换:多线程原理。(CPU在单位时间片里不断来回切换,造成多线程的假象,两个任务同时执行—并发。) 多核才能实现真正的多线程。执行效率高。
4、依赖:任务调度的时间问题导致爹依赖
5、线程的执行状态--CPU/线程池的调度:因为任务task依赖于线程去执行。因此线程状态resume,任务就执行得快;如果线程中止suspend,任务也就不执行了。非优先级,而是取决于线程状态
随机??
- 函数与队列
函数:将任务添加到队列,并指定执行任务的函数;(同步函数与异步函数)
队列:串性队列与并发队列;
- 任务:在线程中执行的那段代码。在 GCD 中是放在 block 中的
任务是什么? GCD中任务封装成了block,block没有参数也没有返回值。任务通过队列的调度,由线程来执行。
执行任务有两种方式:『同步执行』 和 『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
---同步函数和异步函数 TODO:任务是如何封装并调用的呢?这是一个问题!
- 执行任务的函数分为:异步函数和同步函数
- 异步函数dispatch_async
-
不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句。
-
会开启线程执⾏ block 的任务。 ****异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
线程个数肯定没有当前任务多。会从线程池里拿已有的线程执行任务,其次线程也不可能无限创建,CPU吃不消。
面试题:一个异步并发的任务一定会开启新线程么? 不一定。👆
如何控制线程个数---信号量🌟 ->任务很多,但是线程只有2个。(信号量常用方式) 与自旋锁不太一样 TODO:八大锁
当信号量<=0,会休眠;只有>0才会执行,否则会一直等待。
-
NS打印次数>=10, a打印也会>=10
* 异步是多线程的代名词。
-
同步函数dispatch_sync
- 必须等待当前语句执⾏完毕,才会执⾏下⼀条语句
- 不会开启线程
- 在当前线程执⾏ block 的任务
-
队列—串行队列和并发队列区别
队列分为两种:串行队列和并发队列。不同的队列中,任务排列的方式是不一样的,任务通过队列的调度,由线程池安排的线程来执行。
不管是串行队列还是并发队列,都会遵循FIFO的原则,即先进入先调度的原则;任务的执行速度或者说执行时长,与各自任务的复杂度有关。
- 串行队列:通路比较窄,任务按照一定的顺序进行排列,DSF_WIDTH = 1
- 并发队列:通道比较广,同一时间可有多个任务执行. DSF_WIDTH = MAX
队列是什么,如何封装的,如何调度任务的,这也是我们需要研究的内容。
队列与函数的配合使用
队列用来调用任务,函数用来执行任务。那么队列和函数不同的配合会有怎样的运行效果呢?
-
同步函数串行队列
- 不会开启线程,在当前线程中执行任务
- 任务串行执行,任务一个接着一个执行
- 会产生阻塞—死锁(死锁问题)
-
同步函数并发队列--即并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。
- 不会开启线程,在当前线程中执行任务
- 任务一个接着一个执行
-
异步函数串行队列
- 会开启一个线程
- 任务一个接着一个执行
-
异步函数并发队列
- 开启线程,在当前线程执行任务
- 任务异步执行,没有顺序,CPU调度有关
主队列-串行队列
1、抽象—返回什么
主队列与主线程存在绑定关系,其他队列与线程并不是依赖关系。
2、线程和RunLoop的关系,RunLoop与队列关系。TODO
3、dispatch_get_main_queue在以下几个方法前就调用了。NSApplicationMain执行前就已经有主队列了。
串行队列 dispatch_get_main_queue serialNum = 1
全局并发队列,缓存 dispatch_get_global_queue 收集者集合+优先级(负载情况)去决定调用哪个
什么时候创建? 结构体对象已经加载在集合体里面了(全局内存)
__blcok
4、 dylib动态库链接 libdylb.dylib
动态库链接是什么,有哪些——
通过镜像拿到
GCD下层封装等等
GCD使用
步骤其实很简单,只有两步:
- 创建一个队列(串行队列或并发队列);
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)。
下边来看看队列的创建方法 / 获取方法,以及任务的创建方法。
队列的创建方法 / 获取方法
-
可以使用 dispatch_queue_create 方法来创建队列。该方法需要传入两个参数:
- 第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
- 第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
-
对于串行队列,GCD 默认提供了:『主队列(Main Dispatch Queue)』。
- 所有放在主队列中的任务,都会放到主线程中执行。
- 可使用 dispatch_get_main_queue() 方法获得主队列。
注意:主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象。
-
对于并发队列,GCD 默认提供了 『全局并发队列(Global Dispatch Queue)』。
-
可以使用 dispatch_get_global_queue 方法来获取全局并发队列。
需要传入两个参数。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用 0 即可。
-
任务的创建方法
GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
虽然使用 GCD 只需两步,但是既然我们有两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:
- 同步执行 + 并发队列
- 异步执行 + 并发队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
实际上,刚才还说了两种默认队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是当前代码默认放在主队列中,所以主队列很有必要专门来研究一下,所以我们就又多了两种组合方式。这样就有六种不同的组合方式了。
- 同步执行 + 主队列
- 异步执行 + 主队列
那么这几种不同组合方式各有什么区别呢?
任务和队列不同组合方式的区别
我们先来考虑最基本的使用,也就是当前线程为 『主线程』 的环境下,『不同队列』+『不同任务』 简单组合使用的不同区别。暂时不考虑 『队列中嵌套队列』 的这种复杂情况。
『主线程』中,『不同队列』+**『不同任务』**简单组合的区别:
| 区别 | 并发队列 | 串行队列 | 主队列 |
|---|---|---|---|
| 同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁卡住不执行 |
| 异步(async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
注意:从上边可看出: 『主线程』 中调用 『主队列』+『同步执行』 会导致死锁问题。 这是因为 主队列中追加的同步任务 和 主线程本身的任务 两者之间相互等待,阻塞了 『主队列』,最终造成了主队列所在的线程(主线程)死锁问题。 而如果我们在 『其他线程』 调用 『主队列』+『同步执行』,则不会阻塞 『主队列』,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务。
队列嵌套情况下,不同组合方式区别
除了上边提到的『主线程』中调用『主队列』+『同步执行』会导致死锁问题。实际在使用『串行队列』的时候,也可能出现阻塞『串行队列』所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。
比如下面代码这样:在『异步执行』+『串行队列』的任务中,又嵌套了『当前的串行队列』,然后进行『同步执行』。
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ // 异步执行 + 串行队列 dispatch_sync(queue, ^{ // 同步执行 + 当前串行队列 // 追加任务 1 [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 }); });
执行上面的代码会导致 串行队列中追加的任务 和 串行队列中原有的任务 两者之间相互等待,阻塞了『串行队列』,最终造成了串行队列所在的线程(子线程)死锁问题。
主队列造成死锁也是基于这个原因,所以,这也进一步说明了主队列其实并不特殊。
关于 『队列中嵌套队列』这种复杂情况,这里也简单做一个总结。不过这里只考虑同一个队列的嵌套情况,关于多个队列的相互嵌套情况还请自行研究,或者等我最新的文章发布。
『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:
| 区别 | 『异步执行+并发队列』嵌套『同一个并发队列』 | 『同步执行+并发队列』嵌套『同一个并发队列』 | 『异步执行+串行队列』嵌套『同一个串行队列』 | 『同步执行+串行队列』嵌套『同一个串行队列』 |
|---|---|---|---|---|
| 同步(sync) | 没有开启新的线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁卡住不执行 | 死锁卡住不执行 |
| 异步(async) | 有开启新线程,并发执行任务 | 有开启新线程,并发执行任务 | 有开启新线程(1 条),串行执行任务 | 有开启新线程(1 条),串行执行任务 |
好了,关于『不同队列』+『不同任务』 组合不同区别总结就到这里。
关于不同队列和不同任务的形象理解
因为前一段时间看到了有朋友留言说对 异步执行 和 并发队列 中创建线程能力有所不理解,我觉得这个问题的确很容易造成困惑,所以很值得拿来专门分析一下。
他的问题:
在 异步 + 并发 中的解释: (异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务)
以及 同步 + 并发 中的解释: (虽然并发队列可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力)
这个地方看起来有点疑惑,你两个地方分别提到:异步执行开启新线程,并发队列也可以开启新线程,想请教下,你的意思是只有任务才拥有创建新线程的能力,而队列只有开启线程的能力,并不能创建线程 ?这二者是这样的关联吗?
关于这个问题,我想做一个很形象的类比,来帮助大家对 队列、任务 以及 线程 之间关系的理解。
假设现在有 5 个人要穿过一道门禁,这道门禁总共有 10 个入口,管理员可以决定同一时间打开几个入口,可以决定同一时间让一个人单独通过还是多个人一起通过。不过默认情况下,管理员只开启一个入口,且一个通道一次只能通过一个人。
这个故事里,人好比是 任务,管理员好比是 系统,入口则代表 线程。
- 5 个人表示有 5 个任务,10 个入口代表 10 条线程。
- 串行队列 好比是 5 个人排成一支长队。
- 并发队列 好比是 5 个人排成多支队伍,比如 2 队,或者 3 队。
- 同步任务 好比是管理员只开启了一个入口(当前线程)。
- 异步任务 好比是管理员同时开启了多个入口(当前线程 + 新开的线程)。
『异步执行 + 并发队列』 可以理解为:现在管理员开启了多个入口(比如 3 个入口),5 个人排成了多支队伍(比如 3 支队伍),这样这 5 个人就可以 3 个人同时一起穿过门禁了。
『同步执行 + 并发队列』 可以理解为:现在管理员只开启了 1 个入口,5 个人排成了多支队伍。虽然这 5 个人排成了多支队伍,但是只开了 1 个入口啊,这 5 个人虽然都想快点过去,但是 1 个入口一次只能过 1 个人,所以大家就只好一个接一个走过去了,表现的结果就是:顺次通过入口。
换成 GCD 里的语言就是说:
- 『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
- 『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。
下边我们来研究一下上边提到的六种简单组合方式的使用方法。
主队列在Main之前创建
dispatch用法
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 异步执行任务
dispatch_async(queue, ^{
NSLog(@"GCD: 执行后台任务");
// 回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"GCD: 回到主线程更新UI");
});
});
- (void)cancelAllOperations; // 取消当前队列的所有操作任务。
suspended:队列挂起。
@property (getter=isSuspended) BOOL suspended;
mainQueue:主队列。
[NSOperationQueue mainQueue]
currentQueue:当前队列。
[NSOperationQueue currentQueue]
- (void)waitUntilAllOperationsAreFinished; // 等当前队列执行完所有任务,才会继续走,会阻塞当前线程。
// 使用
1.dispatch_once
@implementation GCDDemo
+ (instancetype)shareInstance {
static GCDDemo *demo;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
demo = [GCDDemo new];
});
return demo;
}
@end
2.dispatch_after
- (void)gcddemo2 {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2:%@",[NSThread currentThread]);
});
NSLog(@"1:%@",[NSThread currentThread]);
}
3.dispatch_barrier_async 异步栅栏函数,需要等前面的队列任务执行完,再执行自己的,然后再执行后面的。
- (void)dispatch_barrier_async_request
{
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"2:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3:%@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"4:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"5:%@", [NSThread currentThread]);
});
NSLog(@"1:%@", [NSThread currentThread]);
}
4.dispatch_group 一般来说,队列组适用于在请求几个异步任务,然后等任务执行完后,再到dispatch_group_notify执行所在的任务。
- (void)dispatch_group_request
{
// 创建1个队列组
dispatch_group_t group = dispatch_group_create();
// 创建1个并发队列
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
// 异步添加1个并发队列
dispatch_group_async(group, queue, ^{
// 延迟模拟
sleep(1);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
// 延迟模拟
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
});
// 上面的任务执行完后会来到这里。并没有阻塞当前线程,而且是等前面2个任务执行完毕后,再执行dispatch_group_notify的任务。
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"0--%@",[NSThread currentThread]);
}
5.dispatch_barrier_async
dispatch_once创建1个单例,在整个程序运行过程中只执行一次。这个在多线程的时候也可以保证线程安全的。
dispatch_after是不会阻塞线程的,并且延时后,会回到主线程执行任务。
dispatch_barrier_async是一个栅栏函数,并不会阻塞当前线程。---实现多读单写。🌲
Q:与group配合使用,会阻塞住么? --dispatch_barrier_async和group同时使用会有什么问题么
A:不会,系统有做其他处理。---TODO:
点击展开内容
dispatch_barrier_async和dispatch_group是GCD(Grand Central Dispatch)提供的两种不同的并发编程工具,它们各自有不同的用途。
将它们同时使用时,需要注意它们的交互方式,以避免出现逻辑错误或不符合预期的行为。
dispatch_barrier_async
dispatch_barrier_async函数允许在并发队列中创建一个屏障。这个屏障确保在屏障任务之前提交到队列的任务全部完成后,屏障任务才会执行;屏障任务完成之后,队列中屏障任务之后的任务才会继续执行。dispatch_barrier_async只在自定义的并发队列上有效,在全局并发队列或串行队列上,它的行为与dispatch_async相同。
dispatch_group
dispatch_group允许多个任务被组合成一个任务组,可以监视这一组任务何时完成。通过dispatch_group_enter、dispatch_group_leave来标记任务的开始和结束,使用dispatch_group_notify来设置当组中的所有任务完成时的回调,或者使用dispatch_group_wait来阻塞当前线程直到所有任务完成。
同时使用时的注意事项
当dispatch_barrier_async和dispatch_group同时使用时,主要的问题是确保dispatch_group_enter和dispatch_group_leave的调用是成对出现的,以避免出现组永远不会完成的情况。如果你在屏障任务中使用了dispatch_group,需要确保每个dispatch_group_enter调用都有对应的dispatch_group_leave调用,无论是在屏障任务之前、之中还是之后。
示例
假设你想要在并发队列中执行多个任务,并在这些任务完成后执行一个屏障任务,然后再执行一些其他任务,同时你想要监视所有任务(包括屏障任务之前和之后的任务)何时完成。
// 创建自定义并发队列
dispatch_queue_t queue = dispatch_queue_create("com.example.myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 创建任务组
dispatch_group_t group = dispatch_group_create();
// 添加任务到组和队列
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 执行任务...
dispatch_group_leave(group);
});
// 添加更多任务到组和队列...
// 添加屏障任务
dispatch_barrier_async(queue, ^{
// 执行屏障任务...
});
// 添加屏障任务之后的任务到组和队列
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 执行任务...
dispatch_group_leave(group);
});
// 设置当所有任务完成时的回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 所有任务完成
});
在这个示例中,屏障任务不属于dispatch_group,因为它没有使用dispatch_group_enter和dispatch_group_leave。如果你希望屏障任务也被dispatch_group监视,你需要在屏障任务中适当地调用这些函数。
总之,dispatch_barrier_async和dispatch_group可以同时使用,但需要小心确保任务组的正确管理,避免出现逻辑错误。
dispatch_group_wait:暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。 通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的方法 dispatch_apply。如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。但是这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5 这 6 个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。还有一点,无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。
/**
* 快速迭代方法 dispatch_apply
*/
- (void)apply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
dispatch_group:队列组适用于在请求几个异步任务,然后等任务执行完后,再到dispatch_group_notify执行所在的任务。
TODO:dispatch_group_async 里面要是用的是第三方网络框架调取异步网络请求,异步网络请求是在其框架的并发队列中,这时候数据还没请求返回,dispatch_group_async就走完了,这时候就执行了dispatch_group_notify,达不到想要的效果。
dispatch_group_enter 、dispatch_group_leave是等同于dispatch_group_async.须是成对出现的,一旦有进组了,dispatch_group_notify就不会调用,直到dispatch_group_leave调用后,才会调取。 和信号量其实是差不多的效果的。TODO:信号量控制
- (void)dispatch_group_request1
{
// 创建1个队列组
dispatch_group_t group = dispatch_group_create();
// 创建1个并发队列
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
// 进组
dispatch_group_enter(group);
//异步网络请求
dispatch_async(queue, ^{
//模拟网络请求
sleep(1);
NSLog(@"1--%@",[NSThread currentThread]);
//出组
dispatch_group_leave(group);
});
// 进组
dispatch_group_enter(group);
//异步网络请求
dispatch_async(queue, ^{
//模拟网络请求
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
//出组
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"0--%@",[NSThread currentThread]);
}
dispatch_semaphore:来初始化信号量,通过信号量的值可以控制线程哪个执行,哪个需要等待。并且设置GCD的最大并发数。值为1的时候,还能达到同步锁的效果
信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
作用: 保持线程同步,将异步执行任务转换为同步执行任务;保证线程安全,为线程加锁
dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量。
dispatch_semaphore_signal:发送一个信号,让信号总量加 1。
dispatch_semaphore_wait:如果信号量大于0,则正常执行,而且信号量会减 1 ;如果信号量为 0 ,则会一直等待,等接收到通知信号量大于 0 后才可以正常执行。
等待的时候会起到阻塞当前线程的效果。
- (void)dispatch_semaphore_t_request
{
//创建1个信号量,且信号量的值为0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"1--%@",[NSThread currentThread]);
//信号值+1
dispatch_semaphore_signal(sema);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
//信号值+1
dispatch_semaphore_signal(sema);
});
dispatch_async(dispatch_get_main_queue(), ^{
// 等待2个
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"0--%@",[NSThread currentThread]);
});
}
3个都是异步函数调用,所以3个都是并发进行的,任务1进去后,睡眠1s,任务2进去后,也睡眠1s,任务0进去后,按理其它2个都睡眠,它就直接走dispatch_semaphore_wait了。
但这时候,我们创建的信号量的值为0,只能等待了。
等任务2睡醒了,执行了dispatch_semaphore_signal,信号值为1了,所以第一个dispatch_semaphore_wait就可以走了,且信号量的值减1。
这时候又遇到第二个dispatch_semaphore_wait,也只能等。直到任务1的执行dispatch_semaphore_signal后,信号量加1了,可以继续执行任务0。
我们不管任务1和任务2谁先执行完,我们最后的任务都需要等待他们执行完才可以执行,因为dispatch_semaphore_wait和dispatch_semaphore_signal是一一对应的。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
Dispatch_source:用的做多就是计时器了。dispatch_source_t的定时器不受RunLoop影响,而且dispatch_source_t是系统级别的源事件,精度很高,系统自动触发。
- (void)dispatch_source_request
{
if (_timer) {
//计时器在运行,不动
return;
}
// 创建定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
// 设置定时器时间
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 设置事件触发的回调
dispatch_source_set_event_handler(_timer, ^{
if (self.timeout <= 0) {
dispatch_source_cancel(self.timer);
self.timer = nil;
}else {
dispatch_async(dispatch_get_main_queue(), ^{
self.timeout--;
NSLog(@"计时--%ld", self.timeout);
});
}
});
// 开始执行
dispatch_resume(_timer);
// 暂停
// dispatch_suspend(timer);
}
2、信号量--同步机制
信号量(Semaphore)是一种同步机制,用于控制对特定资源或资源集的访问,以解决多线程或多进程环境下的竞争条件(Race Condition)
信号量(semaphore)是一种用于同步访问资源或者控制线程并发数的机制。信号量可以用来解决多线程编程中的资源竞争问题,确保线程安全。
信号量主要用于两个目的:互斥锁(Mutex)和条件同步。与NSThread、NSOperation和GCD这些多线程技术的区别主要在于它们的使用目的和应用场景。
信号量是一个整数计数器,用于控制同时访问共享资源的线程数量。它主要提供了两个操作:
- 等待(Wait) :如果信号量的值大于0,线程可以继续执行,并将信号量的值减1;如果信号量的值为0,线程将被阻塞,直到信号量的值变为大于0。
- 信号(Signal) :将信号量的值加1。如果有线程因等待该信号量而被阻塞,这些线程中的一个将被唤醒并继续执行。
与NSThread、NSOperation和GCD的区别
- NSThread、NSOperation和GCD:这些都是iOS提供的多线程技术,用于在应用程序中创建和管理线程,以实现并发执行任务。它们提供了不同层次的抽象和API来简化多线程编程的复杂性,主要关注于如何在后台执行任务,而不是如何同步对共享资源的访问。
- 信号量(Semaphore) :信号量是一种低级的同步机制,用于控制对共享资源的并发访问。它不是用来创建线程或执行任务的,而是用来保证在任何时刻只有限定数量的线程可以访问某个共享资源。信号量可以与NSThread、NSOperation和GCD等技术结合使用,以解决多线程环境下的数据竞争和同步问题。
在iOS开发中,Grand Central Dispatch (GCD) 提供了信号量的实现,可以用来解决多线程访问共享资源时的竞争条件问题,或者用于线程之间的同步。
1、创建信号量:使用`dispatch_semaphore_create`函数创建一个信号量。这个函数接受一个`long`类型的参数,表示信号量的初始值。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
2、等待信号量(Wait)
使用`dispatch_semaphore_wait`函数等待信号量。如果信号量的值大于0,这个函数会将信号量的值减1并立即返回。如果信号量的值为0,当前线程会被阻塞,直到信号量的值变为大于0。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 第二个参数是等待时间,`DISPATCH_TIME_FOREVER`表示永久等待直到信号量的值大于0。
3、发送信号量(Signal)
使用`dispatch_semaphore_signal`函数发送信号量。这个函数会将信号量的值加1。如果有其他线程因等待这个信号量而被阻塞,那么其中一个线程会被唤醒。
dispatch_semaphore_signal(semaphore);
4、使用示例:使用信号量来同步两个异步任务的示例。假设我们有两个异步任务,我们希望第二个任务在第一个任务完成后才开始执行。
// 创建信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 异步执行第一个任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始执行第一个任务");
// 模拟耗时操作
sleep(2);
NSLog(@"第一个任务完成");
// 发送信号量
dispatch_semaphore_signal(semaphore);
});
// 异步执行第二个任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 等待信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"开始执行第二个任务");
// 模拟耗时操作
sleep(2);
NSLog(@"第二个任务完成");
});
在这个示例中,第二个任务中的`dispatch_semaphore_wait`会阻塞该任务的执行,直到第一个任务中的`dispatch_semaphore_signal`被调用,信号量的值变为大于0,这时第二个任务才会继续执行。
### 注意事项
- 使用信号量时要注意避免死锁,比如在同一个线程中等待一个永远不会被发送信号的信号量。
- 信号量的使用需要谨慎,确保每次等待(wait)都对应一次发送(signal),以避免资源泄露或不正确的同步行为。
`dispatch_semaphore_t`在Objective-C中创建和使用信号量的示例:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建信号量,初始值为0
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 异步执行任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"异步任务1完成");
// 发送信号,信号量+1
dispatch_semaphore_signal(semaphore);
});
// 等待信号,直到信号量大于0
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"继续执行主线程任务");
}
return 0;
}
在这个示例中,我们首先创建了一个初始值为0的信号量。然后,我们在一个异步线程中执行一个任务,并在任务完成后通过`dispatch_semaphore_signal`函数将信号量加1。主线程通过调用`dispatch_semaphore_wait`函数等待信号量大于0,这意味着它会阻塞主线程的执行,直到异步任务完成信号量被释放。这样就可以确保主线程在异步任务完成后继续执行。
3、RAC里的信号RACSignal-信号是最基本的组件
3.1 RACSignal
ReactiveCocoa(RAC)是一个用于iOS和OS X开发的响应式编程框架,它提供了一套丰富的API来处理异步操作和数据流,用于简化iOS和macOS应用中的事件处理和数据绑定。虽然RAC的核心是响应式编程,它也提供了一些工具来处理并发或串行执行任务。在RAC中,信号(RACSignal)是最基本的组件,用于表示随时间变化的数据流。订阅信号是响应式编程的核心概念之一,它允许你对信号发出的值做出反应。
### 创建信号:在订阅信号之前,首先需要创建一个信号。这里是一个简单的示例,创建一个发送字符串并正常完成的信号:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"Hello, RAC!"];
[subscriber sendCompleted];
return nil;
}];
### 订阅信号:订阅信号意味着告诉信号,当有数据发出时,你想要做出什么反应。
订阅信号通常使用`-subscribeNext:`、`-subscribeError:`和`-subscribeCompleted:`方法。
[signal subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
} completed:^{
NSLog(@"Signal completed");
}];
在上面的代码中,`subscribeNext:`块会在信号发送数据时被调用,`subscribeError:`块会在信号发送错误时被调用,而`subscribeCompleted:`块会在信号完成时被调用。
### 高级订阅方法
RAC还提供了一些高级的方法来订阅信号,这些方法可以帮助你更方便地处理信号:
- **`-subscribeNext:`**:只关心信号发送的数据。
- **`-subscribeError:`**:只关心信号发送的错误。
- **`-subscribeCompleted:`**:只关心信号的完成事件。
- **`-subscribeNext:error:completed:`**:同时关心信号发送的数据、错误和完成事件。
### 链式调用:RAC支持链式调用,使得你可以以更流畅的方式组合多个操作。例如,你可以使用`-map:`方法来转换信号发送的数据,然后订阅转换后的信号:
[[signal map:^id(NSString *value) {
return [value uppercaseString];
}] subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
// 在这个示例中,原始信号发送的字符串会被转换为大写,然后订阅者会接收到转换后的字符串。
3.2
RAC的信号订阅是响应式编程的核心,它允许你以声明式的方式处理异步事件和数据流。通过创建信号、订阅信号以及使用链式调用,你可以构建出清晰、灵活且易于维护的代码。 以下是一些基本的方法来实现并发和串行执行:
3.2.1 并发执行
在RAC中,并发执行通常可以通过**RACSignal的-flatten:或-flattenMap:** 方法结合RACScheduler来实现。你可以使用+schedulerWithPriority:或+scheduler来创建一个并发队列。
// 创建几个信号
RACSignal *signal1 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 模拟异步任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subscriber sendNext:@"Result of task 1"];
[subscriber sendCompleted];
});
return nil;
}];
RACSignal *signal2 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 模拟异步任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subscriber sendNext:@"Result of task 2"];
[subscriber sendCompleted];
});
return nil;
}];
// 使用flattenMap:并发执行信号
[[RACSignal merge:@[signal1, signal2]] subscribeNext:^(id x) {
NSLog(@"%@", x); // 打印任务结果
}];
3.2.2 串行执行
串行执行可以通过-concat方法实现,它按照信号添加的顺序依次执行每个信号。
// 创建信号并串行执行
[[signal1 concat:signal2] subscribeNext:^(id x) {
NSLog(@"%@", x); // 按顺序打印任务结果
}];
-
RAC的并发并不是传统意义上的多线程并发,而是基于事件流的并发。在RAC中,事件可以在不同的调度器(
RACScheduler)上发送,这些调度器可以是并发的或串行的。 -
RAC的核心在于处理异步操作和事件流,而不是直接提供一个并发编程模型。因此,当你需要在RAC中执行并发或串行任务时,应该考虑如何将这些任务转化为信号(
RACSignal),然后使用RAC提供的操作符来组合和管理这些信号。
RAC提供了一种不同于传统并发编程的方式来处理并发和串行任务,通过信号和调度器的组合,可以灵活地控制任务的执行方式。
4、八大锁--自旋锁 &“互斥锁
4.1 “自旋锁” & “互斥锁”
自旋锁Spinlock-os_unfair_lock
自旋锁(Spinlock)是一种用于多线程同步的锁,它在等待锁的线程中不断循环检查锁是否可用。
与其他类型的锁相比,自旋锁在等待时不会使线程进入睡眠状态,因此它避免了线程从睡眠状态唤醒所需的上下文切换开销。
自旋锁适用于锁持有时间短且线程不希望在重新调度上花费太多成本的场景。 1、特点
-
忙等待:自旋锁通过忙等待(busy-waiting)来检查锁的状态,这意味着线程会在一个循环中不断检查锁是否可用。
-
性能开销:对于持锁时间非常短的情况,自旋锁的性能可能优于其他锁,因为它避免了线程睡眠和唤醒的开销。然而,如果持锁时间较长,自旋锁会导致大量的CPU时间浪费在等待上,从而降低程序的整体性能。
-
不适用于单核处理器:在单核处理器上使用自旋锁可能会导致死锁,因为正在等待锁的线程会占用CPU资源,从而阻止持有锁的线程执行并释放锁。
2、使用场景
锁的持有时间非常短。
高并发环境中,线程切换的成本高于忙等待的成本。
多核处理器环境。
### 示例
在Objective-C或Swift中,可以使用`os_unfair_lock`作为自旋锁的替代(在iOS 10及以上版本中,`OSSpinLock`已被弃用,因为它不安全)。
`os_unfair_lock`提供了一种低级的锁机制,可以用于保护临界区。
Objective-C示例:
#import <os/lock.h>
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&lock);
// 临界区代码
os_unfair_lock_unlock(&lock);
Swift示例:
import os.lock
var lock = os_unfair_lock_s()
os_unfair_lock_lock(&lock)
// 临界区代码
os_unfair_lock_unlock(&lock)
### 注意事项
- 使用自旋锁时,需要确保锁的持有时间尽可能短,以避免CPU资源的浪费。
- 考虑到自旋锁可能导致的性能问题,应当谨慎选择使用场景,并考虑其他同步机制(如信号量、互斥锁等)作为替代方案。
4.2 互斥锁
如果其他线程正在执行锁定的代码,此线程就会进入休眠状态,等待锁打开;然后被唤醒
异同
-
共同点:都能够保证线程安全
-
不同点:
互斥锁:如果其他线程正在执行锁定的代码,此线程就会进入休眠状态,等待锁打开;然后被唤醒
自旋锁:如果线程被锁在外面,哥么就会用死循环的方式一直等待锁打开!
无论什么锁,都很消耗性能,效率不高,所以在我们平时开发过程中,会使用nonatomic
@property (strong, nonatomic) NSObject *myNonatomic;
@property (strong, atomic) NSObject *myAtomic;
// 根据上面描述,我们得出结论,当我们重写了myAtomic的setter和getter方法
- (void)setMyAtomic:(NSObject *)myAtomic{
_myAtomic = myAtomic;
}
- (NSObject *)myAtomic{ return _myAtomic; }
那么我们就必须声明一个_myAtomic静态变量
@synthesize myAtomic = _myAtomic;
否则系统在编译的时候找不到 _myAtomic
原子属性(线程安全)与非原子属性,平时我们@property声明对象属性时会用到nonatomic,是什么意思呢?
苹果系统在我们声明对象属性时默认是atomic,也就是说在读写这个属性的时候,保证同一时间内只有一个线程能够执行。
当声明时用的是atomic,通常会生成 \_成员变量 如果同时重写了getter\&setter \_成员变量 就不自动生成。实际上原子属性内部有一个锁,叫做“自旋锁”。
4.3 读写锁pthread_rwlock_rdlock-同步机制---TODO:
读写锁是一种同步机制,允许多个线程同时读取共享数据,但在写入数据时需要独占访问权。这种锁机制非常适合读操作远多于写操作的场景,可以提高程序的并发性能。
通过使用pthread_rwlock_rdlock和相关的pthread读写锁函数,可以在多线程程序中有效地同步对共享资源的访问,特别是在读多写少的场景中提高并发性能。
pthread_rwlock_rdlock是POSIX线程(pthread)库中的一个函数,用于在多线程程序中获取读写锁(read-write lock)的读锁。
1、函数原型
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- **参数**:`rwlock`是指向读写锁对象的指针。
- **返回值**:成功时返回`0`;失败时返回错误码。
2、使用方法
**初始化读写锁**:在使用读写锁之前,需要先对其进行初始化。可以静态初始化或动态初始化。
静态初始化示例:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
动态初始化示例:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
2. **获取读锁**:调用`pthread_rwlock_rdlock`函数尝试获取读锁。如果锁当前未被写锁占用,则调用线程将获得读锁,并且其他线程仍然可以获取读锁,但任何尝试获取写锁的线程都将被阻塞,直到所有读锁被释放。
pthread_rwlock_rdlock(&rwlock);
// 执行读取操作...
3. **释放锁**:完成读取操作后,应该调用`pthread_rwlock_unlock`函数释放读锁,以允许其他线程获取读锁或写锁。
pthread_rwlock_unlock(&rwlock);
4. **销毁读写锁**:当读写锁不再需要时,应该销毁它以释放资源。
pthread_rwlock_destroy(&rwlock);
### 注意事项
- 在持有读写锁的任何锁(读锁或写锁)时,尝试再次获取相同类型的锁可能会导致死锁。
- 读写锁适用于读操作远多于写操作的场景。如果读写操作频率相近,使用读写锁可能不会带来性能优势。
- 使用读写锁需要注意正确匹配`pthread_rwlock_rdlock`和`pthread_rwlock_unlock`调用,避免锁泄露或未正确释放锁。
4.4 并发度很高的情况下,如何设计生产者消费者的锁
在并发度很高的情况下,设计生产者-消费者模式的锁机制需要特别小心,以确保高效且正确的并发操作。以下是一些关键的设计原则和策略:
4.4.1 使用条件变量(Condition Variables) :
条件变量是实现生产者-消费者模式的一种有效方式。它们允许线程在特定条件满足时等待,并在条件改变时被唤醒。
使用条件变量可以避免忙等待(busy-waiting),从而节省CPU资源。
条件变量(Condition Variable)是多线程编程中的一种同步机制,用于线程之间的通信和协调。
条件变量允许线程在某个条件满足时等待,并在条件发生变化时被唤醒。
条件变量通常与互斥锁(Mutex)一起使用,以确保线程在等待和唤醒过程中的正确性和一致性。
### 主要操作
条件变量通常提供以下几个主要操作:
1. **初始化(Initialization)**:创建并初始化一个条件变量。
2. **等待(Wait)**:线程在条件变量上等待,直到被其他线程唤醒。在等待期间,线程会释放它持有的互斥锁,并在被唤醒后重新获取该锁。
3. **通知(Notify)**:唤醒一个或多个在条件变量上等待的线程。通知操作有两种形式:
- **通知一个(Notify One)**:唤醒一个等待的线程。
- **通知所有(Notify All)**:唤醒所有等待的线程。
4. **销毁(Destruction)**:销毁条件变量,释放相关资源。
### 使用示例
以下是一个使用条件变量的简单示例,展示了生产者-消费者问题:
```c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 10
typedef struct {
int buffer[BUFFER_SIZE];
int count;
int in;
int out;
pthread_mutex_t mutex;
pthread_cond_t full;
pthread_cond_t empty;
} Buffer;
Buffer shared_buffer;
void init_buffer(Buffer *buf) {
buf->count = 0;
buf->in = 0;
buf->out = 0;
pthread_mutex_init(&buf->mutex, NULL);
pthread_cond_init(&buf->full, NULL);
pthread_cond_init(&buf->empty, NULL);
}
void destroy_buffer(Buffer *buf) {
pthread_mutex_destroy(&buf->mutex);
pthread_cond_destroy(&buf->full);
pthread_cond_destroy(&buf->empty);
}
void *producer(void *arg) {
for (int i = 0; i < 100; ++i) {
pthread_mutex_lock(&shared_buffer.mutex);
while (shared_buffer.count == BUFFER_SIZE) {
pthread_cond_wait(&shared_buffer.full, &shared_buffer.mutex);
}
shared_buffer.buffer[shared_buffer.in] = i;
shared_buffer.in = (shared_buffer.in + 1) % BUFFER_SIZE;
shared_buffer.count++;
pthread_cond_signal(&shared_buffer.empty);
pthread_mutex_unlock(&shared_buffer.mutex);
}
return NULL;
}
void *consumer(void *arg) {
for (int i = 0; i < 100; ++i) {
pthread_mutex_lock(&shared_buffer.mutex);
while (shared_buffer.count == 0) {
pthread_cond_wait(&shared_buffer.empty, &shared_buffer.mutex);
}
int item = shared_buffer.buffer[shared_buffer.out];
shared_buffer.out = (shared_buffer.out + 1) % BUFFER_SIZE;
shared_buffer.count--;
pthread_cond_signal(&shared_buffer.full);
pthread_mutex_unlock(&shared_buffer.mutex);
printf("Consumed: %d\n", item);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
init_buffer(&shared_buffer);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
destroy_buffer(&shared_buffer);
return 0;
}
关键点
-
互斥锁保护:在访问共享资源(如缓冲区)时,必须使用互斥锁来确保线程安全。
-
条件检查:在等待条件变量之前,必须使用循环检查条件,以防止虚假唤醒(Spurious Wakeups)。
-
通知机制:在修改共享资源后,必须通知等待的线程,以便它们能够重新检查条件并继续执行。
条件变量是多线程编程中非常强大和灵活的工具,能够有效地解决线程间的同步和通信问题。
4.4.2 使用互斥锁(Mutex) :
* 互斥锁用于保护共享资源,确保在任何时刻只有一个线程可以访问该资源。
* 在生产者-消费者模式中,互斥锁通常用于**保护队列(或其他共享缓冲区**)的访问。
4.4.3 无锁数据结构:
* 在高并发环境下,使用无锁数据结构(如CAS操作实现的队列)可以显著提高性能。
* 无锁数据结构避免了传统锁的开销,但实现起来更为复杂,需要仔细处理内存顺序和可见性问题。
4.4.4 分段锁(Segmented Locks) :
* 如果**共享缓冲区很大,可以考虑使用分段锁**,将缓冲区分成多个段,每个段有自己的锁。
* 这样可以减少锁的争用,提高并发度。
4.4.4 读写锁(Read-Write Locks) :
* 如果生产者和消费者对共享资源的访问模式不同(例如,生产者写入,消费者读取),可以考虑使用读写锁。
* **读写锁允许多个读者同时访问资源,但写者独占资源**,这样可以提高读取操作的并发性。
4.4.6 使用信号量(Semaphores) :
* 信号量可以用于控制对共享资源的访问,特别是在生产者-消费者模式中控制队列的大小。
* 生产者可以在队列满时等待信号量,消费者可以在队列空时等待信号量。
4.4.7 批量操作:
* 批量操作可以减少锁的获取和释放次数,从而提高性能。
* 例如,生产者可以一次生产多个项目,消费者可以一次消费多个项目。
4.4.8 避免死锁:
* 确保锁的获取顺序一致,避免循环等待条件。
* 使用**超时机制或死锁检测工具**来防止和解决死锁问题。
在Objective-C中实现生产者-消费者模型,可以使用NSCondition类,它是一个互斥锁和条件变量的组合,非常适合用于生产者-消费者场景。
下面是一个简单的示例:
#import <Foundation/Foundation.h>
@interface ProducerConsumer : NSObject {
NSCondition *condition;
NSMutableArray *buffer;
NSInteger capacity;
}
- (instancetype)initWithCapacity:(NSInteger)capacity;
- (void)produce;
- (void)consume;
@end
@implementation ProducerConsumer
- (instancetype)initWithCapacity:(NSInteger)cap {
self = [super init];
if (self) {
condition = [[NSCondition alloc] init];
buffer = [[NSMutableArray alloc] init];
capacity = cap;
}
return self;
}
- (void)produce {
[condition lock];
while ([buffer count] == capacity) {
NSLog(@"Buffer is full, producer is waiting...");
[condition wait];
}
[buffer addObject:@"Product"];
NSLog(@"Produced a product. Total products: %lu", (unsigned long)[buffer count]);
[condition signal];
[condition unlock];
}
- (void)consume {
[condition lock];
while ([buffer count] == 0) {
NSLog(@"Buffer is empty, consumer is waiting...");
[condition wait];
}
[buffer removeObjectAtIndex:0];
NSLog(@"Consumed a product. Total products: %lu", (unsigned long)[buffer count]);
[condition signal];
[condition unlock];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ProducerConsumer *pc = [[ProducerConsumer alloc] initWithCapacity:10];
// 创建生产者线程
NSThread *producerThread = [[NSThread alloc] initWithBlock:^{
for (int i = 0; i < 20; i++) {
[pc produce];
[NSThread sleepForTimeInterval:1];
}
}];
// 创建消费者线程
NSThread *consumerThread = [[NSThread alloc] initWithBlock:^{
for (int i = 0; i < 20; i++) {
[pc consume];
[NSThread sleepForTimeInterval:1];
}
}];
[producerThread start];
[consumerThread start];
[producerThread join];
[consumerThread join];
}
return 0;
}
ProducerConsumer类使用NSCondition来同步生产者和消费者线程。生产者线程在缓冲区满时等待,消费者线程在缓冲区空时等待。 当生产者添加一个产品到缓冲区或消费者从缓冲区移除一个产品时,它们会分别唤醒等待的消费者或生产者线程。
5、线程不安全
5.1 线程不安全情况
线程不安全通常指在多线程环境下,由于并发访问共享资源而没有进行适当的同步控制,导致数据不一致、程序行为异常或崩溃等问题。
线程不安全的情况主要包括但不限于以下几种:
1. 竞态条件(Race Condition)
当两个或多个线程并发访问相同的资源,并且至少有一个线程对资源进行写操作时,如果没有适当的同步,就可能出现竞态条件。这可能导致数据损坏或不一致。
2. 死锁(Deadlock)
当两个或多个线程在等待对方持有的资源释放,而又不释放自己持有的资源时,会发生死锁。这导致了一个循环等待的情况,使得涉及的线程都无法继续执行。
3. 活锁(Livelock)
活锁是指两个或多个线程不断重试一个操作,但总是失败,因为它们之间的相互干扰,导致没有一个线程能够进展。尽管线程是活动的,但程序却没有做任何有意义的工作。
4. 饥饿(Starvation)
饥饿发生在一个或多个线程无法获得它们需要的资源,以继续执行。这通常是由于资源被其他线程长时间占用或优先级调度不当导致的。
5. 优先级反转(Priority Inversion)
优先级反转是指低优先级线程持有高优先级线程需要的资源,而高优先级线程因此被迫等待。如果一个中优先级线程抢占了低优先级线程,这可能导致高优先级线程比低优先级线程更长时间地等待资源。
6. 不正确的资源释放
线程在使用完资源后,未能正确释放资源(如文件句柄、内存等),可能导致资源泄露,最终可能耗尽资源,影响程序的正常运行。
7. 不一致的视图
在没有适当同步的情况下,一个线程可能看到另一个线程部分完成操作的中间状态,导致数据不一致。
解决线程不安全的方法
-
互斥锁(Mutex):确保同一时间只有一个线程可以访问共享资源。
-
读写锁(Read-Write Lock):允许多个读操作并发,但写操作是互斥的。
-
信号量(Semaphore):限制对资源访问的线程数。
-
条件变量(Condition Variable):允许线程在某些条件下挂起,直到条件满足。
-
原子操作:确保操作的不可分割性,以避免中断。
-
避免共享:尽可能减少共享资源,使用线程局部存储(Thread-Local Storage, TLS)等。
正确理解和使用这些同步机制是确保多线程程序安全运行的关键。
5.2 线程间资源共享&线程加锁
在程序运行过程中,如果存在多线程,那么各个线程读写资源就会存在先后、同时读写资源的操作,因为是在不同线程,CPU调度过程中我们无法保证哪个线程会先读写资源,哪个线程后读写资源。因此为了防止数据读写混乱和错误的发生,我们要将线程在读写数据时加锁,这样就能保证操作同一个数据对象的线程只有一个,当这个线程执行完成之后解锁,其他的线程才能操作此数据对象。NSLock / NSConditionLock / NSRecursiveLock / @synchronized都可以实现线程上锁的操作。
- (void)saleTickets{
while (YES) {
[NSThread sleepForTimeInterval:1.0];
//互斥锁 -- 保证锁内的代码在同一时间内只有一个线程在执行
@synchronized (self){
//1.判断是否有票
if (self.tickets > 0) {
//2.如果有就卖一张
self.tickets --;
NSLog(@"还剩%d张票 %@",self.tickets,[NSThread currentThread]);
}else{
//3.没有票了提示
NSLog(@"卖完了 %@",[NSThread currentThread]);
break;
}
}
}
}
5.3 线程间的通信
在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
iOS中为什么刷新UI是在主线程中呢?
因为UIKit框架不是线程安全的,当多个线程同时操作UI的时候,可能会出现抢夺资源、读写问题,导致崩溃,UI异常等问题。
5.4 线程同步和线程安全
-
线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
-
线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作
-
非线程安全:没有锁。 解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。
iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各种方式。这里我们使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。
/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象
// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;
// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;
// 3.创建卖票操作 op1
__weak typeof(self) weakSelf = self;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];
// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];
// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}detachNewThreadWithBlock
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 加锁
[self.lock lock];
if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}
// 解锁
[self.lock unlock];
if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}
使用dispatch\_semaphore
/**
* 线程安全:使用 semaphore 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
5.5 实现一个线程安全的NSMutableArray
要实现一个线程安全的NSMutableArray,你可以创建一个自定义的封装类,使用内部同步机制(如NSLock、dispatch_semaphore_t或pthread_mutex_t)来保护对数组的所有访问。这里,我将展示如何使用NSLock来实现这样一个线程安全的数组。
### 步骤1: 创建封装类---首先,创建一个新的类,比如叫做`ThreadSafeArray`,这个类将封装一个`NSMutableArray`和一个`NSLock`来保证线程安全。
#import <Foundation/Foundation.h>
@interface ThreadSafeArray : NSObject {
NSMutableArray *internalArray;
NSLock *lock;
}
- (void)addObject:(id)object;
- (void)removeObject:(id)object;
- (id)objectAtIndex:(NSUInteger)index;
- (NSUInteger)count;
@end
### 步骤2: 实现方法:实现这个类的方法,确保每个方法在操作数组之前都获取锁,并在操作完成后释放锁。
#import "ThreadSafeArray.h"
@implementation ThreadSafeArray
- (instancetype)init {
self = [super init];
if (self) {
internalArray = [[NSMutableArray alloc] init];
lock = [[NSLock alloc] init];
}
return self;
}
- (void)addObject:(id)object {
[lock lock];
[internalArray addObject:object];
[lock unlock];
}
- (void)removeObject:(id)object {
[lock lock];
[internalArray removeObject:object];
[lock unlock];
}
- (id)objectAtIndex:(NSUInteger)index {
[lock lock];
id object = [internalArray objectAtIndex:index];
[lock unlock];
return object;
}
- (NSUInteger)count {
[lock lock];
NSUInteger count = [internalArray count];
[lock unlock];
return count;
}
@end
### 步骤3: 使用`ThreadSafeArray`:在你的项目中使用`ThreadSafeArray`来代替`NSMutableArray`,以确保线程安全。
ThreadSafeArray *safeArray = [[ThreadSafeArray alloc] init];
[safeArray addObject:@"Hello"];
[safeArray addObject:@"World"];
NSLog(@"Object at index 0: %@", [safeArray objectAtIndex:0]);
[safeArray removeObject:@"Hello"];
NSLog(@"Count after removal: %lu", (unsigned long)[safeArray count]);
-
使用锁确实可以提供线程安全,但它也可能成为性能瓶颈,特别是在高并发的情况下。因此,使用时需要权衡线程安全和性能。
-
在某些情况下,你可能需要考虑其他同步机制,如
**dispatch_queue(串行队列)或pthread_mutex_t** ,根据具体的使用场景选择最合适的同步机制。
通过上述方法,你可以创建一个线程安全的NSMutableArray封装,以保护数据在多线程环境下的安全性。
用@synchronized实现会怎么样
使用 @synchronized 关键字确实是一种在Objective-C中实现线程同步的简便方法。它可以保护代码块,确保同一时间内只有一个线程可以执行这个代码块。然而,尽管 @synchronized 提供了便利性,它在某些情况下可能不是最优的选择,主要由于以下几个原因:
- 性能开销。TODO:开销大的原因
@synchronized 的实现涉及到一些重量级的操作,包括创建和销毁递归互斥锁等。这些操作相比其他同步机制(如 NSLock、dispatch_semaphore_t 或 pthread_mutex_t)可能会带来更大的性能开销。在高并发的环境下,这种性能差异可能会变得更加明显。
- 可控性和灵活性
@synchronized 提供的是一种相对高级的抽象,它隐藏了锁的具体实现细节。这种简化虽然减少了编码复杂性,但同时也减少了开发者对锁行为的控制。例如,使用 NSLock 或 pthread_mutex_t 等工具时,开发者可以更精细地控制锁的行为,如尝试获取锁、定时锁等。
- 锁的粒度
使用 @synchronized 时,锁的粒度可能不够细。每次使用 @synchronized 都会锁定整个对象或整个代码块,有时候这种粗粒度锁可能不是最高效的选择。更细粒度的锁控制可以帮助提高应用性能,尤其是在只需要保护小部分代码时。
- 调试和错误处理出现死锁或其他同步问题时,使用
@synchronized可能使问题的调试变得更加困难,因为它的实现是黑箱操作。相比之下,使用如NSLock等显式的锁提供了更多的可视性和调试便利性。
总之,虽然 @synchronized 在某些情况下是一个方便的选择,但在需要高性能或更细粒度控制的场景中,考虑使用其他同步机制可能会更合适。
6、实现线程安全的NSString
要实现一个线程安全的`NSString`,可以通过封装一个自定义类并在内部使用同步机制(如`@synchronized`块或`NSLock`)来保护对`NSString`的访问。以下是一个简单的示例,展示了如何使用`@synchronized`来实现线程安全的字符串访问:
```objective-c
@interface ThreadSafeString : NSObject
@property (nonatomic, strong) NSString *safeString;
- (void)setSafeString:(NSString *)string;
- (NSString *)safeString;
@end
@implementation ThreadSafeString {
NSString *_internalString;
NSObject *_lockObject;
}
- (instancetype)init {
self = [super init];
if (self) {
_lockObject = [[NSObject alloc] init];
}
return self;
}
- (void)setSafeString:(NSString *)string {
@synchronized(_lockObject) {
_internalString = [string copy];
}
}
- (NSString *)safeString {
@synchronized(_lockObject) {
return _internalString;
}
}
@end
在这个示例中:
_internalString是实际存储字符串数据的私有变量。_lockObject是一个用于同步的对象,确保每次只有一个线程可以访问_internalString。- 通过
@synchronized块,我们保证了对_internalString的访问(读取和写入)是线程安全的。
使用@synchronized是实现线程安全的一种简单有效的方式,但它可能不是性能最优的选择,因为它会阻塞线程。在高性能要求的场景下,可能需要考虑其他线程同步机制,如NSLock、dispatch_semaphore_t或os_unfair_lock等。
7、多线程使用时导致的crash问题
在多线程环境下开发时,如果不正确地管理线程间的数据访问和资源共享,很容易导致应用程序崩溃(crash)。
以下是一些多线程使用时可能导致崩溃的常见问题及其解决方案:
1. 竞态条件(Race Condition)---TODO:只是数据异常,为什么会crash。??原子性是线程安全的吗
当两个或多个线程在没有适当同步的情况下访问某些共享数据,并且至少有一个线程在修改这些数据时,就会发生竞态条件。这可能导致数据损坏或不一致,进而引发崩溃。
解决方案:使用锁(如NSLock、@synchronized块)、信号量(dispatch_semaphore_t)或其他同步机制来保护对共享资源的访问。
2. 死锁(Deadlock)
当两个或多个线程互相等待对方释放锁,而导致它们都被阻塞时,就发生了死锁。这会导致应用程序挂起或崩溃。
解决方案:仔细设计锁的使用逻辑,避免嵌套锁。使用递归锁(NSRecursiveLock)可以在同一线程中多次获得同一个锁,以避免死锁。
3. 优先级反转(Priority Inversion)
当低优先级线程持有高优先级线程需要的资源时,如果中优先级线程占用了CPU时间,可能会导致高优先级线程长时间得不到执行,这就是优先级反转。
解决方案:使用优先级继承或优先级天花板等技术,确保高优先级线程能够及时获得所需资源。
4. 资源泄露
在多线程环境下,如果线程创建过多而没有被适当管理和释放,可能会导致资源泄露,最终耗尽系统资源,引发崩溃。
解决方案:使用线程池或限制线程的创建数量。确保线程在不再需要时能够被正确回收。
5. 对象生命周期管理错误
在多线程环境下,如果一个线程释放了另一个线程正在使用的对象,就可能导致野指针访问或僵尸对象访问,引发崩溃。
解决方案:在ARC环境下,确保正确使用强引用和弱引用。在手动引用计数(MRC)环境下,确保在多线程间共享对象时正确管理对象的生命周期。
6. 不是线程安全的API
有些API不是线程安全的,如果在多个线程中同时调用这些API,可能会导致不可预测的行为和崩溃。
解决方案:避免在多线程环境下使用不是线程安全的API,或者使用同步机制确保一次只有一个线程调用这些API。
总之,多线程编程需要仔细设计和测试,以确保线程间的正确同步和资源管理,从而避免崩溃。使用现代iOS开发中的并发编程工具和技术(如Grand Central Dispatch(GCD)和Operation Queues)可以帮助简化多线程编程,减少崩溃的风险。
7、优先级队列怎么设计
优先级队列是一种特殊的队列,其中的元素按照一定的优先级进行排序,优先级最高的元素最先被移除。在设计优先级队列时,主要考虑以下几个方面:
-
数据结构选择:
- 二叉堆(最小堆或最大堆)是实现优先级队列的常用数据结构,因为它可以有效地支持插入和删除操作,同时保持元素的有序性。
- 斐波那契堆、配对堆等是更高级的数据结构,可以提供更优的理论性能,适用于元素频繁变动的场景。
-
元素优先级:
- 需要定义一个明确的优先级规则,这通常通过比较函数或者比较器(Comparator)实现。
- 在某些情况下,优先级可以是元素的固有属性,如整数大小;在其他情况下,优先级可能需要根据应用逻辑动态计算。
-
插入操作:
- 插入新元素时,需要将其放置在正确的位置以保持队列的有序性。在二叉堆中,这通常涉及将元素添加到堆的末尾,然后执行上浮(或堆化)操作。
-
删除操作:
- 移除元素通常指移除优先级最高的元素。在二叉堆中,这涉及将堆顶元素(优先级最高)移除,然后将堆的最后一个元素移动到堆顶,最后执行下沉(或堆化)操作以恢复堆的有序性。
-
更新优先级:
- 如果应用场景需要动态更新元素的优先级,需要提供一种机制来调整已经在队列中的元素的位置。这可能涉及到元素的删除后再插入,或者直接在队列中调整元素的位置。
#import <Foundation/Foundation.h>
@interface PriorityQueue : NSObject {
NSMutableArray *heapArray;
}
- (void)insert:(NSNumber *)number;
- (NSNumber *)remove;
- (NSInteger)size;
@end
@implementation PriorityQueue
- (instancetype)init {
self = [super init];
if (self) {
heapArray = [[NSMutableArray alloc] init];
[heapArray addObject:[NSNull null]]; // 堆的索引从1开始
}
return self;
}
- (void)insert:(NSNumber *)number {
[heapArray addObject:number];
[self swim:[heapArray count] - 1];
}
- (NSNumber *)remove {
if ([heapArray count] <= 1) return nil;
NSNumber *min = heapArray[1];
heapArray[1] = [heapArray lastObject];
[heapArray removeLastObject];
[self sink:1];
return min;
}
- (NSInteger)size {
return [heapArray count] - 1;
}
// 上浮
- (void)swim:(NSInteger)index {
while (index > 1 && [heapArray[index / 2] compare:heapArray[index]] == NSOrderedDescending) {
[heapArray exchangeObjectAtIndex:index withObjectAtIndex:index / 2];
index = index / 2;
}
}
// 下沉
- (void)sink:(NSInteger)index {
while (2 * index < [heapArray count]) {
NSInteger j = 2 * index;
if (j < [heapArray count] - 1 && [heapArray[j] compare:heapArray[j + 1]] == NSOrderedDescending) j++;
if ([heapArray[index] compare:heapArray[j]] != NSOrderedDescending) break;
[heapArray exchangeObjectAtIndex:index withObjectAtIndex:j];
index = j;
}
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
PriorityQueue *pq = [[PriorityQueue alloc] init];
[pq insert:@(5)];
[pq insert:@(3)];
[pq insert:@(9)];
[pq insert:@(1)];
[pq insert:@(4)];
while ([pq size] > 0) {
NSLog(@"%@", [pq remove]);
}
}
return 0;
}
参考: juejin.cn/post/706596… juejin.cn/post/684490… demo:juejin.cn/post/684490…
juejin.cn/post/734510… todo:sdwebimage--NSOperation