iOS 多线程总结

2,755 阅读30分钟

本文导读一些相关总结(同步、异步、并行、串行概念,死锁,GCD、NSOperation对比)
一、一些多线程相关
二、多线程 - GCD
三、多线程 - NSOperation
四、多线程 - NSThread
五、线程安全
六、多线程面试题 七、进程与线程概念  

本文导读

同步、异步、并行、串行

同步任务优先级高,在线程中有执行顺序,不会开启新的线程。
异步任务优先级低,在线程中执行没有顺序,看cpu闲不闲。在主队列中不会开启新的线程,其他队列会开启新的线程。

并行和串行决定了任务的执行方式。串行队列,线程按顺序执行,不会同时执行。并行队列,会并发执行。

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁的必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

GCD、NSOperation对比:

GCD 是纯 C 语言的API,NSOperationQueue 是基于 GCD 的 OC 版本封装。

GCD 执行速度比 NSOperationQueue 快(封装GCD,更高层的东西,性能不好,因为还要转化)、有一次性执行、延迟执行、调度组等函数。提供了更多的控制能力以及操作队列中所不能使用的底层函数。

NSOperationQueue 支持设置依赖关系、监听(暂停、继续、挂起、取消)、可以很方便的调整执行顺序、最大并发数量。

在项目什么时候选择使用GCD,什么时候选择NSOperation?

  • NSOperation:
    任务之间有依赖,或者要监听任务的执行情况。
    优点是是对线程高度抽象,使程序结构更好,子类化 NSOperation 的设计思路具有面向对象的优点(复用、封装),使接口简单,建议在复杂项目中用。
  • GCD:
    任务之间不太依赖。
    优点是 GCD 本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而 Block 参数的使用,会使代码易读,建议在简单项目中使用。

一、一些多线程相关

多线程原理

  • 同一时间,CPU只能处理1条线程,只有一条线程在工作(执行)
  • 多线程并发(同时)执行,其实是CPU快速的在多条线程之间调度(切换)
  • 如果线程非常多,会发生什么情况?
    1. CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源
    2. 每条线程被调度执行的频次会降低(线程执行效率降低)

多线程优缺点

优点:

  • 能适当的提高程序的执行效率
  • 能适当的提高资源利用率(CPU、内存利用率) 缺点:
  • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB,如果开启大量的线程,会占用大量的内存空间,降低程序的性能)
  • 线程越多,CPU在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程之间的通信、多线程的数据共享

多线程实现方案

二、多线程-GCD

结构图

GCD简介

全称 Grand Central Dispatch,纯 C 语言,提供了非常多强大的函数

  • 优势:
    · 苹果为多核并行运算提出的解决方案,GCD会自动利用更多的 CPU 内核(比如双核、四核)
    · GCD 自动管理线程的生命周期,如创建线程、调度任务、销毁线程。
    · 程序员只需要告诉 GCD 想要执行什么任务,不需要编写线程管理代码。

  • GCD内部是怎么实现的?

    1. iOS 和 OS X 的核心是 XNU 内核,GCD 是基于 XNU 内核实现的。
    2. GCD 的 API 全部在 libdispatch 库中。
    3. GCD 底层实现主要有 Dispatch Queue 和 Dispatch Source
      · Dispatch Queue: 管理block(操作)
      · Dispatch Source:处理事件(MACH 端口发送,MACH 端口接收,检测与进程相关事件等10种事件)
  • GCD 中2个核心概念和使用:
    GCD 2个核心概念任务(执行什么操作)、队列(用来存放任务),GCD 使用就是:

    1. 定制任务:确定想要做的事
    2. 将任务添加到队列中:GCD会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的FIFO原则(先进先出,后进后出)
GCD 任务执行方式 - dispatch_sync、dispatch_async

dispatch_sync:只能在当前线程中执行任务,不具备开启新线程的能力。
dispatch_async:可以在新的线程中执行任务,具备开启新线程的能力

GCD 队列类型 - 并行、串行

并行
· 队列中的任务 通常 会并发执行
· 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
· 并发功能只有在异步(dispatch_async)函数下才有效
串行
· 队列中的任务 会顺序执行
· 让任务一个接一个的执行(一个任务执行完毕后,再执行下一个任务)

并行、串行 和 同步、异步结合效果图

