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个线程继续执行任务

线程间通信常用方法
  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]);  }
  1. 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和这个任务就形成了相互循环等待,就造成了死锁。