iOS 多线程总结
- 2019 年 10 月 8 日
- 筆記
了解多线程,首先我们需要了解以下知识
进程
●进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app。 ●每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。
线程
●程序执行流的最小单元,线程是进程中的一个实体. ●一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
任务
任务就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)
同步
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务,不具备开启新线程的能力。
dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); NSLog(@"1"); dispatch_sync(concurrentQueue, ^(){ NSLog(@"2"); [NSThread sleepForTimeInterval:2]; NSLog(@"3"); }); NSLog(@"4");
异步
线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力。如果不是添加到主队列上,异步会在子线程中执行任务
dispatch_queue_t queue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // dispatch_async是异步方法。长时间处理,例如数据库访问 dispatch_async(dispatch_get_main_queue(), ^{ // 到主线程队列中执行 // 例如界面更新 }); });
队列
队列(Dispatch Queue):队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的
串行队列
// 串行队列DISPATCH_QUEUE_SERIAL // 并发队列DISPATCH_QUEUE_CONCURRENT dispatch_queue_t serialQueue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_SERIAL); NSLog(@"1"); dispatch_async(serialQueue, ^{ NSLog(@"2"); }); NSLog(@"3"); dispatch_sync(serialQueue, ^{ NSLog(@"4"); }); NSLog(@"5");
打印结果13245
首先先打印1 接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3然后是将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5 所以最终顺序就是13245。
并发队列(Concurrent Dispatch Queue):
同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效。
// 串行队列DISPATCH_QUEUE_SERIAL // 并发队列DISPATCH_QUEUE_CONCURRENT dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ sleep(1); [[NSThread currentThread] setName:@"任务A"]; NSLog(@"任务A thread:%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ sleep(1); [[NSThread currentThread] setName:@"任务B"]; NSLog(@"任务B thread:%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ sleep(1); [[NSThread currentThread] setName:@"任务C"]; NSLog(@"任务C thread:%@",[NSThread currentThread]); }); NSLog(@"结束");
打印
2019-08-30 10:34:02.718139+0800 TestDemo[56896:6617059] 结束 2019-08-30 10:34:03.722077+0800 TestDemo[56896:6617114] 任务B thread:<NSThread: 0x6000030407c0>{number = 4, name = 任务B} 2019-08-30 10:34:03.722092+0800 TestDemo[56896:6617111] 任务A thread:<NSThread: 0x60000305c3c0>{number = 5, name = 任务A} 2019-08-30 10:34:03.722091+0800 TestDemo[56896:6617112] 任务C thread:<NSThread: 0x60000305a6c0>{number = 3, name = 任务C}
iOS中的多线程
主要有三种:NSThread、NSoperationQueue、GCD
1. NSThread
NSThread轻量级别的多线程技术 需要自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造器方式它就会自动启动。只要是我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收。
手动开启线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数"]; // 当使用初始化方法出来的主线程需要start启动 [thread start]; // 可以为开辟的子线程起名字 thread.name = @"NSThread线程"; // 调整Thread的权限 线程权限的范围值为0 ~ 1 。越大权限越高,先执行的概率就会越高,由于是概率,所以并不能很准确的的实现我们想要的执行顺序,默认值是0.5 thread.threadPriority = 1; // 取消当前已经启动的线程 [thread cancel];
通过遍历构造器开辟子线程
[NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方式"];
2.NSoperationQueue
NSOperationQueue *queue = [[NSOperationQueue alloc]init]; NSBlockOperation *opA = [NSBlockOperation blockOperationWithBlock:^{ sleep(1); [[NSThread currentThread]setName:@"任务A"]; NSLog(@"任务A thread:%@",[NSThread currentThread]); }]; NSBlockOperation *opB = [NSBlockOperation blockOperationWithBlock:^{ sleep(10); [[NSThread currentThread]setName:@"任务B"]; NSLog(@"任务B thread:%@",[NSThread currentThread]); }]; NSBlockOperation *opC = [NSBlockOperation blockOperationWithBlock:^{ sleep(3); [[NSThread currentThread]setName:@"任务C"]; NSLog(@"任务C thread:%@",[NSThread currentThread]); }]; //添加依赖关系,保证执行顺序 [opC addDependency:opB]; [opB addDependency:opA]; [queue addOperation:opA]; [queue addOperation:opB]; [queue addOperation:opC];
打印
2019-08-30 10:44:35.753808+0800 TestDemo[57063:6627379] 任务A thread:<NSThread: 0x6000022360c0>{number = 3, name = 任务A} 2019-08-30 10:44:45.759742+0800 TestDemo[57063:6627377] 任务B thread:<NSThread: 0x6000022360c0>{number = 4, name = 任务B} 2019-08-30 10:44:48.765530+0800 TestDemo[57063:6627377] 任务C thread:<NSThread: 0x6000022360c0>{number = 4, name = 任务C}
GCD
dispatch_semaphore
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是 dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
Dispatch Semaphore 在实际开发中主要用于:
保持线程同步,将异步执行任务转换为同步执行任务 保证线程安全,为线程加锁
dispatch_semaphore_signal:
这个函数会使传入的信号量semaphore的值加1;
dispatch_semaphore_wait
这个函数会使传入的信号量semaphore的值减1;这个函数的作用是这样的,如果semaphore信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;如果semaphore的值为0,那么这个函数就阻塞当前线程等待timeout
卖火车票经典案例
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.ticketNumber = 100; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); for (NSInteger i = 0; i < 10; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(sellTicketsWithSemaphore) object:nil]; [thread setName:[NSString stringWithFormat:@"售票员-%zd",i]]; [thread start]; dispatch_semaphore_signal(semaphore); }); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } } - (void)sellTicketsWithSemaphore { while (true) { //信号==0就阻塞当前线程等待timeout,>0就继续执行下面的语句信号量的值减1 if (self.ticketNumber > 0) { self.ticketNumber --; NSLog(@"%@卖了一张票,还剩%ld张票",[[NSThread currentThread] name],self.ticketNumber); }else{ // 退出当前线程 [NSThread exit]; } } }
使用GCD如何实现这个需求:A、B、C 三个任务并发,完成后执行任务 D?
需要解决这个首先就需要了解dispatch_group_enter 和 dispatch_group_leave。
dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1 dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。 当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); [self requestA:^{ NSLog(@"---执行A任务结束---"); dispatch_group_leave(group); }]; dispatch_group_enter(group); [self requestB:^{ NSLog(@"---执行B任务结束---"); dispatch_group_leave(group); }]; dispatch_group_enter(group); [self requestC:^{ NSLog(@"---执行C任务结束---"); dispatch_group_leave(group); }]; dispatch_group_notify(group, globalQueue, ^{ [self requestD:^{ NSLog(@"---执行D任务结束---"); }]; });
- (void)requestA:(void(^)(void))block{ NSLog(@"---执行A任务开始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); } - (void)requestB:(void(^)(void))block{ NSLog(@"---执行B任务开始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); } - (void)requestC:(void(^)(void))block{ NSLog(@"---执行C任务开始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); } - (void)requestD:(void(^)(void))block{ NSLog(@"---执行D任务开始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); }
线程间通信
什么叫做线程间通信?
在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
线程间通信的体现
●1个线程传递数据给另1个线程 ●在1个线程中执行完特定任务后,转到另1个线程继续执行任务
线程间通信常用方法
- NSThread :一个线程传递数据给另一个线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
//点击屏幕开始执行下载方法 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self performSelectorInBackground:@selector(download) withObject:nil]; } //下载图片 - (void)download { // 1.图片地址 NSString *urlStr = @"http://d.jpg"; NSURL *url = [NSURL URLWithString:urlStr]; // 2.根据地址下载图片的二进制数据 NSData *data = [NSData dataWithContentsOfURL:url]; // 3.设置图片 UIImage *image = [UIImage imageWithData:data]; // 4.回到主线程,刷新UI界面(为了线程安全) [self performSelectorOnMainThread:@selector(downloadFinished:) withObject:image waitUntilDone:NO]; // [self performSelector:@selector(downloadFinished:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES]; } - (void)downloadFinished:(UIImage *)image { self.imageView.image = image; NSLog(@"downloadFinished---%@", [NSThread currentThread]); }
- GCD :一个线程传递数据给另一个线程
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"donwload---%@", [NSThread currentThread]); // 1.子线程下载图片 NSURL *url = [NSURL URLWithString:@"http://d.jpg"]; NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; // 2.回到主线程设置图片 dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"setting---%@ %@", [NSThread currentThread], image); [self.button setImage:image forState:UIControlStateNormal]; }); });
死锁
1、定义: 所谓死锁,通常指有两个线程T1和T2都卡住了,并等待对方完成某些操作。T1不能完成是因为它在等待T2完成。但T2也不能完成,因为它在等待T1完成。于是大家都完不成,就导致了死锁。
2、产生死锁的条件: 产生死锁的四个必要条件: (1) 互斥条件:一个资源每次只能被一个进程使用。 (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 (3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 (4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
首先来看一份导致死锁的典型代码:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. NSLog(@"执行任务1"); dispatch_sync(dispatch_get_main_queue(), ^(void){ NSLog(@"这里死锁了"); }); NSLog(@"执行任务3"); }
输出:执行任务1 后死锁了 原因:在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。 同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。 而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。