注意: 使用 sync 函数往当前串行队列中添加任务,会卡住当前的串行队列。

几种队列

主队列:dispatch_get_main_queue()
全局队列 dispatch_get_global_queue

dispatch_queue_t dispatch_get_global_queue(
dispatch_queue_priority_t priority, // 队列的优先级
unsigned long flags); // 此参数暂时无用,用0即可
// 获得全局并发队列
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
全局并发队列的优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
  • 全局队列与并行队列的区别
    (1) 不需要创建,直接 GET 就能用
    (2) 两个队列的执行效果相同
    (3) 全局队列没有名称,调试时,无法确认准确队列
    (4) 全局队列有高中默认优先级

并行队列dispatch_queue_t q = dispatch_queue_create("queuename", DISPATCH_QUEUE_CONCURRENT);
串行队列

dispatch_queue_t
dispatch_queue_create(const char *label, // 队列名称 
dispatch_queue_attr_t attr); // 队列属性,一般用NULL即可
dispatch_queue_t t = dispatch_queue_create("queuename",DISPATCH_QUEUE_SERIAL);

主队列和 GCD 创建的队列的区别:
主队列:主队列中不能开启同步,会阻塞主线程。只能开启异步任务,开启异步任务也不会开启新的线程,只是降低异步任务的优先级,让cpu空闲的时候才去调用。而同步任务,会抢占主线程的资源,会造成死锁。
GCD 创建的队列:GCD 创建的队列优先级没有主队列高,所以在 GCD 中的串行队列开启同步任务里面没有嵌套任务是不会阻塞主线程,只有一种可能导致死锁,就是串行队列里,嵌套开启任务,有可能会导致死锁。 

从子线程回到主线程
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行耗时的异步操作...
    dispatch_async(dispatch_get_main_queue(), ^{
       // 回到主线程,执行UI刷新操作
    });
});
一次性代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});

GCD - dispatch_group、dispatch_group_enter/dispatch_group_leve

dispatch_group_enter:通知 group,下个任务要放入 group 中执行了。 dispatch_group_leave:通知 group,任务成功完成,要移除,与 enter成对出现。
多个网络请求都完成以后,在进行下一步。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_group_async(group, queue, ^{
    dispatch_group_enter(group);
    NSLog(@"网络请求1---%@", [NSThread currentThread]);
    [AFNetWork request:@"" success:^(NetworkResult *res) {	dispatch_group_leave(group);    }];
});

dispatch_group_async(group, queue, ^{
    dispatch_group_enter(group);
    NSLog(@"网络请求12---%@", [NSThread currentThread]);
    [AFNetWork request:@"" success:^(NetworkResult *res) {	dispatch_group_leave(group);    }];
});

// 获得所有调度组里面的异步任务完成的通知
//    dispatch_group_notify(group, queue, ^{
//        NSLog(@"下一步---%@", [NSThread currentThread]); 
//    });

// 注意点: 在调度组完成通知里,可以跨队列通信
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 更新UI,在主线程
    NSLog(@"下一步---%@", [NSThread currentThread]); 
});

GCD - semaphore

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER),这个时候线程会等待,阻塞当前线程,直到dispatch_semaphore_signal(sem)调用之后

semaphore 实现顺序执行

image.png

semaphore 实现最大并发数

image.png

semaphore 实现 dispatch_group

image.png

GCD - 栅栏函数 dispatch_barrier_(a)sync

在并行队列中创造一个同步点,用于资源保护,防止同一资源被同时使用。

dispatch_barrier_async 和 dispatch_barrier_sync 区别

dispatch_barrier_sync 会阻塞主线程的任务,而 dispatch_barrier_async 不会阻塞主线程的任务。

image.png image.png image.png image.png

作用如:打造线程安全的 NSMutableArray

NSMutableArray 本身是线程不安全的。
在《Effective Objective-C 2.0..》书中第41条写到多用派发队列,少用同步锁中指出:使用“串行同步队列”(serial synchronization queue),将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。而通过并发队列,结合 GCD 的栅栏块(barrier)来不仅实现数据同步线程安全,还比串行同步队列方式更高效。 打造线程安全的 NSMutableArray 重写一下方法,用 dispatch_barrier_sync 同步。

- (void)addObject:(id)anObject;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;

