iOS多线程

作者: Ljson | 来源:发表于2015-08-07 19:41 被阅读1812次

    1.Pthreads

    真正跨平台的多线程技术,可以跨UNIX、Linux、windows平台。

    • 创建Pthreads线程

      • 如果要使用Pthreads,先导入头文件<pthread.h>
      • 创建
        pthread_create(pthread_t *restrict, const pthread_attr_t *attr,void *(*)(void *), void *restrict)
        • 第一个参数 : 保存线程ID的pthread_t,线程的代号(当做就是线程)
        • 第二个参数 : 线程的属性
        • 第三个参数 : 线程开启之后,用来执行的函数,传入函数指针,就是将来线程需要执行的函数
        • 第四个参数 : 给第三个参数的指向函数的指针 传递的参数
      • 函数返回值为int类型:0代表线程开启成功;其他代表开启失败
      • 线程开启之后,就会在子线程里面执行传入的函数
    • 其实pthread的功能相当强大,这里只是做一个最简单的了解,后期看情况是否要继续研究


    补充点:函数指针

    和block的指针类似,函数指针这么来表示:
    • 函数指针: (返回值类型)(* 变量名)(参数类型) 例如: (int)(* sum)(int,int)

      • 这个函数指针的变量名为sum,函数的返回值类型为int,两个参数都是int类型的
    • block指针:(返回值类型)(^变量名)(参数类型) 例如:(void)(^success)(int,int)

      • 这个block指针的变量名为success,函数无返回值,两个参数类型为int

    2.NSThread

    NS开头的直接就来到了Foundation框架,一个NSThread对象,就代表一条线程

    • 创建线程:

      1. **alloc + init **
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

        • 此时线程上的任务不会立即执行,而是要启动线程:

        • 会返回创建的线程,可以设置线程的一些属性。

        • 系统会强引用该线程,直到线程死亡(任务执行完毕或强制关闭)。
          [thread start];//线程一旦启动,就会执行任务

      2. 直接开启新线程[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

        • 这个方法会直接开启新线程,并执行任务

        • 无返回值,拿不到创建的线程

      3. 隐式开启线程[self performSelectorInBackground:@selector(run) withObject:nil];

        • 这个方法会直接开启新线程,并执行任务

        • 无返回值,拿不到创建的线程

    • 线程常用方法:

      • [NSThread currentThread] : 获取当前线程

      • [NSThread mainThread] : 获取主线程

      • [thread setName:name] : 设置线程的名称,方便调试

      • [thread name] : 获取线程的名称

    • 控制线程的状态

      • alloc + init : 创建线程,进入新建状态

      • start : 启动线程,进入准备就绪状态(等待CPU来调度)

      • CPU调度 : 进入运行状态

      • sleep : 进入阻塞状态

      • exit : 关闭线程,进入死亡状态

    • 线程状态示意图:

    线程状态
    • 线程间通信常用方法

      • -(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

      • -(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

      • 用上面的方法已经可以满足普通的多线程开发了。

    • 多线程数据访问问题:

    不同线程,同一时刻访问同一块内存,可能导致数据出错。
    解决办法,对可能会同时访问一块内存的代码加锁,同一时刻最多只能有一条线程访问这块内存。

    互斥锁:对一段代码加锁之后,同一时刻,最多只能有一条线程执行加锁的代码。

    • 使用方法:@synchronized(锁对象) { 要锁住的代码 }

    • 注意点:

      1. 一定要是同一把锁,否则达不到上锁的的目的。
        1. 锁住尽量少的代码,互斥锁(上锁、解锁过程)非常耗资源。
    • 当多条线程想同时访问加锁的代码:(例如让三个线程同时执行一段加锁的代码)

      1. 当三个线程都开启之后,会陆续(虽然时间基本相同,但是还是有时间差的)来执行这段代码。
      2. 第一个线程来到之后,会开锁,进入锁住的代码,进入之后,就会解锁,防止其他线程进入。
      3. 当第一个线程执行完锁住的代码之后,就会走出加锁的代码,此时就会解锁。
      4. 之后,在锁外等候的第二个线程,就会进入加锁的代码,进入之后就会上锁。依次循环往复。
    • 关键点:
      当一个线程进入加锁的代码后,就会上锁,执行完毕之后就会解锁;当一个线程访问互斥锁锁住的代码,如果这段代码处于锁住的状态,这个线程就会等待,当这段代码解锁之后,马上进入代码,加上锁,执行代码。

    互斥锁解决资源抢夺

    补充点:自旋锁(automic)

    • iOS中属性默认是automic的,这种原子属性,相当于给set方法加上了自旋锁:
      • 使用自旋锁的时候,当上锁之后,等候的线程不会休眠,会一直循环,等候解锁;
    • 互斥锁:
      • 互斥锁,当加锁的资源已经被一条线程访问的时候,等候的线程会进入休眠状态。

    3.GCD

    GCD:Grand Central Dispatch,伟大的中枢调度器。使用GCD的时候要把自己置身于一个调度者的身份,而不是纠结线程的问题。就好比十字路口的交警,你不能只关注于一条路,而是调度所有的车辆在不同的道路上畅通行驶。

    • GCD中两个非常重要的概念:

      • 任务:要执行的操作
      • 队列:存放要执行的操作的地方
    • 将任务添加到队列中

      • GCD会自动将队列中的任务取出,放到对应的线程中执行
      • 任务的取出遵循FIFO原则:先进先出,后进后出
      • 注意:
        • 任务的取出是有顺序的
        • 只要将任务添加到队列,我们不用管线程的问题,系统会自己调度
    • 同步与异步 :是否会阻塞当前线程

      • 同步 :不具备开启新线程的能力,会等到当前的任务执行完毕,函数才返回
      • 异步 :具备开启新线程的能力,不会等到当前的任务执行完毕,函数就会返回
    • 串行与并行 :决定任务的执行方式

      • 串行 : 任务会一个接着一个的执行
      • 并行 : 任务会同时一起执行
    • 关于串行和并行:

      • 任务被添加至队列之后,GCD按照FIFO(先进先出)的原则取出队列中的任务,放到线程上执行,对于不同的情况,系统会选择是否创建子线程来执行任务。
        • 串行 : 从线程上取任务是FIFO的,而且要等一个任务执行完毕之后,才去取下一个任务;下一个执行完毕,再取下一个,依次循环,直到任务都执行完毕,队列被销毁。
        • 并行 : 从线程上取任务是FIFO的,但是把一个任务放到线程上之后,马上会去取另一个任务,知道队列上的任务都被放到线程上,如果此时系统的有能力开启多的线程,这些任务都会执行起来,至于哪个任务先执行完不一定。
    • 不同函数与队列的搭配方式下,线程开辟及任务执行方式:

    不同的搭配方式
    • 简单的代码来说明:

       #pragma mark - 几种 函数 与 队列 的搭配方式
       /**
        *  异步函数 + 并行队列 ==>会创建一条或多条子线程,任务并行执行。
        */
       - (void)asyncConcurrent {
           /**
            *  创建一个队列
            *
            *  @param 第一个参数:队列的标示(方便我们调试)
            *  @param 第二个参数:创建的队列的类型(串行/并行)
            
               DISPATCH_QUEUE_CONCURRENT ==> 并行队列
               DISPATCH_QUEUE_SERIAL 《==》NULL ==> 串行队列
            
            *
            *  @return 返回创建好的队列
            */
       //  dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc", DISPATCH_QUEUE_CONCURRENT);
      
           /**
            *  获取一个全局的并行队列,这个队列已经由系统创建好
            *
            *  @param 第一个参数:队列的优先级/服务质量,传 0 代表使用默认的(具体可以查看头文件)
            *  @param 第二个参数:只作为占位,苹果目前没有用上,要求传 0
            *
            *  @return 返回一个全局队列
            */
           dispatch_queue_t queue = dispatch_get_global_queue(0, 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, ^{
               for (int i = 0; i < 9999; i ++) {
                   NSLog(@"%d-%@",i,[NSThread currentThread]);
               }
               NSLog(@"4-%@",[NSThread currentThread]);
           });
           /**
            这种情况下:
            会首先执行下面的代码,再执行任务(执行block中的代码)。
            在异步函数时:
            1.首先会执行当前的代码,而不会马上把任务(block中的代码)拿出来执行。
            2.在当前的代码执行完毕,就会来执行任务,至于需不需要开辟新的线程,还要看任务是放在什么队列当中执行:
               普通串行队列:开辟一条新的线程,所有任务在这个线程中串行执行。
               并行队列:可能会开辟多条线程(至于开多少条,有系统决定),并发执行这些任务。
               主队列:不会开辟新的线程,任务会在主队列当中串行执行。
            */
           NSLog(@"code over");
       }
       /**
        异步函数 + 串行队列 ==> 会创建一条子线程,任务会在新创建的子线程里面串行执行
        */
       - (void)asyncSerial {
           dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc",DISPATCH_QUEUE_SERIAL);
           dispatch_async(queue, ^{
               NSLog(@"1-%@",[NSThread currentThread]);
           });
           dispatch_async(queue, ^{
               NSLog(@"2-%@",[NSThread currentThread]);
           });
           dispatch_async(queue, ^{
               NSLog(@"3-%@",[NSThread currentThread]);
           });
           dispatch_async(queue, ^{
               for (int i = 0; i < 9999; i ++) {
                   NSLog(@"%d-%@",i,[NSThread currentThread]);
               }
               NSLog(@"4-%@",[NSThread currentThread]);
           });
           /**
            这种情况下,也会先执行下面的代码,再执行任务(block中的代码)
            道理同上
            */
           NSLog(@"code over");
       }
       /**
        同步函数 + 并行队列 ==> 不会创建子线程,任务会在当前线程串行执行。
        */
       - (void)syncConcurrent {
           dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
           dispatch_sync(queue, ^{
               NSLog(@"1-%@",[NSThread currentThread]);
           });
           dispatch_sync(queue, ^{
                NSLog(@"2-%@",[NSThread currentThread]);
           });
           dispatch_sync(queue, ^{
               NSLog(@"3-%@",[NSThread currentThread]);
           });
           dispatch_sync(queue, ^{
               for (int i = 0; i < 9999; i ++) {
                   NSLog(@"%d-%@",i,[NSThread currentThread]);
               }
               NSLog(@"4-%@",[NSThread currentThread]);
           });
           /**
            这里会在任务都执行完毕之后,再执行下面的代码
            在同步函数时:
            1、当前线程会被阻塞。
            2、立即执行任务
            3、在任务执行完毕之前,当前线程相当于阻塞住了(所以,在主线程中使用相当于没用)
            */
           NSLog(@"code over");
       }
       /**
        同步函数 + 串行队列 ==> 不会创建子线程,任务会在当前线程串行执行
        */
       - (void)syncSerial {
           dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc",DISPATCH_QUEUE_SERIAL);
           dispatch_sync(queue, ^{
               NSLog(@"1-%@",[NSThread currentThread]);
           });
           dispatch_sync(queue, ^{
               NSLog(@"2-%@",[NSThread currentThread]);
           });
           dispatch_sync(queue, ^{
               NSLog(@"3-%@",[NSThread currentThread]);
           });
           dispatch_sync(queue, ^{
               for (int i = 0; i < 9999; i ++) {
                   NSLog(@"%d-%@",i,[NSThread currentThread]);
               }
               NSLog(@"4-%@",[NSThread currentThread]);
           });
           /**
            会把任务依次执行完毕,才会执行下面的代码
            道理同上
            */
           NSLog(@"code over");
       }
       /**
        异步函数 + 主队列 ==> 不会创建子线程,任务会在主队列串行执行
        */
       - (void)asyncMainQueue {
           /**
            *  获取主队列。系统会自己创建主队列,主队列是串行队列
            *
            */
           dispatch_queue_t mainQueue = dispatch_get_main_queue();
           dispatch_async(mainQueue, ^{
               NSLog(@"1-%@",[NSThread currentThread]);
           });
           dispatch_async(mainQueue, ^{
               NSLog(@"2-%@",[NSThread currentThread]);
           });
           dispatch_async(mainQueue, ^{
               NSLog(@"3-%@",[NSThread currentThread]);
           });
           dispatch_async(mainQueue, ^{
               for (int i = 0; i < 9999; i ++) {
                   NSLog(@"%d-%@",i,[NSThread currentThread]);
               }
               NSLog(@"4-%@",[NSThread currentThread]);
           });
           /**
            这里会先执行下面的代码,再执行任务
            原因:
            这里是异步函数,不会阻塞当前线程。
            因为是主队列(主队列是串行队列),所以任务会在主线程上串行执行。
            */
           NSLog(@"code over");
       }
       /**
        同步函数 + 主队列 ==> 不会创建子线程,主线程会卡死
        */
       - (void)syncMainQueue {
           /**
            这里的主线程卡死。是由于调用的是同步函数,会阻塞当前的线程(当前的线程是主线程),所以把任务添加到主队列中之后,GCD会将主队列中的任务,取到主线程执行,但是此时主线程被阻塞,所以无法执行,导致主线程卡死。
            */
           dispatch_queue_t mainQueue = dispatch_get_main_queue();
           dispatch_sync(mainQueue, ^{
               NSLog(@"1-%@",[NSThread currentThread]);
           });
           dispatch_sync(mainQueue, ^{
               NSLog(@"2-%@",[NSThread currentThread]);
           });
           dispatch_sync(mainQueue, ^{
               NSLog(@"3-%@",[NSThread currentThread]);
           });
           dispatch_sync(mainQueue, ^{
           });
           NSLog(@"code over");
       }
      
    • 注意

      • 如果手动创建多条串行队列,这些队列将会并行,每个队列里面的任务会串行。
    • GCD 内存管理:

      • ARC环境,就像使用OC对象一样使用
      • MRC环境使用dispatch_retaindispatch_release 管理
    内存管理
    • GCD dispatch_barrier:

      • 俗称 栅栏,顾名思义,就是将任务分隔开。
      • dispatch_barrier_async(,)dispatch_barrier_sync(,)两个函数。
      • 会让栅栏前面的任务执行完毕之后,才执行栅栏里面的任务;栅栏里面的任务执行完毕,才执行栅栏后面的任务。
    • GCD dispatch_group

      • 将任务包装在一个组里面,组里面的任务执行完毕之后,会调用dispatch_group_notify(, ,)函数。
      • 用这种方式,可以实现任务的依赖,但是不能跨组和队列。
    • GCD的其他常用函数
      • 延时执行:dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2秒后执行这里的代码... });
      • 快速迭代:dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){ // 执行10次代码,index顺序不确定 });
        • 注意,这里的快速迭代,会开启多条线程进行遍历,效率更高。
      • 一次性代码:static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这里面默认是线程安全的) });
        • + load+ inliazed 是不一样的,理论上,只要能拿到他们的类对象就可以执行他们。
          • +load方法在一个类加载的时候会调用。如果这个类有分类,那么先调用这个类的+load方法,再调用这个类的+ load方法。
          • + inliazed 会在子类和本类初始化类对象的时候调用。子类初始化类对象的时候,调用+inliazed方法的时候,会调用父类的+ inliazed方法。

    补充:单例

    单例: 程序运行过程中,一个类始终只有一个实例对象。从创建好之后,程序死亡,才会让这个实例对象死亡。
    一次性代码,经常是用在创建单例对象的时候,保证只分配一次内存。

    • 实现单例的方案

      • 保证只分配一次内存
        • 调用alloc方法的时候,内部会调用allocWithZone方法,所以控制好allocWithZone方法的内存开辟操作就能控制alloc
        • copymutableCopy 同样要控制,直接返回调用者就好(因为copymutableCopy是对象方法,所以如果第一次内存分配控制好了,这里直接返回self
    • 具体实现代码

        //保存单例对象的静态全局变量
        static id _instance;
        + (instancetype)sharedTools {
            return [[self alloc]init];
        }
        //在调用alloc方法之后,最终会调用allocWithZone方法
        + (instancetype)allocWithZone:(struct _NSZone *)zone {
            //保证分配内存的代码只执行一次
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                _instance = [super allocWithZone:zone];
            });
            return _instance;
        }
        //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
        - (id)copyWithZone:(NSZone *)zone {
            return self;
        }
        //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
        - (id)mutableCopyWithZone:(NSZone *)zone {
            return self;
        }
        #if __has_feature(objc_arc)
        //如果是ARC环境
        #else
        //如果不是ARC环境
      
        //既然是单例对象,总不能被人给销毁了吧,一旦销毁了,分配内存的代码已经执行过了,就再也不能创建对象了。所以覆盖掉release操作
        - (oneway void)release {
        }
        //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
        - (instancetype)retain {
            return self;
        }
        //为了便于识别,这里返回 MAXFLOAT ,别的程序员看到这个数据,就能意识到这是单例了。纯属装逼……
        - (NSUInteger)retainCount {
            return MAXFLOAT;
        }
        #endif
      
    • 注意

      • 单例不能继承,由于保存单例的是静态全局变量,所以如果有子类继承的话,拿到的将是同一个对象,访问的是同一块内存。
      • 不同的单例,最好直接继承自NSObject,而不要继承自实现单例的类。
      • 为了便于创建单例,可以把上面的代码,抽成宏,方便以后使用。

    4.NSOperation

    是苹果用OC对GCD的封装,更加的面向对象。把任务创建好,添加到队列即可,系统会自己分配线程,让任务执行。

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

      1. 先将需要执行的操作封装到一个NSOperation对象中

      2. 然后将NSOperation对象添加到NSOperationQueue

      3. 系统会自动将NSOperationQueue中的NSOperation取出来

      4. 将取出的NSOperation封装的操作放到一条新线程中执行

    • 队列 NSOperationQueue

      • mainQueue :主队列
        • 获取方式:[NSOperationQueue mainQueue]
        • 任务添加到主队列之后,只会被分配到主线程来执行,所以任务一定会是串行
      • 自己创建的队列:
        • 获取方式 :alloc + init 创建
        • 任务添加到这个自己创建的队列,不会被分派到主线程来执行,所以一定会在子线程执行。至于开多少条线程来执行任务,要根据任务的数量以及队列的maxConcurrentOperationCount来决定。
        • maxConcurrentOperationCount:最大并发数:
          • 给自己创建的队列设置最大并发数,能够控制系统同时最多开启的线程数
            • 设置为1,任务会在子线程里面串行执行。(因为对已一条线程而言,任务只会在上面串行执行)
            • 设置为大于1,任务会并发在多条子线程上执行
            • 设置为0,任务不会执行
    • 任务:NSOperation

    NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

    • NSOperation的子类:

      • NSInvocationOperation,设置targetselector,任务是:要执行某个对象的某个方法

        • 调用它的start方法之后,这个任务就会被添加到当前线程执行
        • 添加到队列之后,就会在新的线程执行
      • NSBlockOperation 类方法创建:[NSBlockOperation blockOperationWithBlock:^{ }]

        • 添加到队列之后,就会执行任务
      • 自定义Operation继承自NSOperation

        • 把要做的操作放到自定义Operationmain方法里面即可
        • 即实现- (void)main方法
        • 创建自定义的Operation对象,添加到队列中,就会执行 main方法里面的任务
    • 队列的操作:

      • 取消任务:

        • 取消队列的所有任务 - (void)cancelAllOperations;
        • 也可以调用NSOperation的- (void)cancel方法取消单个操作
      • 注意:

        • 取消操作的时候,只会取消还没有执行的操作,已经在执行的操作会执行完毕
        • 所以,在自定义Operation的时候,每开始一个耗时操作,就检测一下,当前的操作是否已经取消([self isCancelled]),取消了就直接return,不要执行。
        • 操作一旦取消了,就不能恢复
      • 暂停和恢复队列

        • -(void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列
        • -(BOOL)isSuspended; //判断是否被暂停了
      • 注意:

        • 操作暂停,只会暂停还没有执行的操作,已经在执行的操作会执行完毕
        • 操作暂停了可以再恢复
    • NSOperation依赖:
      • 添加依赖:调用addDependency 方法添加依赖。
      • 一个必须等到它依赖的操作执行完毕了,才能执行这个操作。
      • 操作是可以跨队列依赖的
      • 不要相互依赖,不然操作执行不了

    相关文章

      网友评论

      • Ljson: @南栀倾寒 对的。
      • 南栀倾寒:同步和异步并不是确定开不开线程 而是 是否会发送阻塞 至于开不开线程是要看任务在哪里调度
        翻滚的炒勺2013:@南栀倾寒 同步和异步并不是确定开不开线程 而是 是否会发送阻塞 怎么理解 ?

      本文标题:iOS多线程

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