多线程

作者: iChuck | 来源:发表于2018-03-19 15:07 被阅读11次

    线程与进程的区别和联系?

    • 线程是进程的最小基本单位。
    • 进程和线程都是由操作系统所产生的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。
    • 进程和线程主要差别在于他们是不同的操作系统资源管理方式。
    • 进程有独立的地址空间,一个进程崩溃后,在保护环境下不会对其它进程产生影响。
    • 线程只是一种进程中的不同执行的路径。
    • 线程有自己的堆栈和局部变量,但线程直接没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换的时,耗费大,效率差一些。
    • 但对于一些需求同事进行并且又要共享某些变量的并发操作,只能用线程,不能使用进程。

    oc 中的几种多线程

    几种多线程技术.png

    多线程使用

    // 耗时操作
    for (int i = 0; i < 500; i++) {
            //[NSThread currentThread] 判断是否是在主线程
            // number == 1 说明是主线程  != 1 就是其他线程
            //NSLog 用来做调试
            NSLog(@"%d%@",i,[NSThread currentThread]); 
        }
    
    • 在使用多线程的时候,比较耗时的操作,并且没有必要非要在主线程操作的时候,就需要在子线程中进行。[NSThread currentThread]; 可以打印当前的线程,当 number == 1; 的时候就为主线程。

    pthread

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        [self pthreadDemo];
    }
    
    - (void)pthreadDemo{
        
        /**
         pthread 是属于 POSIX 多线程开发框架
         
         参数:
         1.指向线程代号的指针
         2.线程的属性
         3.指向函数的指针
         4.传递给该函数的参数
         
         返回值
         - 如果是0,标示正确
         - 如果非0,标示错误代码
         
         void *   (*)      (void *)
         返回值   (函数指针)  (参数)
         void *  和OC中的  id 是等价的!
         
         
         */
        NSString * str = @"hello Hank";
        pthread_t threadId;
        /**
         - 在 ARC 开发中,如果涉及到和C语言中的相同的数据类型进行转换,需要使用 __bridge "桥接"
         - 在 MRC 不需要
         */
        
        int result = pthread_create(&threadId, NULL, &demo, (__bridge  void *)(str));
       
        if (result == 0) {
            NSLog(@"OK");
        }else{
            NSLog(@"error %d",result);
        }
        
        
        
    }
    
    void * demo(void * param){
        NSLog(@"%@ %@", [NSThread currentThread], param);
        
        return NULL;
    }
    

    NSThread

    • 创建线程
    // 实例方法
    - (void)threadDemo1{
        
        NSLog(@"A-------------");
        
        // 创建一个NSThread
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"Thread"];
        //启动线程
        [thread start];
        for (int i = 0; i < 10; i++) {
            NSLog(@"%d",i);
        }
        NSLog(@"B-------------");
    }
    
    // 类方法
    - (void)threadDemo2{
        //1
        NSLog(@"A---%@",[NSThread currentThread]);
        
        //detach ==> 分离
        [NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"Detach"];
        //1
        NSLog(@"B---%@",[NSThread currentThread]);
    }
    
    // NSObject 分类方法,全局使用
    - (void)threadDemo3{
        
        //1
        NSLog(@"A---%@",[NSThread currentThread]);
        
        // InBackground 就是在后台(子线程)运行!!
        // 是NSObject的分类 意味着所有的基础NSObject的都可以使用这个方法
        // 非常方便.不用NSThread对象
        [self performSelectorInBackground:@selector(demo:) withObject:@"background"];
        //1
        NSLog(@"B---%@",[NSThread currentThread]);
    }
    
    - (void)demo:(id)obj{
        for (int i = 0; i < 2; i++) {
            // !=1
            NSLog(@"C---------------%@",[NSThread currentThread]);
        }
    }
    
    
    • 线程状态
      • 创建一个 线程的时候需要开启线程(start)
      • 阻塞,当运行满足某个条件的时候,会让线程“睡一会”(sleep)
      • 当线程满足某一条件时,可以强行终止(exit),终止当前线程
    //开启主线程!!
    [[NSThread mainThread] start];
    
    // exit 杀掉主线程,但是 App 不会挂掉!
    [NSThread exit];
    
    // 休眠状态
    [NSThread sleepForTimeInterval:1.0];
    
    • 属性
      • name 属性可以知道当前调用的是哪个线程,进行定位。
      • 多线程中threadPriority可以设置优先级,可以确保紧急的任务先执行 。
        NSThread * t = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
        //在大型的商业项目中,通常希望程序在崩溃的时候,能够获取到程序准确的所以在的线程!
        t.name = @"Thread A";
        //优先级 从 0.0 -- 1.0  默认值 0.5
        /**  优先级翻转
         优先级 只是保证 CPU 调度的可能性会高!
         
         多线程目的:将耗时操作放在后台,不阻塞UI线程!
         建议:在开发的时候,不要修改优先级
         
         在多线程开发中,不要相信一次的运行结果!!
         */
        t.threadPriority = 0.1;
        [t start];
        
        NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
        //在大型的商业项目中,通常希望程序在崩溃的时候,能够获取到程序准确的所以在的线程!
        t1.name = @"Thread B";
        t1.threadPriority = 1.0;
        [t1 start];
    
      • 在多线程使用中,当资源进行竞争的时候,防止数据多次写入需要进行加锁操作。
      • 实际上原子属性内部就有一把锁!自旋锁。
      • 自旋锁 & 互斥锁
        • 共同点 : 都能保证同一时间点只有一条线程访问
        • 不同:互斥锁:如果发现有其他线程正在执行锁定的代码,线程就会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程就会被唤醒!
        • 自旋锁:如果发现其他线程正在执行锁定代码,线程就会用死循环的方式,一直等待代码执行完毕!自旋锁更适合执行非常短的代码!
      • 无论什么锁都是以性能作为代价来保证安全。
    /** 票 */
    @property(assign,nonatomic)int tickets;
    /** 锁  */
    @property(nonatomic,strong)NSObject * lockObjc;
    
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.tickets = 20;
        NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
        t1.name = @"售票员 A";
        [t1 start];
        
        NSThread * t2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
        t2.name = @"售票员 B";
        [t2 start];
        
    //    [self saleTickets];
        
    }
    
    
    - (void)saleTickets{
        while (YES) {
            //0.买票要1秒钟
            [NSThread sleepForTimeInterval:1.0];
            //参数:就是能够加锁的任意 NSOjbect 对象
            //局部变量: 每个线程单独拥有的,无法锁住!!
            //注意: 锁一定要是所有线程共享的对象!!
    //        NSObject * lockObj = [[NSObject alloc]init];
            @synchronized (self.lockObjc) {
            //互斥锁 - 保证锁内的代码,同一时间,只有一条线程能够执行!!
            //互斥锁它的范围,应该尽量小,锁范围越大,效率越低!
                //1.还有没有票
                if (self.tickets > 0) {
                    //2.卖出一张票
                    self.tickets --;
                    NSLog(@"剩下 %d 张票 --- %@",self.tickets,[NSThread currentThread]);
                }else{
                    NSLog(@"没有票了 %@",[NSThread currentThread]);
                    break;
                }
            }
        }
        
    }
    

    GCD(Grand Centeral Dispatch)

    • 纯 C 语言,提供了非常强大的函数

    • GCD 是苹果公司为多核并发运算提出的解决方案

    • GCD 会自动利用更多的 CPU 的内核(双核、四核)

    • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

    • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

    • 任务和队列

      • 任务(block):执行什么操作
      • 队列(queue):用来存放任务
    • GCD 使用步骤

      • 定制任务
      • 确定想做的事情
    • 将任务添加到队列中

      • GCD 会自动将队列中的任务取出,放到对应的线程中执行
      • 任务的取出遵循队列的 FIFO 原则,先进先出,后进后出
    • 同步和异步的区别

      • 同步:只能在当前线程中执行任务,不具备开启新线程的能力
      • 异步:可以在新的线程中执行任务,具备开启新线程的能力
    • GCD 中常用的函数

    // 同来同步的方式执行任务
    void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
    
    // 用来异步的方式执行任务
    void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    
    // 在前面的任务执行结束它才执行,而且他后面的任务等他执行完成之后才会执行
    void dispatch_barrier_sync(dispatch_queue_t queue,
            DISPATCH_NOESCAPE dispatch_block_t block);
            
    void dispatch_barrier_async(dispatch_queue_t queue,
            DISPATCH_NOESCAPE dispatch_block_t block);
    
    • GCD 的队列可以分为两大类
      • 并发队列(concurrent dispatch queue)
        • 可以让多个任务并发(同时)执行(自动开启多线程同时执行任务)
        • 并发功能只有异步(dispatch_async)函数下才有效
      • 串行队列(serial dispatch queue)
        • 让任务一个接着一个地执行(一个任务执行完毕后,在执行下一个任务)
    • 同步和异步主要影响:能不能开启新的线程
    • 并发和串行主要影响:任务的执行方式
    • 并发队列
    
    dispatch_queue_t
    dispatch_queue_create(const char *_Nullable label, // 队列名称
            dispatch_queue_attr_t _Nullable attr); // 队列类型
            
    // 创建并发队列   
    dispatch_queue_t queue = dispatch_queue_create("com.Mr.Right", DISPATCH_QUEUE_CONCURRENT);
    
    // gcd 提供了默认全局并发队列
    dispatch_queue_t
    dispatch_get_global_queue(long identifier, //队列的优先级
    unsigned long flags); // 暂时无用,默认0
    
    dispatch_queue_t queue = 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 // 后台
    
    • 串行队列
    // 创建串行队列(队列类型传 NULL 或者 DISPATCH_QUEUE_SERIAL)
    dispatch_queue_t queue = dispatch_queue_create("com.Mr.Right", DISPATCH_QUEUE_SERIAL);
    
    /**
    使用主队列(跟主线程相关联的队列)
    主队列是 GCD 自带的一种特殊的串行队列
    放在主队列中的任务,都会放到主线程中执行
    使用 dispatch_get_main_queue()获取主队列
    */
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    • 线程间的通讯(从子线程回到主线程)
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           
            // 执行耗时操作
            dispatch_async(dispatch_get_main_queue(), ^{
                // 回到主线程操作 UI
            });
        });
    
    • 延迟任务
    // NSObject 的方法 2s 后执行 demo 方法
    [self performSelector:@selector(demo) withObject:nil afterDelay:2.0];
    
    // GCD  函数, gcd时间更精确
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2s 后执行代码
    });
    
    // NSTimer 
    [NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(demo) userInfo:nil repeats:NO];
    
    • 一次性代码
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 在程序执行期间只会被执行一次(默认是线程安全的)
    });
    
    • 快速迭代
    dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
       // 执行10次代码,Index 不确定
    });
    
    • 队列组
    // 在网络请求的时候,分别异步执行2个耗时操作,其两个耗时操作执行完毕后再回到主线程执行操作
    // 可以使用队列组
    
    dispatch_group_t group = dispatch_group_create();
        
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        // 执行第一个耗时操作
    });
        
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        // 执行第二个耗时操作
    });
        
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       // 任务完成执行 UI 操作
    });
    

    NSOperaction

    • 配合 NSOperation 和 NSOperationQueue 也能实现多线程编程
    • 先将需要执行的操作封装到 NSOperation 对象中
    • 然后将 NSOperation 对象添加到 NSOperationQueue 中
    • 系统会自动将 NSOperationQueue 中的 NSOperation 取出来
    • 将取出的NSOperation封装的操作放到一条新线程中执行
    • NSOperation是一个抽象类,并不具备封装操作的能力,必须使用它的子类
    • 使用NSOperation 子类的方式有3中
      • NSInvocationOperation
      • NSBlockOperation
      • 自定义子类继承NSOperation, 实现内部相应的方法
    • NSInvocationOperation
    // 创建 NSInvocationOperation
    - (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
    // 调用 start 方法开始执行操作
    - (void)start;
    
    // 一旦执行操作,就会执行 target 的 sel 方法
    // 注意:默认情况下,调用了 start 方法后并不会开一条新线程去执行操作,而是当前线程同步执行操作;
    // 只有将 NSOperation 放到一个 NSOperationQueue 中,才会执行异步操作
    
    • NSBlockOperation
    // 创建 NSBlockOperation 对象
    + (instancetype)blockOperationWithBlock:(void (^)(void))block;
    // 添加更多操作
    - (void)addExecutionBlock:(void (^)(void))block;
    
    注意:只要 NSBlockOperation 封装的操作数大于1,就会执行异步操作
    
    • NSOperationQueue

      • NSOperation 可以调用 start 方法来执行任务,但默认是同步执行的
      • 如果将 NSOperation 添加到 NSOperationQueue(操作队列)中,系统会自动异步执行 NSOperation 中的操作
      • 添加操作到 NSOperationQueue 中
      @property NSInteger maxConcurrentOperationCount;
      
      // setter & getter
      - (void)addOperation:(NSOperation *)op;
      - (void)addOperationWithBlock:(void (^)(void))blokc;
      
      • 什么是最大并发送:同时执行任务数
      • 例如:同时开了3个线程执行3个任务,并发数就是3
      - (NSInteger)maxConcurrentOperationCount;
      - (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
      
      • 队列取消、暂停、恢复
      // 取消队列的所有操作
      - (void)cancelAllOperations;
      
      // YES 代表暂停队列,NO 表示恢复队列
      @property (getter=isSuspended) BOOL suspended;
      
      
      • 操作依赖:比如 A 执行完了在执行 B
        • 可以在不同的 Queue 的 NSOperation 之间建立依赖关系
        • 但是不能相互依赖:比如 A 依赖 B,B 依赖 A
      [operationB addDependency:operationA];
      
      • 可以监听一个操作完成completionBlock
    • 自定义 NSOperation 的步骤很简单

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

    我们项目中为什么多线程用 GCD 而不用 NSOperation?你有没有发现国外的大牛他们多线程都是用 NSOperation?你能告诉我他们这样做的理由吗?

    1. NSOperation 是用 GCD 构建封装的,是 GCD 的高级抽象。
    2. GCD 仅仅支持 FIFO 队列,而 NSOperationQueue 中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。GCD 不支持异步操作之间的依赖关系设置。如果某个操作的依赖另一个操作的数据(生产者-消费者模型),使用 NSOperationQueue 能够按照正确的顺序执行操作。GCD 则没有內建依赖关系支持。
    3. NSOperationQueue 支持 KVO,意味着我们可以观察任务执行的状态。
    4. GCD 更接近底层,而 NSOperationQUeue 则更高级抽象,所以 GCD 在追求性能的底层操作来说,是速度最快的。

    详解 GCD 死锁

    • Unix 主要支持3中通信方式
      • 基本通信:主要用来协调进程间的同步和互斥
        • 锁文件通信:通信双方通过查找特定目录下特定类型的文件(称锁文件)来完成进程间对临界资源访问时的互斥;例如进程p1访问一个临界资源,首先查看是否有一个特定类型文件,若有,则等待一段时间在查找锁文件。
        • 记录锁文件。
      • 管道通信:适应大批量的数据传递
      • IPC:适应大批量的数据传递
    • 列举几种进程的同步机制、进程的通信途径、死锁以及死锁的处理方法
      • 进程的同步机制原子操作、信号量机制、自旋锁、管程、会合、分布式系统
      • 进程之间的通信途径:共享存储系统消息传递系统管道:以文件系统为基础
      • 进程死锁的原因:资源竞争及进程推进顺序非法
      • 死锁的4个必要条件:互斥、请求保持、不可剥夺、环路
      • 死锁的处理:鸵鸟策略、预防策略、避免策略、检测与解除死锁

    iOS 多线程底层实现原理

    • 首先要搞清楚什么是线程、什么是多线程
      • Mach 是第一个以多线程方式处理任务的系统,因此多线程的底层实现机制是基于 mach 线程
      • 开发中很少使用 mach 级的线程,因为 mach 级的线程没有提供多线程的基本特征,线程之间是相互独立的
    • 开发中的实现方案
      • pthread
      • NSThread
      • GCD
      • NSOperation 和 NSOperationQueue

    多线程安全问题?何为线程同步,如何实现?分线程回调主线程的方法是什么?有什么作用?

    • 解决方案:使用锁:锁是线程编程同步工具的基础。锁可以让你很容易保护代码中一大块区域以便你可以确保代码的正确性。
      • 使用 POSIX 互斥锁
      • 使用 NSLock 类
      • 使用@synchronized 指令
    • 回到主线程的方法:dispatch_async(dispatch_get_main_queue(),^{});
    • 作用:主线程是显示 UI 界面,子线程多数进行数据处理。

    使用 atomic 一定线程安全吗?

    • 不是的。
    • atomic 原子操作,系统会为 setter 方法加锁。具体使用 @synchronized(self){//code};
    • nonatomic 不会为 setter 方法加锁
    • atomic 线程安全,需要消耗大量的系统资源来为属性加锁
    • nonatomic 非线程安全,适合内存较小的移动设备
    • 使用 atomic 并不能保证绝对的线程的安全,对于要绝对保证线程安全的操作,还需要高级的方式来处理,比如 NSSpinLock、@synchronized等

    谈谈你对多线程开发的理解(多线程的好处,多线程的作用)

    • 好处
      • 使用线程可以吧占据时间长的程序中任务放到后台处理
      • 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理进度
      • 程序的运行效率更高
      • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等。线程就比较有用了。
    • 缺点:
      • 如果有大量的线程,会影响性能,以为操作系统需要在他们之间切换
      • 更多的线程需要更多的内存空间
      • 线程的终止需要考虑其对程序的影响
    • 通常块模型数据是多个线程间共享的,需要防止线程死锁的情况发生

    相关文章

      网友评论

          本文标题:多线程

          本文链接:https://www.haomeiwen.com/subject/jakpqftx.html