不使用 atomic 打造线程安全的原因:

  1. atomic 仅保证了属性的 setter/getter 方法是原子性的,为 setter 方法加锁,是线程安全的,但是属性的其他方法,如数组添加/移除元素等并不是原子操作,所以不能保证属性是线程安全的。
  2. atomic 虽然保证了getter/setter 方法线程安全,但是付出的代价很大,执行效率要比nonatomic 慢很多倍(有说法是慢10-20倍)。

总之,使用 nonatomic 修饰 NSMutableArray 对象就可以了,而使用锁、dispatch_queue 来保证 NSMutableArray 对象的线程安全。

GCD 面试题

异步里面的延迟

image.png 原因是子线程的 NSRunLoop 默认没有开启,而 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; 内部是通过 NSTimer 实现,在 NSRunLoop 没有开启的情况下,NSTimer 不会得到正常运行。

dispatch_get_global_queue 的 sync 嵌套

image.png image.png

几种任务嵌套:

  • 在主队列开启异步任务,不会开启新的线程而是依然在主线程中执行代码块中的代码。为什么不会阻塞线程?
    主队列开启异步任务,虽然不会开启新的线程,但是他会把异步任务降低优先级,等闲着的时候,就会在主线程上执行异步任务。

  • 在主队列开启同步任务,为什么会阻塞线程?
    主队列是串行队列,里面的线程是有顺序的,先执行完一个线程才执行下一个线程,而主队列始终就只有一个主线程,主线程是不会执行完毕的,因为他是无限循环的,除非关闭程序。因此在主线程开启一个同步任务,同步任务会想抢占执行的资源,而主线程任务一直在执行某些操作,不肯放手。两个的优先级都很高,最终导致死锁,阻塞线程。

  • 主线程队列注意: 下面代码执行顺序 1111 2222

    - (void)main_queue_deadlock {
        dispatch_queue_t q = dispatch_get_main_queue();
        NSLog(@"1111");
        dispatch_async(q, ^{
            NSLog(@"主队列异步 %@", [NSThread currentThread]);
        });
        NSLog(@"2222");
        // 下面会造成线程死锁
    //    dispatch_sync(q, ^{
    //        NSLog(@"主队列同步 %@", [NSThread currentThread]);
    //    });
    }
    
  • 串行队列开启异步任务后嵌套同步任务造成死锁

    dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_async(q, ^{
        NSLog(@"异步任务 %@", [NSThread currentThread]);
        // 下面开启同步造成死锁:因为串行队列中线程是有执行顺序的,需要等上面开启的异步任务执行完毕,才会执行下面开启的同步任务。而上面的异步任务还没执行完,要到下面的大括号才算执行完毕,而下面的同步任务已经在抢占资源了,就会发生死锁。
        dispatch_sync(q, ^{
            NSLog(@"同步任务 %@", [NSThread currentThread]);
        });
    });
    
  • 串行队列开启同步任务后嵌套同步任务造成死锁

    dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(q, ^{
      NSLog(@"同步任务 %@", [NSThread currentThread]);
      // 下面开启同步造成死锁:因为串行队列中线程是有执行顺序的,需要等上面开启的同步任务执行完毕,才会执行下面开启的同步任务。而上面的同步任务还没执行完,要到下面的大括号才算执行完毕,而下面的同步任务已经在抢占资源了,就会发生死锁。
      dispatch_sync(q, ^{
             NSLog(@"同步任务 %@", [NSThread currentThread]);
      });
    });
    NSLog(@"同步任务 %@", [NSThread currentThread]);
    
  • 串行队列开启同步任务后嵌套异步任务不造成死锁

  • 死锁举例

    NSLog(@"执行任务1");
    dispatch_queue_t queue = dispatch_queue_create(@"myqueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create(@"myqueue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create(@"myqueue3", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
        // 会造成死锁
    //        dispatch_sync(queue, ^{
    //            NSLog(@"执行任务3");
    //        });
         // 换成并发队列,就不会产生死锁,放在不同的队列中,就不存在互相等待的问题
          dispatch_sync(queue2, ^{
              NSLog(@"执行任务3");
          });
    
        // 换一个串行队列 也不会产生死锁
        dispatch_sync(queue3, ^{
            NSLog(@"执行任务3");
        });
        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
    
  • 并行队列的任务嵌套例子

    dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    // 任务嵌套
    dispatch_sync(q, ^{
        NSLog(@"1 %@", [NSThread currentThread]);
        dispatch_sync(q, ^{
            NSLog(@"2 %@", [NSThread currentThread]);
            dispatch_sync(q, ^{
                NSLog(@"3 %@", [NSThread currentThread]);
            });
        });
        dispatch_async(q, ^{
            NSLog(@"4 %@", [NSThread currentThread]);
        });
        NSLog(@"5 %@", [NSThread currentThread]);
    });
    // 运行结果是: 12345 或12354  
    

    并行队列里开启同步任务是有执行顺序的,只有异步才没有顺序。
    串行队列开启异步任务,是有顺序的。

三、多线程-NSOperation:

结构图

NSOperation和NSOperationQueue实现多线程的具体步骤

  1. 先将需要执行的操作封装到一个NSOperation对象中
  2. 然后将NSOperation对象添加到NSOperationQueue中
  3. 系统会自动将NSOperationQueue中的NSOperation取出来
  4. 将取出的NSOperation封装的操作放到一条新线程中执行

NSOperation queue

存放NSOperation的集合类。
(1) 用来存放NSOperation对象的队列,可以用来异步执行一些操作
(2) 一般可以用在网络请求等耗时操作
解释:操作和操作队列,基本可以看成java中的线程和线程池的概念。用于处理ios多线程开发的问题。网上部分资料提到一点是,虽然是queue,但是却并不是带有队列的概念,放入的操作并非是按照严格的先进现出。
这边又有个疑点是,对于队列来说,先进先出的概念是Afunc添加进队列,Bfunc紧跟着也进入队列,Afunc先执行这个是必然的,但是Bfunc是等Afunc完全操作完以后,B才开始启动并且执行,因此队列的概念离乱上有点违背了多线程处理这个概念。但是转念一想其实可以参考银行的取票和叫号系统。因此对于A比B先排队取票但是B率先执行完操作,我们亦然可以感性认为这还是一个队列。但是后来看到一票关于这操作队列话题的文章,其中有一句提到“因为两个操作提交的时间间隔很近,线程池中的线程,谁先启动是不定的。”瞬间觉得这个queue名字有点忽悠人了,还不如pool~综合一点,我们知道他可以比较大的用处在于可以帮组多线程编程就好了。

常用方法

  • NSOperation的子类 使用NSOperation子类的方式有3种:
    1. NSInvocationOperation
    2. NSBlockOperation
    3. 自定义子类继承NSOperation,实现内部相应的方法
    • NSInvocationOperation
      • 创建NSInvocationOperation对象
        - (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
      • 调用start方法开始执行操作 - (void)start; ,一旦执行操作,就会调用target的sel方法
      • 注意 默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作
        只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作
    • NSBlockOperation
      • 创建NSBlockOperation对象 + (id)blockOperationWithBlock:(void (^)(void))block;
      • 通过addExecutionBlock:方法添加更多的操作 - (void)addExecutionBlock:(void (^)(void))block;
      • 注意:只要NSBlockOperation封装的操作数 > 1,就会异步执行操作
    • NSOperationQueue
      • NSOperationQueue的作用
        NSOperation可以调用start方法来执行任务,但默认是同步执行的
        如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
      • 添加到非主队列中的操作,都会放到子线程去执行: [[NSOperationQueue alloc] init];
        添加到主队列中的操作,都会放到主线程去执行: [NSOperationQueue mainQueue];
      • 添加操作到NSOperationQueue中
        - (void)addOperation:(NSOperation *)op;
        - (void)addOperationWithBlock:(void (^)(void))block;
  • 最大并发数
    最大并发数的相关方法
    - (NSInteger)maxConcurrentOperationCount;
    - (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
  • 队列的取消、暂停、恢复
    • 取消队列的所有操作
      - (void)cancelAllOperations;
      提示:也可以调用NSOperation的- (void)cancel方法取消单个操作
    • 暂停和恢复队列
      - (void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列
      - (BOOL)isSuspended;
  • 添加依赖
    // 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 创建3个操作
    NSOperation *a = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operationA---");
    }];
    NSOperation *b = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operationB---");
    }];
    NSOperation *c = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operationC---");
    }];
    
    // 添加依赖
    [c addDependency:a];
    [c addDependency:b];
    
    // 执行操作
    [queue addOperation:a];
    [queue addOperation:b];
    [queue addOperation:c];
  • 可以在不同queue的NSOperation之间创建依赖关系
  • 注意:不能相互依赖 , 比如A依赖B,B依赖A
  • 操作优先级
    • 设置NSOperation在queue中的优先级,可以改变操作的执行优先级
      - (NSOperationQueuePriority)queuePriority;
      - (void)setQueuePriority:(NSOperationQueuePriority)p;
    • 优先级的取值
      NSOperationQueuePriorityVeryLow = -8L,
      NSOperationQueuePriorityLow = -4L,
      NSOperationQueuePriorityNormal = 0,
      NSOperationQueuePriorityHigh = 4,
      NSOperationQueuePriorityVeryHigh = 8
  • 操作的监听
    可以监听一个操作的执行完毕
    - (void (^)(void))completionBlock;
    - (void)setCompletionBlock:(void (^)(void))block;

