ios-多线程(NSThread,GCD,NSOperation
线程:
英文:Thread
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。 -----百度百科
ios中实现多线程的几种方式:
- Pthreads(不用)
- NSThread(用一部分,其中几个比较方便的方法)
- GCD(常用)
- NSOperation&NSOperationQueue(看需求)
- Pthreads
pthread 是 POSIX 多线程开发框架,是基于C 语言的跨平台框架。没用过,不了解,感兴趣的同学可以自己度娘下。
- NSThread
NSThread是基于Thread使用,轻量级的多线程编程方法,一个NSThread对象代表一个线程,需要手动管理线程的生命周期,处理线程同步等问题。所以一般只使用其中几个方法,方便调试线程。
[NSThread isMainThread]; // 是否主线程
[NSThread currentThread]; // 当前线程
- GCD
Apple基本c++开发的一套多线程处理技术,自动管理生命周期。
任务与队列
任务:你要执行的操作,GCD将任务放在block中。执行任务有2中方式,同步执行,异步执行。
- 同步
不具备开启线程的能力,会阻塞当前线程。 - 异步
具备开启新线程的能力,不会阻塞当前线程。
队列:用来存放任务的队列,是一种特殊的线性表,采用FIFO(先进先出)的原则,则从顶部开始读取任务,从尾部加入任务到队列。在GCD中有3种队列:串行队列,并行队列,主队列(特殊的串行队列)。
- 串行队列
一个一个任务有序执行,上一个任务没执行完毕,下一个任务不会执行。 - 并行队列
同时执行多个任务,不用等待上一个任务执行完毕。 - 主队列
和串行队列一样,需要等待上一个任务执行完成,才能执行下一个任务。
创建队列:
- 全局并行队列(系统自带的,全局唯一)
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- 自定义并行队列:
dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
- 自定义串行队列:
dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
- 主队列:
dispatch_get_main_queue()
创建任务:
- 同步任务
dispatch_sync(队列, ^{要执行的任务});
- 异步任务
dispatch_async(队列, ^{要执行的任务});
基本使用:
- 同步任务+串行队列(不会开启新线程,在当前线程中执行任务)
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
- 同步任务+并行队列(不会开启新线程,在当前线程中执行任务)
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
- 同步任务+主队列(不会开启新线程,在主线程中执行任务)
dispatch_sync(dispatch_get_main_queue(), 0), ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
- 异步任务+串行队列(会开启一条新的线程,串行执行任务)
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
- 异步任务+并行队列(会开启至少一条新的线程,并行执行任务)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
- 异步任务+主队列(不会开启新的线程,会在主线程中执行任务)
dispatch_async(dispatch_get_main_queue(), 0), ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
总结:
1.同步任务都不会开启新的线程,所以会阻塞当前线程。
2.主队列中的任务不会开启新的线程,会在主线程中执行。
3.异步任务+串行队列,会开启一条新的线程,在新的线程串行执行任务。
4.异步任务+并行多列,会开启至少一条新的线程,在新的线程中并发执行任务。
线程阻塞:
实例1:
NSLog(@"当前线程----->%@",[NSThread currentThread]); // 会打印
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]); // 这句话永远不会打印,此时主线程已经阻塞了,你对界面的所有操作都没反应了。
});
NSLog(@"当前线程----->%@",[NSThread currentThread]); // 不会打印
原因:
1.dispatch_sync同步任务,不会开启新的线程,所以上面的代码是在主线程中执行的,也就会阻塞主线程,等待block中的任务完成。
2.dispatch_get_main_queue()主队列,会把block中的任务放进主队列,也就是主线程中去执行,可是此时主线程已经阻塞了,block永远无法完成任务。所以就会一直阻塞主线程。
实例2:
// 自定义串行队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
// 串行异步
dispatch_async(queue, ^{ // @1
NSLog(@"当前线程----->%@",[NSThread currentThread]); // 会打印
dispatch_sync(queue, ^{ // 不会打印 @2
NSLog(@"当前线程1----->%@",[NSThread currentThread]);
});
NSLog(@"当前线程2----->%@",[NSThread currentThread]); // 不会打印
});
NSLog(@"当前线程3----->%@",[NSThread currentThread]); // 会打印 @3
原因:
1.上面的代码@1会开启一条新的线程,但因为是在串行队列中,所以会一个一个执行任务。我们假设开启的新线程叫“B”;
2.打印完"当前线程"后,@2同步任务,不会开启新的线程,会阻塞当前线程,所以还是在"B"线程中执行任务,此时线程"B"已经阻塞了,@2会把block当中的任务放入"myQueue"中去执行,但是"myQueue"是串行的,所以必须等"myQueue"执行完上一个任务,而它执行的上一个任务就是当前block中的任务,也就是阻塞了的@2,@2永远执行不了,所以会一直阻塞。
3.@3会打印,是因为它是在主线程中的。
实例3:
// 创建并行队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // @1
NSLog(@"当前线程----->%@",[NSThread currentThread]);
dispatch_sync(queue, ^{ // @2
NSLog(@"当前线程1----->%@",[NSThread currentThread]);
});
NSLog(@"当前线程2----->%@",[NSThread currentThread]); // @3
});
NSLog(@"当前线程3----->%@",[NSThread currentThread]); // @4
-----------------------------以上都会打印-----------------------------
原因:
1.@1会开启至少一条新的线程,并行执行任务。
2.@2不会开启新的线程,在当前线程并行执行任务。会阻塞当前线程,但因为是并行队列中,所以会执行完@2,在执行@3.
3.@3在主线程中执行,不受影响。
队列组
// 创建组
dispatch_group_t group = dispatch_group_create();
// 系统全局唯一并行队列
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 自定义串行队列,按顺序执行
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_group_async(group, queue, ^{ // @1
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{ // @2
NSLog(@"当前线程1----->%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{ // @3
NSLog(@"当前线程2----->%@",[NSThread currentThread]);
});
dispatch_group_notify(group, queue, ^{ // @4
NSLog(@"当前线程3----->%@",[NSThread currentThread]);
});
- 并行队列
@1,2,3会随机打印,最后打印@4- 串行队列
@1<@2<@3<@4 按顺序打印
单列
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
延时执行
// 如果在串行、并行队列中执行,会开启线程。也就是说dispatch_after方法是异步执行的
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
栅栏方法(分割任务)
// dispatch_barrier_async 方法需使用自定义队列,不能使用系统全局队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{ // @1
NSLog(@"当前线程----->%@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{ // @2
NSLog(@"当前线程1----->%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{ // @3
NSLog(@"当前线程2----->%@",[NSThread currentThread]);
});
注意:
使用dispatch_barrier_async时:
1.必须使用自定义队列,不能使用系统全局队列。
dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT)
NSOperation&NSOperationQueue
NSOperation是Apple对GCD的封装,是面向对象的。NSOperation、NSOperationQueue分别对应GCD中的任务和队列。
注意:NSOperation是个抽象类,不能直接使用,必须使用它的2个子类:NSInvocationOperation、NSBlockOperation。
创建任务:
- NSInvocationOperation
NSInvocationOperation *invocationOP = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
[invocationOP start];
- NSBlockOperation
NSBlockOperation *blockOP = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前线程---->%@",[NSThread currentThread]);
}];
[blockOP addExecutionBlock:^{
NSLog(@"当前线程1---->%@",[NSThread currentThread]);
}];
[blockOP start];
注意:
1.addExecutionBlock方法可能开启新的线程,也可能在主线程中执行。
2.addExecutionBlock方法调用必须在start方法之前,否则会报错。
- 自定义任务:新建一个类继承NSOperation,需要重写main,cancel,finished,executing等方法。
创建队列:(只有主队列,和其他队列,没有串行和并行区分)
- NSOperationQueue(其他队列)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
- 主队列
[NSOperationQueue mainQueue];
使用:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1; // 串行,默认为-1不限制,既并行
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前线程---->%@",[NSThread currentThread]);
}];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前线程1---->%@",[NSThread currentThread]);
}];
[queue addOperation:operation];
[queue addOperation:operation1];
注意:任务加入队列中,会自动执行,不需要调用start方法,否则会报错。
依赖
必须等A任务执行完毕之后在执行B任务。
比如从网上开启一个线程下载图片,必须等图片下载完成之后在主线程加载图片,刷新UI,这个时候就可以用上依赖了。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//queue.maxConcurrentOperationCount = 1; // 串行,默认为-1不限制
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前线程---->%@",[NSThread currentThread]);
}];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前线程1---->%@",[NSThread currentThread]);
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前线程2---->%@",[NSThread currentThread]);
}];
[operation addDependency:operation1]; // operation依赖operation1
[operation2 addDependency:operation]; // 2operation依赖operation
[queue addOperations:@[operation,operation1] waitUntilFinished:NO];
[NSOperationQueue.mainQueue addOperation:operation2];
2017-08-10 10:46:34.938 ZTSlidingVC[1273:222606] 当前线程1----><NSThread: 0x7fd9f3dd9510>{number = 2, name = (null)}
2017-08-10 10:46:34.938 ZTSlidingVC[1273:222606] 当前线程----><NSThread: 0x7fd9f3dd9510>{number = 2, name = (null)}
2017-08-10 10:46:34.948 ZTSlidingVC[1273:222557] 当前线程2----><NSThread: 0x7fd9f3d287a0>{number = 1, name = main}
注意:依赖关系是可以跨队列的,如上面例子所示。
其他属性、方法
- NSOperation
属性:
@property (readonly, getter=isCancelled) BOOL cancelled; // 是否取消任务
@property (readonly, getter=isExecuting) BOOL executing; // 是否正在执行
@property (readonly, getter=isFinished) BOOL finished; // 是否完成任务
方法:
- (void)cancel; // 取消任务
- (void)start; // 开始任务
- (void)addDependency:(NSOperation *)op; // 添加依赖
- (void)removeDependency:(NSOperation *)op; // 删除依赖
- NSOperationQueue
属性:
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0); // 获取队列中的任务数量
@property NSInteger maxConcurrentOperationCount; // 设置最大任务数
@property (getter=isSuspended) BOOL suspended; // YES:暂停,NO:继续(对正在执行的任务无效,只是暂停调度新的任务执行)
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0); // 获取当前队列
@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0); // 获取主队列
方法:
- (void)cancelAllOperations; // 取消所有任务
- (void)waitUntilAllOperationsAreFinished; // 等待所有队列中的任务执行完成,会阻塞线程(在等待时,其他线程仍然可以往队列中添加任务)
线程同步
- 为什么需要线程同步:
当多个线程同时访问一个统一资源,造成数据状态不一致,产生的数据混乱,安全等问题。 - 实现线程同步的2种方式:
1.加锁
- @synchronized 关键字加锁
- NSLock 对象锁
- NSCondition
- NSConditionLock 条件锁
- NSRecursiveLock 递归锁
- pthread_mutex 互斥锁(C语言)
- dispatch_semaphore 信号量实现加锁(GCD)
- OSSpinLock
方法有点多,这就不一一介绍了,开发中也用不了这么多。这里就简单介绍一下1,2,7的使用把,需要其他更详细的的功能请自行goolge。
- @synchronized 关键字加锁(性能较差,使用简单)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
NSLog(@"做你想做的事");
}
});
- NSLock(性能一般)
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([lock tryLock]) { // 尝试加锁,如果失败了,并不会阻塞线程,只是立即返回NO
NSLog(@"做你想做的事");
[lock unlock]; // 记得解锁
}
});
- dispatch_semaphore 信号量实现加锁(GCD,推荐使用此方法)
dispatch_semaphore_create 创建一个semaphore
dispatch_semaphore_signal 发送一个信号(计数器+1)
dispatch_semaphore_wait 等待信号(信号量-1,如果信号量<=0,则一直等待,会阻塞线程)
dispatch_semaphore_t dsema = dispatch_semaphore_create(2); // 创建信号量,后面的数字既最大并发量
for(int i=0; i<10; i++){
dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER); // -1 DISPATCH_TIME_FOREVER会一直等待,直到信号量大于0。DISPATCH_TIME_NOW不等待,也就不能控制线程并发数了。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"------>%d 当前线程---->%@",i,[NSThread currentThread]);
dispatch_semaphore_signal(dsema); // +1
});
}
上面的列子,看起来创建了10个线程,其实同时只有2个线程在并发执行。
2.使用串行队列
参考:
http://www.jianshu.com/p/0b0d9b1f1f19
http://ksnowlv.github.io/blog/2014/09/07/ios-tong-bu-suo-xing-neng-dui-bi/