zoukankan      html  css  js  c++  java
  • iOS 多线程

    什么是进程?

    • 进程是指在系统中正在运行的一个应用程序。

    • 每一个进程之间是独立的,每一个进程均运行在其专用且受保护的内存空间内。


    什么是线程?


    • 1个进程要想运行任务,必须得有线程(每1个进程至少要有1条线程)。

    • 线程是进程的基本运行单元,一个进程(程序)的全部任务都在线程中运行。

    小拓展

    - 线程的串行(就像烤串一样)
        - 1个线程中任务的运行是串行的。

    - 假设要在1个线程中运行多个任务。那么仅仅能一个一个地按顺序运行这些任务。 - 在`同一时间内`,1个线程仅仅能运行1个任务。


    什么是多线程?

    • 1个进程中能够开启多条线程。每条线程能够并行(同一时候)运行不同的任务。
    • 线程的并行(同一时候运行)
      • 比方同一时候开启3条线程分别下载3个文件(各自是文件A、文件B、文件C。

    • 多线程并发运行的原理:

      • 在同一时间里,CPU仅仅能处理1条线程。仅仅有1条线程在工作(运行)。

      • 多线程并发(同一时候)运行。事实上是CPU高速地在多条线程之间调度(切换)。假设CPU调度线程的时间足够快。就造成了多线程并发运行的假象。(例如以下图)

      CPU调用线程


    多线程优缺点:

    • 长处
      • 能适当提高程序的运行效率。

      • 能适当提高资源利用率(CPU、内存利用率)
    • 缺点
      • 开启线程须要占用一定的内存空间(默认情况下,主线程占用1M。子线程占用512KB),假设开启大量的线程,会占用大量的内存空间。减少程序的性能。
      • 线程越多。CPU在调度线程上的开销就越大。

      • 程序设计更加复杂:比方线程之间的通信、多线程的数据共享

    多线程在iOS开发中的应用

    - 主线程
        - 一个iOS程序运行后,默认会在自己的进程中开启1条线程,称为“主线程”也叫“UI线程”。
        - 作用:刷新显示UI,处理UI事件。

    - 使用注意 - 不要将耗时操作放到主线程中去处理。由于会卡住主线程,造成UI卡顿(用户体验差)。

    - 和UI相关的刷新操作`必须`放到主线程中进行处理。


    线程的状态

    • 线程的各种状态:新建-就绪-运行-堵塞-死亡
    • 常常使用的控制线程状态的方法

          [NSThread exit];//退出当前线程
          [NSThread sleepForTimeInterval:7.0];//堵塞线程
          [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:7.0]];//堵塞线程
      

      注意:线程死亡后不能复生


    线程安全:

    • 前提:多个线程同一时候訪问同一块资源会发生数据安全问题 解决方式:加相互排斥锁
    • 相关代码:@synchronized(self){}
    • 专业术语-线程同步
    • 原子和非原子属性(是否对setter方法加锁)

    IOS中多线程的实现方案

    方案 简单介绍 语言 线程生命周期 使用频率
    pthread 一套通用的多线程API
    (跨平台可移植)
    C语言 程序猿管理 差点儿不用
    NSThread 使用更加面向对象
    (简单易用。可直接操作线程对象)
    OC语言 程序猿管理 偶尔使用
    GCD 为了替代NSThread为生
    充分利用设备多核
    C语言 系统自己主动管理 常常使用
    NSOperation 基于GCD
    更加面向对象 更方便地设置线程之间的依赖 监听线程状态KVO
    OC语言 系统自己主动管理 常常使用

    pthread简单使用

    1.包括头文件(必须)

    #import <pthread.h>

    2.创建线程

    //  创建线程
    /**
         *
         * 參数一:线程对象(传地址)
         * 參数二:线程的属性(名称优先级)
         * 參数三:仅仅想函数的指针
         * 參数四:函数须要接受的字符串參数。能够不传递(注:由于我们创建的是OC的字符串,所以在传值的时候须要将其转换成C的字符串)
         */
        pthread_t thread;
        NSString *num = @"123";
        pthread_create(&thread, NULL, task, (__bridge void *)(num));
    

    3.定义參数所须要的函数指针

    
    void *task(void *num)
    {
        NSLog(@"当前线程 -- %@,传入的參数:-- %@", [NSThread currentThread], num);
    
        return NULL;
    }

    假设须要退出线程的话仅仅需调用以下代码

    pthread_exit(NULL);

    运行结果:
    pthread线程使用截图


    NSThread简单使用

    这边介绍NSThread创建线程的4种方式:

    • 第一种 (alloc nitWithTarget:selector:object:)
      • 特点:须要手动开启线程,能够拿到线程对象进行具体设置
      • 优缺点:
        • 缺点:须要手动开启线程运行任务
        • 长处:能够拿到线程对象
    //  创建线程
        /**
         * 參数一:目标对象
         * 參数二:方法选择器(线程启动后调用的方法)
         * 參数三:调用方法须要接受的參数
         */
        NSThread *thread = [[NSThread alloc] initWithTarget:self
                                                   selector:@selector(task)
                                                     object:nil];
    
        //  開始运行
        [thread start];
    • 另外一种(分离出一条子线程)
      • 特点:自己主动启动线程,无法对线程进行更具体的设置
      • 优缺点:
        • 缺点:无法拿到线程对象 进行更具体设置
        • 长处:代码简单且自己主动开启线程运行
    //  创建线程
        /**
         * 參数一:要调用的方法
         * 參数二:目标对象 self
         * 參数三:调用方法需传递的參数
         */
        [NSThread detachNewThreadSelector:@selector(task)
                                 toTarget:self
                               withObject:nil];
    • 第三种(后台线程)
      • 特点:自己主动启动线程。无法进行更具体设置
      • 优缺点:
        • 缺点:无法拿到线程对象 进行更具体设置
        • 长处:代码简单且自己主动开启线程运行
    
    /**
     *  NSThread创建一条后台线程
     */
    - (void)nsthreadTest3
    {
        //  创建线程
        /**
         * 參数一:要调用的方法
         * 參数二:调用方法需传递的參数
         */
        [self performSelectorInBackground:@selector(run:) withObject:@"后台线程"];
    
    }
    
    - (void)run:(NSString *)str
    {
        NSLog(@"当前线程:%@ -- 接收到的參数:%@", [NSThread currentThread], str);
    }
    
    • 第四种(自己定义NSThread类并重写内部的方法实现)
      • 特点:能够不暴露一些实现细节,使代码添加隐蔽性。(一般出如今第三方框架内)
      • 优缺点:
        • 缺点:繁琐,且须要手动开启线程运行
        • 长处:添加代码隐蔽性

    1.创建自己定义类继承自NSThread
    2.重写NSThread类中的main方法

    - (void)main
    {
        NSLog(@"当前线程--%@", [NSThread currentThread]);
    }

    3.创建线程对象

    /**
     *  NSThread创建一条后台线程
     */
    - (void)nsthreadTest4
    {
        //  创建线程
        SJThread *thread = [[SJThread alloc] init];
    
        //  开启运行
        [thread start];
    }

    线程间通信

    有时候我们会从server上下载图片然后再展示出来,下载的操作我们会放到子线程,而UI刷新的操作仅仅能在主线程中运行

    这样就涉及到线程间的通信。接下来我们分三种方式来简单实现一下:
    - 方式一:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 开启一条线程下载图片
        [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
    
    }
    
    
    - (void)downloadImage
    {
        //  网络图片url
        NSURL *url = [NSURL URLWithString:@"http://img3.imgtn.bdimg.com/it/u=3841157212,2135341815&fm=206&gp=0.jpg"];
        //  依据url下载图片数据到本地
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        //  把下载到本地的二进制数据转成图片
        UIImage *image = [UIImage imageWithData:imageData];
        //  回到主线程刷新UI
        //  第一种方式
        [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    
        //  另外一种方式
        //  直接调用iconView里面的setImage:方法就能够实现刷新
    //    [self.iconView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    
        //  第三种方式
        //  此方法能够方便自由在主线程和其他线程切换
    //    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
    }
    
    - (void)showImage:(UIImage *)image
    {
        self.iconView.image = image;
    }
    

    GCD简单使用

    什么是GCD

    • GCD全称是Grand Central Dispatch(牛逼的中枢调度器)
    • 纯C语言,提供了很多强大的函数

    GCD优势

    • GCD是苹果公司为多核的并行运算提出的解决方式
    • GCD会自己主动利用很多其他的CPU内核
    • GCD会自己主动关了线程生命周期(创建、调度、销毁线程)
    • GCD性能很好(接近底层)

    GCD的组合方式

    • 异步函数+并发队列:开启多条线程。并发运行任务
    • 异步函数+串行队列:开启一条线程。串行运行任务
    • 同步函数+并发队列:不开线程,串行运行任务
    • 同步函数+串行队列:不开线程,串行运行任务
    • 异步函数+主队列:不开线程,在主线程中串行运行任务
    • 同步函数+主队列:不开线程,串行运行任务(注意死锁发生

    注意同步函数和异步函数在运行顺序上面的差异

    GCD的任务和队列

    • 任务:运行什么操作
    • 队列:用来存放任务(GCD中提供了2种队列)
      • 串行队列
      • 并发队列

    GCD的使用

    • 定制任务 —— 确定须要做的操作
    • 将任务加入到队列中
    • GCD会自己主动将队列中的任务取出,存放到线程中运行
    • 任务的取出遵循队列的FIFO原则(先进先出,后进后出)

    FIFO原则图片


    GCD创建线程

    • 接下来看看同步函数和异步函数有什么差别:

    1.先来看看异步并发队列

    - (void)test
    {
        /**
         * 參数一:C语言的字符串,给队列起一个名字或标识
         * 參数二:队列类型
            DISPATCH_QUEUE_CONCURRENT   并发
            DISPATCH_QUEUE_SERIAL   串行
         */
        dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    
        /**
         *  使用函数封装任务
         * 參数一:获取队列
         * 參数二:须要运行的任务
         */
        dispatch_async(queue, ^{
            NSLog(@"在:%@线程运行了任务",[NSThread currentThread]);
        });
    
        NSLog(@"结束");
    }

    运行结果:
    异步并发队列截图

    2.再来看看同步并发队列

    - (void)test
    {
        /**
         * 參数一:C语言的字符串。给队列起一个名字或标识
         * 參数二:队列类型
            DISPATCH_QUEUE_CONCURRENT   并发
            DISPATCH_QUEUE_SERIAL   串行(串行队列能够用NULL表示)
         */
        dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    
        /**
         *  使用函数封装任务
         * 參数一:获取队列
         * 參数二:须要运行的任务
         */
        dispatch_sync(queue, ^{
            NSLog(@"在:%@线程运行了任务",[NSThread currentThread]);
        });
    
        NSLog(@"结束");
    }

    运行结果:
    同步并发队列截图


    结论:

    从上面的2个运行结果的时间能够看出
    1.异步并发队列,会开启一条子线程来处理任务。以达到主线程和子线程同一时候运行的并发效果。
    2.同步并发队列,不会开线程,必须等block块中的代码先运行完毕才会继续运行以外的任务,所以并发队列对于同步函数来说等同于“无效”

    • 再看看并发队列对异步函数和同步函数的影响:

    1.同步函数+并发队列

    dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    

    运行结果:同步函数+并发队列没有开启子线程的能力

    并发队列对同步函数的影响

    2.异步函数+并发队列

    - (void)test2
    {
        dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    }

    运行结果:异步函数+并发队列会自己主动开启3条子线程运行任务

    并发队列对异步函数的影响


    结论:

    从上面能够看出,异步函数拥有开启子线程的能力,而同步函数没有开启子线程的能力。


    • GCD中,除了并发队列外,还有串行队列,我们来看看假设把并发队列换成串行队列会有如何的变化

    1.同步函数+串行队列

    - (void)test2
    {
        dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    }

    运行结果:进一步证明同步函数没有开启子线程的能力,他的全部任务都在主线程中运行

    同步函数+串行队列

    2.异步函数+串行队列

    - (void)test2
    {
        dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
    }

    运行结果:开启了一条子线程。在子线程中依次运行任务

    异步函数+串行队列


    结论

    1.在同步函数+串行队列中。任务依然是在主线程中运行。

    2.在异步函数+串行队列中,会自己主动开启一条子线程,在子线程中依次运行任务

    3.再一次证明同步函数没有开启子线程的能力


    系统提供的4个全局并发队列

    • 在iOS中系统默认给我们提供了4个全局并发队列
    - (void)test3
    {
        //  获取全局并发队列
        //  系统内部默认提供4个全局并发队列
        /**
         * 參数一:优先级
         * 參数二:时间(传0就可以)
         */
    //优先级:DISPATCH_QUEUE_PRIORITY_HIGH 2
    //      DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    //      DISPATCH_QUEUE_PRIORITY_LOW (-2)
    //      DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN    级别最低
    
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        dispatch_async(queue, ^{
            NSLog(@"1当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"2当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"3当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"4当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"5当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"6当前线程:%@", [NSThread currentThread]);
        });
    }
    

    运行结果:在结果中我们看到GCD创建了6条线程,可是实际上GCD创建多少条线程全然由系统当前情况而定。我们是无法控制的。

    获取全局队列


    特殊的串行队列 —— 主队列(与主线程相关联的队列)

    • 主队列是GCD自带的一种特殊的串行队列
    • 放在主队列中的人物,都会放到主线程中运行
    • 使用dispatch_get_main_queue()的方式可获取主队列
      • 特点
        • 1.放在主队列中的任务。必须在主线程中运行
        • 2.主队列运行任务的时候,在调度任务的时候,会先调用主线程的状态。假设当前有任务在做。则会等待主线程运行完任务再运行自己的任务

    1.主队列+异步函数

    - (void)test4
    {
        //  获取主队列
        dispatch_queue_t queue = dispatch_get_main_queue();
    
        //  加入任务
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_async(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    }

    运行结果:任务都在主线程中运行

    主队列+异步函数

    2.同步函数+主队列

    - (void)test4
    {
        //  获取主队列
        dispatch_queue_t queue = dispatch_get_main_queue();
    
        //  加入任务
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    }

    运行结果:
    进入死锁状态,由于主队列运行任务的时候,在调度任务的时候,会先调用主线程的状态,假设当前有任务在做,则会等待主线程运行完任务再运行自己的任务

    假设要解决以上的情况,那么能够将任务加入到子线程中,这样就不会出现死锁的情况,程序也就能够正常运行了

    
    [self performSelectorInBackground:@selector(test4) withObject:nil];
    
    - (void)test4
    {
        //  获取主队列
        dispatch_queue_t queue = dispatch_get_main_queue();
    
        //  加入任务
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    
        dispatch_sync(queue, ^{
            NSLog(@"当前线程:%@", [NSThread currentThread]);
        });
    }
    

    运行结果:

    死锁解决方式


    总结

    函数类型 并发队列 手动创建的串行队列 主队列
    同步 (sync) 1.没有开启新线程
    2.串行运行任务
    1.有开启新线程
    2.串行运行任务
    死锁
    异步(async) 1.有开启新线程
    2.并发运行任务
    1.有开启新线程
    2.串行运行任务
    1.没有开启新线程
    2.串行运行任务

    注意

    使用sync函数往当前串行队列中加入任务。会卡主当前的串行队列。

    GCD线程间的通信

    • 有时候我们须要在子线程进行一些耗时操作,等耗时操作完毕后再回到主线程进行对应的UI刷新,那么就能够使用以下的方式在子线程和主线程之间进行通信
    
    - (void)test5
    {
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        dispatch_async(queue, ^{
    
            NSLog(@"在%@线程中运行任务", [NSThread currentThread]);
            //  回到主线程
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"在%@线程中运行任务", [NSThread currentThread]);
            });
        });
    }
    

    运行结果:
    线程间通信


    GCD延迟运行

    • 特点:能够选择在哪个线程中运行任务
    - (void)test6
    {
        NSLog(@"方法開始运行");
        /**
         *  GCD延迟运行方法
         *
         *  參数一: 要延迟的时间 (以秒为单位)
         *  參数二: 在哪个线程中运行
         */
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"GCD定时器");
        });
    }

    运行结果:

    GCD延迟运行


    一次性代码

    • 特点:
      • 能保证整个程序运行过程中。block内的代码块仅仅会被运行一次
      • 线程是安全的
      • 应用:简单的单例模式(单例模式实现点我)
      • 注意点:不可放在懒载入中
    
    - (void)test8
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"一次性代码运行");
        });
    }
    

    栅栏函数

    • 作用:能够控制并发队列里面任务的运行顺序
    • 注意:不能使用全局并发队列(会没有不论什么差别。文档中有凝视——仅仅对自己创建的并发队列有效)
    - (void)test7
    {
        //  创建队列
        dispatch_queue_t queue = dispatch_queue_create(0, 0);
    
        dispatch_async(queue, ^{
    
            for (int i = 0; i<5; i++) {
                NSLog(@"1");
            }
        });
        dispatch_async(queue, ^{
    
            for (int i = 0; i<5; i++) {
                NSLog(@"2");
            }
        });
    
        dispatch_barrier_async(queue, ^{
            NSLog(@"进入栅栏函数");
        });
    
        dispatch_async(queue, ^{
            for (int i = 0; i<5; i++) {
                NSLog(@"3");
            }
        });
    
        dispatch_async(queue, ^{
            for (int i = 0; i<5; i++) {
                NSLog(@"4");
            }
        });
    }

    运行结果:

    栅栏函数运行结果


    GCD迭代开发(遍历)

    • 一般我们传统的遍历方式例如以下,它的缺点就是在处理比較耗时的操作时效率较低,由于仅仅在一个线程中运行任务。
        //  传统的遍历方式
        for (int i ; i< 10; i++) {
            NSLog(@"%d -- 当前线程%@", i, [NSThread currentThread]);
        }

    运行结果:
    这里写图片描写叙述

    • 在GCD中,为我们提供了一个迭代函数,能够开启子线程高速进行遍历。这样就能够大大提高效率,并且使用很easy。

      接下来使用迭代函数来进行文件复制的操作:

    - (void)test9
    {
        //  获得文件原始路径(上层文件夹得路径)
        NSString *fromPath = @"/Users/yeshaojian/Desktop/test";
    
        //  获得文件的目标路径
        NSString *toPath = @"/Users/yeshaojian/Desktop/test2";
    
        //  得到文件路径以下的全部文件
        NSArray *subpaths =  [[NSFileManager defaultManager] subpathsAtPath:fromPath];
        NSLog(@"文件名称:%@",subpaths);
    
        //  获取数组中文件的个数
        NSInteger count = subpaths.count;
    
        //  将要迭代的操作放到迭代函数内
        dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t index){
                //  拼接须要复制的文件的全路径
                NSString *fromFullpath = [fromPath stringByAppendingPathComponent:subpaths[index]];
                //  拼接目标文件夹的全路径
                NSString *toFullpath = [toPath stringByAppendingPathComponent:subpaths[index]];
                //  运行文件剪切操作
                /*
                 * 參数一:文件在哪里的全路径
                 * 參数二:文件要被剪切到哪里的全路径
                 */
                [[NSFileManager defaultManager] moveItemAtPath:fromFullpath toPath:toFullpath error:nil];
    
               NSLog(@"拼接须要复制的文件的全路径:%@ -- 拼接目标文件夹的全路径:%@ -- 当前线程:%@",fromFullpath,toFullpath,[NSThread currentThread]);
           });
    
    }

    运行结果:
    GCD迭代截图


    队列组

    • 假如开发中有多个任务,要求在全部任务都在子线程中并发运行。且不能使用栅栏函数。当全部任务都运行完毕后打印“完毕”。

      这种需求就须要用到GCD中的队列组。

    • 应用场合:
      • 对多个任务有强制依赖性,缺一不可时使用

    1.队列组的基本使用

    - (void)test10
    {
        // 获取队列组,用来管理队列
        dispatch_group_t group = dispatch_group_create();
    
        //  获取并发队列
        dispatch_queue_t queue = dispatch_queue_create("cs", DISPATCH_QUEUE_CONCURRENT);
    
        //  加入任务
        dispatch_group_async(group, queue, ^{
            NSLog(@"cs1---%@", [NSThread currentThread]);
        });
        dispatch_group_async(group, queue, ^{
            NSLog(@"cs2---%@", [NSThread currentThread]);
        });
        dispatch_group_async(group, queue, ^{
            NSLog(@"cs3---%@", [NSThread currentThread]);
        });
    
        //  拦截通知:当队列组中全部的任务都运行完毕后,会调用以下方法的block块
        dispatch_group_notify(group, queue, ^{
            NSLog(@"完毕");
        });
    }

    运行结果:
    队列组截图


    队列组函数内部操作简要流程

    处理流程:
    1.封装任务
    2.把任务提交到队列
    3.把当前任务的运行情况纳入到队列注的监听范围
    
    注意:以下方法本身是异步的
    dispatch_group_notify(group, queue, ^{
    
        });
    

    拓展:
    在一些框架或者早期项目中,可能会见到以下2种队列组的用法,在这边顺带提及一下。但不推荐使用。由于太过繁琐。

    第一种

    - (void)test11
    {
        //  获得队列组,管理队列
        dispatch_group_t group = dispatch_group_create();
    
        //  获得并发队列
        dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
    
        //  表示開始把后面的异步任务纳入到监听范围
        //dispatch_group_enter & dispatch_group_leave
        dispatch_group_enter(group);
    
        //  使用异步函数封装任务
        dispatch_async(queue, ^{
            NSLog(@"1---%@",[NSThread currentThread]);
    
            //  通知队列组该任务已经运行完毕
            dispatch_group_leave(group);
        });
    
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"2---%@",[NSThread currentThread]);
            dispatch_group_leave(group);
        });
    
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"3---%@",[NSThread currentThread]);
            dispatch_group_leave(group);
        });
    
        //  拦截通知
        dispatch_group_notify(group, queue, ^{
            NSLog(@"--完毕---");
        });
    }
    

    另外一种

    
    - (void)test11
    {
        //  获得队列组,管理队列
        dispatch_group_t group = dispatch_group_create();
    
        //  获得并发队列
        dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
    
        //  表示開始把后面的异步任务纳入到监听范围
        //dispatch_group_enter & dispatch_group_leave
        dispatch_group_enter(group);
    
        //  使用异步函数封装任务
        dispatch_async(queue, ^{
            NSLog(@"1---%@",[NSThread currentThread]);
    
            //  通知队列组该任务已经运行完毕
            dispatch_group_leave(group);
        });
    
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"2---%@",[NSThread currentThread]);
            dispatch_group_leave(group);
        });
    
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"3---%@",[NSThread currentThread]);
            dispatch_group_leave(group);
        });
    
        //  等待DISPATCH_TIME_FOREVER 死等,一直要等到全部的任务都运行完毕之后才会继续往下运行
        //  同步运行
        dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 0.00001 * NSEC_PER_SEC);
    
        //  等待timer m的时间 无论队列中的任务有没有运行完毕都继续往下运行,假设在该时间内全部事任务都运行完毕了那么会返回一个0,否则是非0值
        long n =  dispatch_group_wait(group, timer);
        NSLog(@"%ld",n);
    
        NSLog(@"--完毕---");
    }
    

    补充:同步异步函数还有一种创建方式

    • 事实上同步函数和异步函数还有另外的创建方式,可是使用起来比較不方便。所以上面就没提及,想想还是补充一下好了

    1.异步函数(创建一个使用函数封装代码的异步函数)

    - (void)test12
    {
        /**
         *  參数一:队列
         *  參数二:要传给函数的參数
         *  參数三:函数
         */
        dispatch_async_f(dispatch_get_global_queue(0, 0), NULL, testTask);
    }
    
    void testTask(void *param)
    {
        NSLog(@"%@", [NSThread currentThread]);
    }

    2.同步函数(创建一个使用函数封装代码的同步函数)

    - (void)test12
    {
        /**
         *  參数一:队列
         *  參数二:要传给函数的參数
         *  參数三:函数
         */
        dispatch_sync_f(dispatch_get_global_queue(0, 0), NULL, testTask);
    }
    
    void testTask(void *param)
    {
        NSLog(@"%@", [NSThread currentThread]);
    }
    
    上面使用的是函数来封装要处理的代码,使用比較不方便,且block是轻量级的数据结构。更推荐使用block封装代码的形式创建同步异步函数。


    GCD一些须要注意的细节

    • 全局并发队列是默认存在的(在我们程序运行的时候就存在)
    • 全局队列依据队列的优先级分为 (高。默认,低,后台优先级)4个并发队列
    • iOS 6之前,我们通过创建的线程,是要自己手动施放的
      • 施放的方式 —— dispatch_release()
    • 使用栅栏函数,苹果官方文档明白规定栅栏函数仅仅有在和使用create函数创建的笔法队列一起使用才有效
    • 临时就想到这么多O(∩_∩)O,由于GCD已经开源。想研究的朋友能够到网上搜索一下,有哪里不正确的能够联系我,谢谢!

    NSOperation简单使用

    NSOperation作用

    • 配合使用NSOperation和NSOperationQueue也能实现多线程编程

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

    • 先将须要运行的操作封装到一个NSOperation对象中
    • 然后将NSOperation对象加入到NSOperation对象中
    • 系统会自己主动将NSOperationQueue中的NSOperation取出来
    • 将取出来的NSOperation封装的操作放到一条新线程中运行

    NSOperation的子类

    • NSOperation是个抽象类,并不具备封装操作的能力。必须使用它的子类
    • 使用NSOperation子类的方式有3种
      • NSInvocationOperation
      • NSBlockOperation
      • 自己定义子类继承NSOperation,实现内部对应的方法

    NSOperation封装操作

    • 第一种方式 —— NSInvocationOperation
    - (void)invocationTest
    {
        /**
         * 參数一:目标对象
         * 參数二:调用方法
         */
        NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    
        //  开启任务
        [op1 start];
    }
    
    - (void)download
    {
        NSLog(@"下载:%@",[NSThread currentThread]);
    }
    

    运行结果:须要和队列并用才会开启子线程运行任务
    NSInvocationOperation

    • 另外一种方式 —— Block
    - (void)blockTest
    {
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载:%@",[NSThread currentThread]);
        }];
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载:%@",[NSThread currentThread]);
        }];
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载:%@",[NSThread currentThread]);
        }];
    
        [op1 addExecutionBlock:^{
            NSLog(@"添加的下载:%@", [NSThread currentThread]);
        }];
    
        //  开启任务
        [op1 start];
        [op2 start];
        [op3 start];
    }
    
    - (void)download
    {
        NSLog(@"下载:%@",[NSThread currentThread]);
    }
    

    运行结果:假设一条线程中运行的操作大于1就会开启新线程并发运行
    NSBlockOperation

    • 方式三 —— 自己定义NSOperation

    1.先创建一个继承自NSOperation的类并重写main方法

    
    - (void)main
    {
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    }
    

    2.在须要使用的类中引用自己定义的类,并创建开启任务

    - (void)custom
    {
        SJOperation *op1 = [[SJOperation alloc] init];
    
        [op1 start];
    }

    运行结果:须要手动开启线程或者与队列并用才会开启子线程
    自己定义NSOperation


    NSOperation中的队列

    • 主队列 (获取方式:+mainQueue)
      • 全部在主队列中的任务都在主线程中运行
      • 本质上是串行队列
    - (void)invocationQueue
    {
        NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    
        NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    
        NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
        //  获取主队列
        NSOperationQueue *queue = [NSOperationQueue mainQueue];
    
        [queue addOperation:op1];
        [queue addOperation:op2];
        [queue addOperation:op3];
    
    }
    

    运行结果:全部任务都在主队列中运行,且是串行队列

    NSInvocationOperation在主队列使用情况

    • 非主队列(获取方式:alloc init)
      • 同一时候具备并发和串行功能
      • 默认下是并发的
    - (void)invocationQueue
    {
        NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    
        NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    
        NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
        //  获取非主队列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
        [queue addOperation:op1];
        [queue addOperation:op2];
        [queue addOperation:op3];
    
    }
    

    运行结果:全部任务在子线程中并发运行

    NSInvocationOperation在非主队列使用情况

    注意:addOperation:内部已经帮我们运行了开启任务方法。全部不须要另外实现。


    NSBlockOperation与队列并用的简单写法

        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载:%@",[NSThread currentThread]);
        }];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载:%@",[NSThread currentThread]);
        }];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载:%@",[NSThread currentThread]);
        }];
    

    运行结果:全部任务都在子线程中并发运行

    NSBlockOperation在队列中的简单写法


    设置最大并发数

    • 在NSOperation中,我们要想控制串行队列或者并发队列,仅仅须要设置maxConcurrentOperationCount属性就可以
      • 一般我们要使用串行队列,仅仅需设置值为1就可以
      • 假设值大于1,则为并发队列

    1.串行队列演示样例

    - (void)blockQueue
    {
        //  创建非主队列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        //  设置最大并发数为1,则队列为串行队列
        queue.maxConcurrentOperationCount = 1;
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载1:%@",[NSThread currentThread]);
        }];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载2:%@",[NSThread currentThread]);
        }];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载3:%@",[NSThread currentThread]);
        }];
    
    }

    运行结果:依照任务加入顺序运行,所以是串行队列
    NSOperationQueue串行运行

    2.并发队列演示样例

    - (void)blockQueue
    {
        //  创建非主队列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        //  设置最大并发数为6,一般子线程控制在6以内。太多线程会使设备压力过大
        queue.maxConcurrentOperationCount = 6;
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载1:%@",[NSThread currentThread]);
        }];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载2:%@",[NSThread currentThread]);
        }];
    
        [queue addOperationWithBlock:^{
            NSLog(@"下载3:%@",[NSThread currentThread]);
        }];
    
    }

    运行结果:程序并没有依照加入顺序完毕任务,所以是并发运行

    NSOperationQueue并发运行

    #### 注意:
    1. 一般子线程控制在6以内,太多线程会使设备压力过大
    2. maxConcurrentOperationCount默认值为-1(在计算机中。-1一般指最大值)
    3. 假设将maxConcurrentOperationCount设置为0。说明同一时间内运行0个任务,所以任务将不会运行。

    maxConcurrentOperationCount系统默认值


    NSOperation暂停、恢复和取消功能

    • 在NSOperation中,已经为我们提供了暂停、恢复和取消的功能,我们仅仅需调用对应的方法就可以。

    1.暂停

        //  暂停
        [queue setSuspended:YES];

    2.恢复

        //  取消
        [queue setSuspended:NO];

    3.取消

        //  取消队列中全部操作,且取消后的任务不可恢复
        [queue cancelAllOperations];

    注意:

    1.队列中的的任务是有状态的。各自是 —— 等待;运行。完毕三种状态,且暂停、恢复和取消操作并不能作用于当前正处于运行状态的任务。仅仅能作用于等待状态的任务。
    2.假设是自己定义的NSOperation,会发现暂停、恢复操作对其无效。对于这种情况,能够用以下方式解决 —— 使用取消操作

    - (void)main
    {
        //  模拟耗时操作
        for (int i = 0; i< 200; i++) {
            NSLog(@"1当前线程:%@", [NSThread currentThread]);
        }
        //  推断当前状态。假设已经取消。直接返回
        if (self.cancelled) return;
    
        //  模拟耗时操作
        for (int i = 0; i< 200; i++) {
            NSLog(@"2当前线程:%@", [NSThread currentThread]);
        }
        //  推断当前状态。假设已经取消,直接返回
        if (self.cancelled) return;
    
        //  模拟耗时操作
        for (int i = 0; i< 200; i++) {
            NSLog(@"3当前线程:%@", [NSThread currentThread]);
        }
        //  推断当前状态,假设已经取消,直接返回
        if (self.cancelled) return;
    }

    解决这个问题思路:事实上这是苹果官方文档中的建议 —— 由于。当我们调用cancelAllOperations:方法的时候。他内部的cancelled属性就会为真,每运行完一个耗时操作后都进行一次推断,假设发现已经取消,则退出运行。假设想更精确操控的话,也能够将推断操作放到耗时操作中。可是不建议这样做,由于这样性能极差。


    NSOperation中的依赖操作

    • NSOperation提供了一套很便捷好用的操作依赖方式,比起GCD,那种酸爽简直不敢相信
    - (void)blockQueue
    {
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载1:%@",[NSThread currentThread]);
        }];
    
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载2:%@",[NSThread currentThread]);
        }];
    
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载3:%@",[NSThread currentThread]);
        }];
    
        //  设置依赖关系
        //  op1依赖op2。仅仅有当op2运行完毕后,才会运行op1
        [op1 addDependency:op2];
        //  op2依赖op3,仅仅有当op3运行完毕后,才会运行op2
        [op2 addDependency:op3];
    
        //  获取主队列
        NSOperationQueue *queue = [NSOperationQueue mainQueue];
    
        [queue addOperation:op1];
        [queue addOperation:op2];
        [queue addOperation:op3];

    运行结果:先运行完op3,等op3运行完毕后才运行op2。当op2运行完毕后,才运行op1

    NSOperation操作依赖

    注意

    • NSOperation提供的操作依赖功能特别强大,能够设置不同队列的依赖
    • 可是不能循环依赖。比方op1依赖op2。op2又依赖op1。并且并不会报错,但会发生死锁,且有关任务都不运行。

    NSOperation的监听

    • 我们常常有这种须要:在某些任务运行完毕后,再运行指定的某些操作,那么NSOperation中的监听功能就派上用场了,使用很easy
        NSOperation *op = [[NSOperation alloc] init];
    
        op.completionBlock = ^{
            NSLog(@"下载完毕");
        };
    
        [op start];

    NSOperation线程间通信

    • NSOperation线程间的通信相似于GCD,所以就不多叙述了。直接上代码
    - (void)downloadPhoto
    {
        //  获取非主队列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
        //  创建下载任务
        [queue addOperationWithBlock:^{
    
            //  图片地址
            NSURL *url = [NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201512/05/20151205092106_aksZU.jpeg"];
            //  下载图片
            NSData *imageData = [NSData dataWithContentsOfURL:url];
            //  转换图片
            UIImage *image = [UIImage imageWithData:imageData];
            //  回到主线程刷新
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    
                NSLog(@"回%@线程刷新UI", [NSThread currentThread]);
    
                self.imageView.image = image;
            }];
    
        }];
    }
    

    运行结果:

    NSOperation线程间通信.gif


    GCD和NSOperation差别

      在开发中最常常使用的就是GCD和NSOperation来进行多线程开发,NSThread很多其他是在測试时辅助使用。pthread则很少看见,这里为大家简单整理一下他们之间的差别

    • GCD和NSOperation的对照

      • GCD是纯C语言的API,而操作队列(NSOperation)则是Object-C的对象
      • 在GCD中。任务用Block块来表示。而块是轻量级的数据结构,相反,操作队列(NSOperation)中的操作NSOperation是比較重量级的Object-C对象
    • 那么在开发中如何选择呢?

      • 一般假设任务之间有依赖关系或者须要监听任务运行的过程(KVO)。首选NSOperation
      • 单纯进行一些耗时操作则选用GCD。由于相比NSOperation,GCD效率更高。性能更好
    • NSOperation和NSOperationQueue长处

      • NSOperation能够方便设置操作优先级(表示操作在队列中与其他操作之间的优先关系,级别越高越先运行)
      • NSOperation能够通过KVO的方式对NSOperation对象进行监听控制(监听当前操作是处于完毕,取消还是运行状态)
      • NSOperation能够方便设置操作之间的依赖关系
      • 通过自己定义NSOperation子类能够实现操作复用
  • 相关阅读:
    Alpha冲刺
    Alpha冲刺
    Alpha冲刺
    Alpha冲刺
    抽奖系统(记一次未完成的教训)
    Alpha冲刺
    Alpha冲刺
    Alpha冲刺 (2/10)
    Alpha 冲刺 (1/10)
    软工 团队第三次作业
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7282106.html
Copyright © 2011-2022 走看看