自定义NSOperation

  1. 自定义NSOperation的步骤很简单
    重写- (void)main方法,在里面实现想执行的任务
  2. 重写- (void)main方法的注意点
    自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池)
    经常通过- (BOOL)isCancelled方法检测操作是否被取消,对取消做出响应

四、多线程-NSThread

结构图

创建线程的方式

// 创建线程的方式1
- (void)createThread1 {
    // 创建线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download:) object:@"http://a.png"];
    thread.name = @"下载线程";
    
    // 启动线程(调用self的download方法)
    [thread start];
}
// 创建线程的方式2
- (void)createThread2 {
    [NSThread detachNewThreadSelector:@selector(download:) toTarget:self withObject:@"http://b.jpg"];
}
// 创建线程的方式3
- (void)createThread3 {
    // 这2个不会创建线程,在当前线程中执行
    // [self performSelector:@selector(download:) withObject:@"http://c.gif"];
    // [self download:@"http://c.gif"];
    [self performSelectorInBackground:@selector(download:) withObject:@"http://c.gif"];
}

第2、3种创建线程方式的优缺点
优点:简单快捷
缺点:无法对线程进行更详细的设置

线程安全

-(void)threadSafe {
    self.leftTicketCount = 50;
    
    self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread1.name = @"1号窗口";
    
    self.thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread2.name = @"2号窗口";
    
    self.thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread3.name = @"3号窗口";
    
    [self threadSafeStart];
}
- (void)threadSafeStart {
    [self.thread1 start];
    [self.thread2 start];
    [self.thread3 start];
}
// 卖票
- (void)saleTicket {
    while (1) {
        // ()小括号里面放的是锁对象
        @synchronized(self) { // 开始加锁
            int count = self.leftTicketCount;
            if (count > 0) {
                [NSThread sleepForTimeInterval:0.05];
                
                self.leftTicketCount = count - 1;
                
                NSLog(@"%@卖了一张票, 剩余%d张票", [NSThread currentThread].name, self.leftTicketCount);
            } else {
                return; // 退出循环
            }
        } // 解锁
    }
}

