美文网首页
关于iOS多线程

关于iOS多线程

作者: luckyYU | 来源:发表于2017-03-13 10:45 被阅读0次

    看了简书里伯恩的遗产大大的文章关于iOS多线程,你看我就够了,感觉很不错,自己做下笔记。笔记中的代码只用了OC,更多详细内容请看原文。

    在 iOS 中其实目前有4套多线程方案,他们分别是:

    Pthreads
    NSThread
    GCD
    NSOperation&NSOperationQueue

    Pthreads

    简单地说,这是一套在很多操作系统上都通用的多线程API,所以移植性很强(然并卵),当然在 iOS 中也是可以的。不过这是基于c语言的框架,使用起来这酸爽!感受一下:

    当然第一步要包含头文件

    / #import<pthread.h>

    然后创建线程,并执行任务

    - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
        
        pthread_t thread;//创建一个线程并自动执行
        
        pthread_create(&thread,NULL, start,NULL);
        
    }
    

    Pthreads需要 c语言函数,而且需要手动处理线程的各个状态的转换即管理生命周期,比如,这段代码虽然创建了一个线程,但并没有销毁。
    Pthreads只写这么多了,因为它在iOS开发几乎用不到。

    很遗憾,在我目前的 swift1.2中无法执行这套方法,原因是这个函数需要传入一个函数指针 CFunctionPointer<T>
    类型,但是目前 swift 无法将方法转换成此类型。听说 swift 2.0
    引入一个新特性 @convention(c)
    , 可以完成 Swift 方法转换成 c 语言指针的。在这里可以看到
    (PS:这部分我还没搞懂,以后搞懂会更新出来)

    NSThread

    这套方案是经过苹果封装后的,完全面向对象。你可以直接操控线程对象,非常直观和方便。但是,它的生命周期还是需要我们手动管理,所以这套方案也是偶尔用用,比如 [NSThread currentThread],它可以获取当前线程类,你就可以知道当前线程的各种属性,用于调试十分方便。下面来看看它的一些用法。

    创建并启动

    • 先创建线程类,再启动
    // 创建
      NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
    
      // 启动
      [thread start];
    
    • 创建并自动启动
     [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
    
    • 使用NSObject方法创建比自动启动
    [self performSelectorInBackground:@selector(run:) withObject:nil];
    

    很遗憾 too! 苹果认为 performSelector: 不安全,所以在 Swift 去掉了这个方法。

    Note: The performSelector: method and related selector-invoking methods are not imported in Swift because they are inherently unsafe.

    其他方法

    除了创建启动外,NSThread 还以很多方法,下面我列举一些常见的方法,当然我列举的并不完整,更多方法大家可以去类的定义里去看。

    //取消线程
    - (void)cancel;
    
    //启动线程
    - (void)start;
    
    //判断某个线程的状态的属性
    @property (readonly, getter=isExecuting) BOOL executing;
    @property (readonly, getter=isFinished) BOOL finished;
    @property (readonly, getter=isCancelled) BOOL cancelled;
    
    //设置和获取线程名字
    -(void)setName:(NSString *)n;
    -(NSString *)name;
    
    //获取当前线程信息
    + (NSThread *)currentThread;
    
    //获取主线程信息
    + (NSThread *)mainThread;
    
    //使当前线程暂停一段时间,或者暂停到某个时刻
    + (void)sleepForTimeInterval:(NSTimeInterval)time;
    + (void)sleepUntilDate:(NSDate *)date;
    

    NSThread 用起来也挺简单的,因为它就那几种方法。同时,我们也只有在一些非常简单的场景才会用 NSThread, 毕竟它还不够智能,不能优雅地处理多线程中的其他高级概念。所以接下来要说的内容才是重点。

    GCD

    Grand Central Dispatch。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时它使用的也是 c语言,不过由于使用了 Block(Swift里叫做闭包),使得使用起来更加方便,而且灵活。所以基本上大家都使用 GCD 这套方案。

    任务和队列

    GCD 中,加入了两个非常重要的概念:任务队列

    • 任务:即操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。任务有两种执行方式: 同步执行(sync) 和 异步执行(async),他们之间的区别是会不会阻塞当前线程,直到 Block 中的任务执行完毕
      如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
      如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。

    • 队列:用于存放任务。一共有两种队列, 串行队列并行队列

    串行队列:放到串行队列的任务,GCDFIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。

    并行队列:放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。

    虽然很绕,但请看下表:

    同步执行 异步执行
    串行队列 当前线程,一个一个执行 其他线程,一个一个执行
    并行队列 当前线程,一个一个执行 开很多线程,一起执行

    创建队列

    • 主队列:这是一个特殊的串行队列。它用于刷新 UI,任何需要刷新 UI 的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行。
    //OBJECTIVE-C
      dispatch_queue_t queue = ispatch_get_main_queue();
    
    //SWIFT
      let queue = ispatch_get_main_queue()
    
    • 自己创建的队列:自己可以创建串行队列, 也可以创建并行队列。 它有两个参数,其中第一个参数是标识符,用于 DEBUG 的时候标识唯一的队列,可以为空。大家可以看xcode的文档查看参数意义。第二个才是最重要的。第二个参数用来表示创建的队列是串行的还是并行的,传入 DISPATCH_QUEUE_SERIALNULL 表示创建串行队列。传入DISPATCH_QUEUE_CONCURRENT表示创建并行队列。
    //串行队列
      dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
      dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
      //并行队列
      dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    • 全局并行队列:并行任务一般都加入到这个队列。这是系统提供的一个并发队列。
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    创建任务

    • 同步任务:会阻塞当前进程(sync
     dispatch_sync(<#queue#>, ^{
          //code here
          NSLog(@"%@", [NSThread currentThread]);
      });
    
    • 异步任务:不会阻塞当前进程(async)
     dispatch_async(<#queue#>, ^{
          //code here
          NSLog(@"%@", [NSThread currentThread]);
      });
    

    NOTE:
    为了更好的理解同步和异步,和各种队列的使用,下面看两个示例:

    示例一:
    以下代码在主线程调用,结果是什么?

    NSLog("之前 - %@", NSThread.currentThread())
    dispatch_sync(dispatch_get_main_queue(), { () -> Void in
    NSLog("sync - %@", NSThread.currentThread())
    })
    NSLog("之后 - %@", NSThread.currentThread())

    答案:
    只会打印第一句:之前 - <NSThread: 0x7fb3a9e16470>{number = 1, name = main} ,然后主线程就卡死了,你可以在界面上放一个按钮,你就会发现点不了了。
    解释:
    同步任务会阻塞当前线程,然后把 Block 中的任务放到指定的队列中执行,只有等到 Block 中的任务完成后才会让当前线程继续往下运行。
    那么这里的步骤就是:打印完第一句后,dispatch_sync 立即阻塞当前的主线程,然后把 Block 中的任务放到 main_queue 中,可是 main_queue 中的任务会被取出来放到主线程中执行,但主线程这个时候已经被阻塞了,所以 Block 中的任务就不能完成,它不完成,dispatch_sync 就会一直阻塞主线程,这就是死锁现象。导致主线程一直卡死。
    
    >示例二:
    以下代码会产生什么结果?
    
    >```
    let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)
      NSLog("之前 - %@", NSThread.currentThread())
        dispatch_async(queue, { () -> Void in
            NSLog("sync之前 - %@", NSThread.currentThread())
            dispatch_sync(queue, { () -> Void in
                 NSLog("sync - %@", NSThread.currentThread())
            })
            NSLog("sync之后 - %@", NSThread.currentThread())
       })
    NSLog("之后 - %@", NSThread.currentThread())
    

    答案:
    2015-07-30 02:06:51.058 test[33329:8793087] 之前 - <NSThread: 0x7fe32050dbb0>{number = 1, name = main}
    2015-07-30 02:06:51.059 test[33329:8793356] sync之前 - <NSThread: 0x7fe32062e9f0>{number = 2, name = (null)}
    2015-07-30 02:06:51.059 test[33329:8793087] 之后 - <NSThread: 0x7fe32050dbb0>{number = 1, name = main}
    很明显 sync - %@ 和 sync之后 - %@ 没有被打印出来!这是为什么呢?我们再来分析一下:

    分析:
    我们按执行顺序一步步来哦:

    使用 DISPATCH_QUEUE_SERIAL 这个参数,创建了一个 串行队列。
    打印出 之前 - %@ 这句。
    dispatch_async 异步执行,所以当前线程不会被阻塞,于是有了两条线程,一条当前线程继续往下打印出 之后 - %@这句, 另一台执行 Block 中的内容打印 sync之前 - %@ 这句。因为这两条是并行的,所以打印的先后顺序无所谓。
    注意,高潮来了。现在的情况和上一个例子一样了。dispatch_sync同步执行,于是它所在的线程会被阻塞,一直等到 sync 里的任务执行完才会继续往下。于是 sync 就高兴的把自己 Block 中的任务放到 queue 中,可谁想 queue 是一个串行队列,一次执行一个任务,所以 sync 的 Block 必须等到前一个任务执行完毕,可万万没想到的是 queue 正在执行的任务就是被 sync 阻塞了的那个。于是又发生了死锁。所以 sync 所在的线程被卡死了。剩下的两句代码自然不会打印。

    队列组

    队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。下面是使用方法,这是一个很实用的功能。

    //1.创建队列组
    dispatch_group_t group = dispatch_group_create();
    //2.创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //3.多次使用队列组的方法执行任务, 只有异步方法
    //3.1.执行3次循环
    dispatch_group_async(group, queue, ^{
        for (NSInteger i = 0; i < 3; i++) {
            NSLog(@"group-01 - %@", [NSThread currentThread]);
        }
    });
    
    //3.2.主队列执行8次循环
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        for (NSInteger i = 0; i < 8; i++) {
            NSLog(@"group-02 - %@", [NSThread currentThread]);
        }
    });
    
    //3.3.执行5次循环
    dispatch_group_async(group, queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            NSLog(@"group-03 - %@", [NSThread currentThread]);
        }
    });
    
    //4.都完成后会自动通知
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"完成 - %@", [NSThread currentThread]);
    });
    

    打印结果

    2015-07-28 03:40:34.277 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}

    2015-07-28 03:40:34.277 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.277 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.277 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}

    2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.278 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}

    2015-07-28 03:40:34.278 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}

    2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.277 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number = 2, name = (null)}

    2015-07-28 03:40:34.278 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}

    2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.278 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number = 2, name = (null)}

    2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.278 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number = 2, name = (null)}

    2015-07-28 03:40:34.279 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.279 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    2015-07-28 03:40:34.279 test[12540:3319146] 完成 - <NSThread: 0x7f977240ba60>{number = 1, name = main}

    这些就是 GCD 的基本功能,但是它的能力远不止这些,等讲完 NSOperation 后,我们再来看看它的一些其他方面用途。而且,只要你想象力够丰富,你可以组合出更好的用法。

    dispatch_semaphore(信号量)

    信号量:就是一种可用来控制访问资源的数量的标识(比如3个线程争抢2个资源),设定了一个信号量(2个资源),在线程访问之前,加上信号量的处理(加/减资源),则可告知系统按照我们指定的信号量数量来执行多个线程。

    其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。

    信号量的3个主要函数

    //创建信号量,参数:信号量的初值,如果小于0则会返回NULL
    dispatch_semaphore_create(信号量值)
     
    //等待降低信号量
    dispatch_semaphore_wait(信号量,等待时间)
     
    //提高信号量
    dispatch_semaphore_signal(信号量)
    

    **NOTE:正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。 (具体可参考下面的代码示例)

    举个🌰:3个线程抢2个资源

    -(void)dispatchSignal{
        //crate的value表示,最多几个资源可访问
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);   
        dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
         
        //任务1
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 1");
            sleep(1);
            NSLog(@"complete task 1");
            dispatch_semaphore_signal(semaphore);       
        });<br>
        //任务2
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 2");
            sleep(1);
            NSLog(@"complete task 2");
            dispatch_semaphore_signal(semaphore);       
        });<br>
        //任务3
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 3");
            sleep(1);
            NSLog(@"complete task 3");
            dispatch_semaphore_signal(semaphore);       
        });   
    }
    

    执行结果:

    image.png
    以上信号量部分来源于iOS GCD中级篇 - dispatch_semaphore(信号量)的理解及使用

    NOTE:关于GCD,还有两个需要说的:

    • func dispatch_barrier_async(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
      这个方法重点是你传入的 queue,当你传入的 queue 是通过 DISPATCH_QUEUE_CONCURRENT 参数自己创建的 queue 时,这个方法会阻塞这个 queue(注意是阻塞 queue ,而不是阻塞当前线程),一直等到这个 queue 中排在它前面的任务都执行完成后才会开始执行自己,自己执行完毕后,再会取消阻塞,使这个 queue 中排在它后面的任务继续执行。
      如果你传入的是其他的 queue, 那么它就和 dispatch_async 一样了。
    • func dispatch_barrier_sync(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
      这个方法的使用和上一个一样,传入 自定义的并发队列(DISPATCH_QUEUE_CONCURRENT),它和上一个方法一样的阻塞 queue,不同的是 这个方法还会 阻塞当前线程。
      如果你传入的是其他的 queue, 那么它就和 dispatch_sync 一样了。

    NSOperation和NSOperationQueue

    NSOperation 是苹果公司对GCD的封装,完全面向对象,所以使用起来更好理解。 大家可以看到NSOperationNSOperationQueue分别对应GCD任务队列。操作步骤也很好理解:

    1. 将要执行的任务封装到一个NSOperation对象中。
    2. 将此任务添加到一个NSOperationQueue对象中。

    然后系统就会自动在执行任务。至于同步还是异步、串行还是并行请继续往下看:

    添加任务

    值得说明的是,NSOperation只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperationNSBlockOperation。创建一个Operation后,需要调用start方法来启动任务,它会默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其cancel方法即可。

    • NSInvocationOperation : 需要传入一个方法名。
    //1.创建NSInvocationOperation对象
      NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    
      //2.开始执行
      [operation start];
    

    NOTE:NSInvocationOperation不是类型安全的,swift已弃用。苹果如是说。这里有相关解释

    • NSBlockOperation
     //1.创建NSBlockOperation对象
      NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
          NSLog(@"%@", [NSThread currentThread]);
      }];
    
      //2.开始任务
      [operation start];
    

    之前说过这样的任务,默认会在当前线程执行。但是NSBlockOperation还有一个方法:addExecutionBlock:,通过这个方法可以给Operation添加多个执行Block。这样 Operation 中的任务会并发执行,它会在主线程和其它的多个线程执行这些任务,注意下面的打印结果:

     //1.创建NSBlockOperation对象
          NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
              NSLog(@"%@", [NSThread currentThread]);
          }];
    
          //添加多个Block
          for (NSInteger i = 0; i < 5; i++) {
              [operation addExecutionBlock:^{
                  NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
              }];
          }
    
          //2.开始任务
          [operation start];
    

    打印输出

    2015-07-28 17:50:16.585 test[17527:4095467] 第2次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}

    2015-07-28 17:50:16.585 test[17527:4095666] 第1次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}

    2015-07-28 17:50:16.585 test[17527:4095665] <NSThread: 0x7ff5c961b610>{number = 3, name = (null)}

    2015-07-28 17:50:16.585 test[17527:4095662] 第0次 - <NSThread: 0x7ff5c948d310>{number = 2, name = (null)}

    2015-07-28 17:50:16.586 test[17527:4095666] 第3次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}

    2015-07-28 17:50:16.586 test[17527:4095467] 第4次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}

    NOTE:addExecutionBlock方法必须在start()方法之前执行,否则就会报错:

    NOTE:大家可能发现了一个问题,为什么我在 Swift 里打印输出使用NSLog()而不是println()呢?原因是使用print() / println()输出的话,它会简单地使用 流(stream) 的概念,学过 C++ 的都知道。它会把需要输出的每个字符一个一个的输出到控制台。普通使用并没有问题,可是当多线程同步输出的时候问题就来了,由于很多println()同时打印,就会导致控制台上的字符混乱的堆在一起,而NSLog()就没有这个问题。到底是什么样子的呢?你可以把上面NSLog()改为println(),然后一试便知。 更多 NSLog() 与 println() 的区别看这里

    • 自定义Operation

    除了上面的两种 Operation 以外,我们还可以自定义 Operation。自定义 Operation 需要继承NSOperation类,并实现其main()方法,因为在调用start()方法的时候,内部会调用main()方法完成相关逻辑。所以如果以上的两个类无法满足你的欲望的时候,你就需要自定义了。你想要实现什么功能都可以写在里面。除此之外,你还需要实现cancel()在内的各种方法。所以这个功能提供给高级玩家,我在这里就不说了,等我需要用到时在研究它,到时候可能会再做更新。

    创建队列

    相关文章

      网友评论

          本文标题:关于iOS多线程

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