常用方法

  • 线程的调度优先级
    + (double)threadPriority;
    + (BOOL)setThreadPriority:(double)p;
    - (double)threadPriority;
    - (BOOL)setThreadPriority:(double)p;
    调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高
  • 线程的名字
    - (void)setName:(NSString *)n;
    - (NSString *)name;
  • 主线程
    // 获取主线程
    + (NSThread)mainThread;
    // 是否为主线程
    - (BOOL)isMainThread;
    + (BOOL)isMainThread;
    
  • 控制线程状态
    // 启动线程
    // 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
    - (void)start; 
    
    // 阻塞(暂停)线程
    // 进入阻塞状态
    + (void)sleepUntilDate:(NSDate *)date;
    // 进入阻塞状态 
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    
    // 强制停止线程
    // 进入死亡状态
    + (void)exit;
    
    注意:一旦线程停止(死亡)了,就不能再次开启任务
  • 线程间通信
    • 什么叫做线程间通信
      在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
    • 线程间通信的体现
      1个线程传递数据给另1个线程
      在1个线程中执行完特定任务后,转到另1个线程继续执行任务
    • 线程间通信常用方法
      - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
      - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
    • 实例:图片下载

五、线程安全

线程安全就是多个线程访问同一段代码,程序不会异常、不Crash。
线程安全主要依靠线程同步。

  • 多线程的安全隐患:资源共享
    多个线程访问同一块资源,很容易引发数据错乱和数据安全问题。比如多个线程访问同一个对象、同一个变量、同一个文件。

线程安全 -> 加锁

  • 互斥锁

    1. 互斥锁使用格式
      @synchronized(锁对象) { // 需要锁定的代码 }
      注意:锁定1份代码只用1把锁,用多把锁是无效的
      • 锁定的代码尽量少
      • 加锁范围内的代码,同一时间只允许一个线程进行
      • 互斥锁的参数,任何继承 NSObject * 对象都可以
      • 要保证这个锁,所有线程都能访问到,而且是所有线程访问的是同一个锁对象
    2. 互斥锁的优缺点
      优点:能有效防止因多线程抢夺资源造成的数据安全问题
      缺点:需要消耗大量的CPU资源
  • 自旋锁和互斥锁
    互斥锁在锁定的时候,其他线程会睡眠,等待条件满足,再唤醒。
    自旋锁在锁定的时候,其他线程会做死循环,一直等待这条件满足,一旦条件满足,立马去执行,少了一个唤醒过程。

  • atomic、nonatomic (原子属性内部使用的自旋锁)
    atomic:原子属性,为setter方法加锁(默认就是atomic)。线程安全,需要消耗大量的资源
    nonatomic:非原子属性,不会为setter方法加锁。非线程安全,适合内存小的移动设备

    iOS开发建议
    所有的属性都声明为nonatomic
    尽量避免多线程抢夺同一块资源
    尽量将加锁、资源抢夺的业务逻辑交给服务器端你处理,减小移动客户端的压力

  • 线程安全与UI更新

    1. UI线程 -- 主线程
      UIKit中绝大部分的类,都不是线程安全的
    2. iOS中怎么解决这个线程不安全的问题?
      苹果约定,所有程序更新UI都在主线程进行,也就不会出现多个线程同时改变一个资源。
    3. 在主线程更新UI有什么好处?
      只有在主线程更新UI,就不会出现多个线程同时改变同一个UI控件。
      主线程的优先级最高。也就意味着UI的更新优先级高。会让用户感觉很流畅。
  • 多线程安全怎么控制:

    1. 只在主线程刷新访问UI
    2. 如果要防止资源抢夺,得用synchronized进行加锁保护
    3. 如果异步操作要保证线程安全等问题,尽量使用GCD(有些函数默认就是安全的)
  • @synchronized @synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改。这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。 一般在公用变量的时候使用,如单例模式或者操作类的static变量中使用。

  • NSLock 从微观上看一个线程在CPU上走走停停的,其运行状态可以分为(经典)三种:运行状态、就绪状态、阻塞。 两个线程同时访问一个共享的变量,一个对其加1、一个对其减1 由于内存的速度远低于CPU的速度,所以在设计CPU时通常不允许直接对内存中的数据进行运行。如果要运算内存中的数据通常是用一条CPU指令把内存中的数据读入CPU、再用另外一CPU指令对CPU中的数据做运算、最后再用一条CPU指令把结果写回内存。 i++ 读数据 运算 写数据 由于CPU在执行两条指令中间可以被打断,就可能导致另一个访问同样内存的线程运行,最终导致运算结果出错 解决办法就是加线程锁 NSLock lock方法 加锁:如果这个没被锁上则直接上锁、如果锁已经被其它线程锁上了当前线程就阻塞直至这个锁被其它线程解锁 unlock方法 解锁:解开这个锁,如果有其它线程因为等这个锁而进入了阻塞状态还要把那个线程变成就绪 用lock保护共享数据的原理 先上锁 访问共享的数据(临界区) 解锁

  • NSCondition 线程的同步,有一个经典的生产者消费者问题: 比如一个线程A负责下载数据 另一个线程B负责处理数据 还有一个线程C负责显示 解决方法就是用NSCondition: lock wait signal unlock 产生者线程(产生数据的线程),生产数据之后: lock signal(发出信号:如果有其它线程在等待这个信号就把那个线程变为就绪状态) unlock 消费者线程(处理或使用数据的线程): lock wait(等待信号:如果有线程发信号当前线程就会进入阻塞状态、直到有线程发出信号) unlock NSCondition有一个问题:假唤醒(wait的时候明明没有线程发信号,wait也可能返回),通常用一个表示状态的变量就能解决这个问题

  • 不可改变的对象,通常是线程安全的 线程安全的类和函数: NSArray, NSData, NSNumber..... 非线程安全: NSBundle, NSCoder, NSArchiver, NSMutableArray

  • 列举几种进程的同步机制,并比较其优缺点。 答案:原子操作、信号量机制、自旋锁、管程、会合、分布式系统

六、多线程面试题:

  • 回到主线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 下载图片
    UIImage *image = nil;
    dispatch_async(dispatch_get_main_queue(), ^{
        // 回到主线程
    });
    // [self performSelector:@selector(settingImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES modes:nil];
    // [self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:YES];
});

  • 主线程中执行代码

    [self performSelectorOnMainThread: withObject: waitUntilDone:];
    [self performSelector: onThread:[NSThread mainThread] withObject: waitUntilDone:];
    dispatch_async(dispatch_get_main_queue(), ^{
    });
    
  • 延时执行

    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,  (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){        
    });
    [self performSelector: withObject: afterDelay:];
    [NSTimer scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:];
    
  • GCD怎么用的?
    全局队列异步操作,会新建多个子线程,操作无序执行,如果队列前有其他任务,会等待其他任务执行完毕在调用;
    全局队列同步操作,不会新建线程,顺序执行;
    主队列所有的操作都是主线程顺序执行,没有异步概念,主队列添加的同步操作永远不会执行,会死锁;
    串行队列添加的同步操作会死锁,但是会执行嵌套同步操作之前的代码;
    并行队列添加的同步操作不会死锁都在主线程执行;
    全局队列添加的同步操作不会死锁;
    同步操作 最主要的目的,阻塞并行队列任务的执行,只有当前的同步任务执行完毕之后,后边的任务才会执行,应用:用户登录。

  • 学习多线程,先了解手机里有几个重要的芯片:
    主芯片:CPU(双核)+GPU(相当于电脑里的显卡)
    内存(RAM):相当于电脑的内存条
    闪存芯片:相于于电脑的硬盘
    电源管理芯片
    蓝牙、wifi、gps芯片

  • 多线程应用:
    耗时操作(数据库中的读取,图片的处理(滤镜) )
    进程是来帮你分配内存的
    多线程开线程一般五条以内

  • 进程之间通信的途径 答案:共享存储系统消息传递系统管道:以文件系统为基础

  • 进程死锁的原因 答案:资源竞争及进程推进顺序非法

  • 死锁的4个必要条件 答案:互斥、请求保持、不可剥夺、环路

  • 死锁的处理 答案:鸵鸟策略、预防策略、避免策略、检测与解除死锁

  • 下载图片

七、进程与线程概念

  • 进程百度百科

    进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

  • 线程百度百科

    线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪阻塞运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。 线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程

  • 进程总结

    1. 进程是指在系统中正在运行的一个应用程序
    2. 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
  • 线程总结

    1. 1个进程要想执行任务,必须得有线程(每1个进程至少要有一个线程
    2. 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行

    进程(process)是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。简而言之,一个程序至少有一个进程,一个进程至少有一个线程。一个程序就是一个进程,而一个程序中的多个任务则被称为线程。 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。应用程序(application)是由一个或多个相互协作的进程组成的。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  • 线程的串行 1个线程中的任务的执行是串行的。
    如果要在1个线程中执行多个任务,那么只能一个一个按顺序的执行,也就是说,在同一时期内,1个线程只能执行一个任务。

  • 线程与进程对比?

    1. 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
    2. 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
    3. 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
    4. 系统开销: 进程切换(创建或撤消)时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销,效率要差一些。对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程
    5. 进程都有自己的独立地址空间。一个进程崩溃后,在保护模式下不会对其它进程产生影响。线程有自己的堆、局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮
    6. 线程是指进程内的一个执行单元,也是进程内的可调度实体。它可以与同进程的其他线程共享数据,但拥有自己的栈空间

    简而言之,一个程序至少有一个进程,一个进程至少有一个线程(即主线程)。一个程序就是一个进程,而一个程序中的多个任务则被称为线程。

更多实用详见 Demo