iOS学习笔记-05线程

Posted by 蔡华的博客 on September 16, 2016

5.线程

5.1 GCD

  • GCD是一套多线程库,可以有效的替换NSThread或者NSOperation。它的基本结构是dispatch_async(queue, block);参数中的queue可以通过dispatch_queue_create或者系统提供的标准dispatch queue。
    // 生成一个serial dispatch queue
    dispatch_queue_t serialQueue = dispatch_queue_create("com.demo.sai", NULL);
    // 生成一个concurrent dispatch queue
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.demo.sai", DISPATCH_QUEUE_CONCURRENT);

    // 生成的dispatch queue需要手动release,注意ARC不会释放dispatch_queue_t类型的变量
    dispatch_release(serialQueue);
    dispatch_release(concurrentQueue);

    // 使用系统已经提供的方法来create queue
    dispatch_queue_t serialQueue2 = dispatch_get_main_queue();
    dispatch_queue_t concurrentQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 因此真正在代码中经常是这样写的
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"execute in main thread");
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"execute in a concurrent thread");
    });

  • GCD中线程分为Serial Dispatch Queue和Concurrent Dispatch Queue,分别为顺序执行和并发执行。在使用dispatch_get_main_queue时获得的是主线程queue,因此它一定是顺序执行的。使用dispatch_get_global_queue获得的queue所能并行的线程数量由系统来确定,并且可以甚至优先级,然后由于XNU内核用于Global Dispatch Queue的线程不保证实时性,因此执行优先级只是大致的判断(说白了就是并不是严格执行的)。

  • dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);可以用来改变queue的优先级先度

// 变更queue的priority
dispatch_set_target_queue(concurrentQueue, serialQueue);
  • 延迟将block添加到queue中,dispatch_after
 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"dispatch_after");
    });

<font size=3 color=red>注意这个方法可以用来做指定一个时间后执行某段代码但是这个时间参数并不准确,因为这个只是在一段时间后将block加入到queue中,但是并不意味着马上执行。</font>

PS:关于用到的时间的宏的说明

NSEC:纳秒。

USEC:微妙。

SEC:秒

PER:每

所以:

NSEC_PER_SEC,每秒有多少纳秒。

USEC_PER_SEC,每秒有多少毫秒。(注意是指在纳秒的基础上)

NSEC_PER_USEC,每毫秒有多少纳秒。

所以,延时1秒可以写成如下几种:

dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);

dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC);

dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC);

最后一个“USEC_PER_SEC * NSEC_PER_USEC”,翻译过来就是“每秒的毫秒数乘以每毫秒的纳秒数”,也就是“每秒的纳秒数”

  • dispatch_group,当在并发完成一些处理后,可能需要一个节点来完成某个操作,这个操作必须等到之前的处理全部完成,那么就需要用到dispatch_group来实现。配合dispatch_group使用的有两个,dispatch_group_notify dispatch_group_wait,前者是当处理全部完成后执行,后者是等待一段时间后,用返回值判断是否所有的任务都完成了。当等待时间为DISPATCH_TIME_FOREVER时返回值一定会是0。另外,dispatch_group_wait会阻塞调用的线程。
       dispatch_queue_t groupQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();

    // 第一个处理
    dispatch_group_async(group, groupQueue, ^{
        NSLog(@"first");
    });

    // 第二个处理
    dispatch_group_async(group, groupQueue, ^{
        NSLog(@"secend");
    });

    // 完成后最终的处理
    dispatch_group_notify(group, groupQueue, ^{
        NSLog(@"done");
    });

    long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    if (result == 0) {
        NSLog(@"done");
    }
    else {
        NSLog(@"not done");
    }
    // 需要release
    dispatch_release(group);

-dispatch_barrier_async,在代码中会等待之前加入某个queue的block全部执行完,然后使得concurrent queue变为一个serial queue,只能执行用dispatch_barrier_async添加的block,等完成后,queue变回并发。典型应用场景DB read的操作使用dispatch_async,当需要write DB时使用dispatch_barrier_async,可以保证数据的一致性,也相当于给写操作加了锁(好吧这样说不够严谨)。

-dispatch_sync,同步的将block放到某个queue中,执行这个函数的线程会阻塞等待block执行完成。很容易导致死锁,目前看最好别用。

关于dispatch_sync导致死锁的问题: dispatch_sync(dispatch_get_main_queue, ...)这样写一定会死锁,dispatch_(a)sync这两个函数本质上是将block放到一个queue中,只不过一个会阻塞当前调用函数的线程,一个不会。

dispatch_async(dispatch_get_main_queue, ...)假设是main thread执行这个函数,那么线程不会等待block执行,虽然这个block是在main thread中执行的,最有可能的是在下一个loop中才会执行block。

dispatch_async(dispatch_get_global_queue(...), ...)假设是main thread执行这个函数,那么线程不会等待block执行,而是由系统分配另一个线程完成block,这个方式就是典型的多线程。

dispatch_sync(dispatch_get_main_queue, ...)假设是main thread执行这个函数,那么线程会等待block执行,但是这个block又是在main thread执行的,导致死锁。

dispatch_sync(dispatch_get_global_queue(...), ...)假设是main thread执行这个函数,那么线程会等待block执行,由系统分配另一个线程完成block,这个方式可用,但是最好不用main thread,而是自己创建一个serial queue。

  • 最后看一组代码的结果
  // 使用系统已经提供的方法来create queue
    dispatch_queue_t serialQueue2 = dispatch_get_main_queue();
    dispatch_queue_t concurrentQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 因此真正在代码中经常是这样写的
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"execute in main thread");
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"execute in a concurrent thread");
    });

    // 变更queue的priority
    //    dispatch_set_target_queue(concurrentQueue, serialQueue);

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"dispatch_after");
    });

    dispatch_queue_t groupQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();

    // 第一个处理
    dispatch_group_async(group, groupQueue, ^{
        NSLog(@"first");
    });

    // 第二个处理
    dispatch_group_async(group, groupQueue, ^{
        NSLog(@"secend");
    });

    // 完成后最终的处理
    dispatch_group_notify(group, groupQueue, ^{
        NSLog(@"done");
    });

    long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    if (result == 0) {
        NSLog(@"result done");
    }
    else {
        NSLog(@"result not done");
    }
    // 需要release
    dispatch_release(group);

    dispatch_async(concurrentQueue2, ^{
        NSLog(@"read1");
    });
    dispatch_async(concurrentQueue2, ^{
        NSLog(@"read2");
    });

    dispatch_barrier_async(concurrentQueue2, ^{
        NSLog(@"write");
    });

    dispatch_async(concurrentQueue2, ^{
        NSLog(@"read3");
    });
    dispatch_async(concurrentQueue2, ^{
        NSLog(@"read4");
    });

2016-07-09 14:46:28.500 CRHelper[3027:231293] execute in a concurrent thread
2016-07-09 14:46:28.500 CRHelper[3027:231294] first
2016-07-09 14:46:28.500 CRHelper[3027:231297] secend
2016-07-09 14:46:28.502 CRHelper[3027:231297] done
2016-07-09 14:46:28.502 CRHelper[3027:231205] result done
2016-07-09 14:46:28.507 CRHelper[3027:231297] read1
2016-07-09 14:46:28.507 CRHelper[3027:231293] read2
2016-07-09 14:46:28.507 CRHelper[3027:231390] write
2016-07-09 14:46:28.507 CRHelper[3027:231294] read3
2016-07-09 14:46:28.507 CRHelper[3027:231297] read4
2016-07-09 14:46:28.546 CRHelper[3027:231205] execute in main thread
2016-07-09 14:46:31.782 CRHelper[3027:231294] dispatch_after

“execute in main thread”这句话在代码顺序中是第一个但是却在倒数第二个输出,因为是在main thread执行的,可以很明确的看出来这个代码是在下一个loop中执行的,还有就是dispatch_after也并不是严格的按照3秒后执行的.

  • dipatch_apply的应用场合主要是循环在一个queue中调用某个block,可以用于处理集合。这个函数会想wait一样阻塞线程,因此在非主线程中用比较好。
    NSArray* arr = [NSArray arrayWithObjects:@1, @2, nil];
    for (NSInteger i = 0; i < [arr count]; i++) {
        NSLog(@"%@", [arr objectAtIndex:i]);
    }
    dispatch_async(concurrentQueue2, ^{
        dispatch_apply([arr count], concurrentQueue2, ^(size_t i) {
            NSLog(@"%@", [arr objectAtIndex:i]);
        });
    });
  • 拾遗:dipatch_semaphoredipatch_suspenddipatch_resume等。基本没用过,懒得写了。

5.2 NSThread和performSelector

  • 看一组代码和结果。可以看出performSelector方法是在main线程执行,而且performSelectorOnMainThread这个方法的输出在4之后,应该是这个方法会在下一个main thread的runloop中执行。1和4在测试中先后顺序不定,也比较好理解。
- (void)doSome:(NSString*)arg
{
    NSLog(@"%@ -> %@", [NSThread currentThread], arg);
}

- (void)startThread
{
    NSThread* th = [[NSThread alloc] initWithTarget:self selector:@selector(doSome:) object:@"1"];
    BOOL state = [th isMainThread];
    state = [th isCancelled];
    state = [th isFinished];
    state = [th isExecuting];

    [th start];

    [self performSelectorOnMainThread:@selector(doSome:) withObject:@"2" waitUntilDone:NO];
    [self performSelector:@selector(doSome:) withObject:@"3" afterDelay:3];

    [self performSelector:@selector(doSome:) withObject:@"4"];
}
2016-08-28 22:40:45.055 ARCTest[3619:236163] <NSThread: 0x7fc0c3501b70>{number = 1, name = main} -> 4
2016-08-28 22:40:45.055 ARCTest[3619:236310] <NSThread: 0x7fc0c3723f30>{number = 2, name = (null)} -> 1
2016-08-28 22:40:45.077 ARCTest[3619:236163] <NSThread: 0x7fc0c3501b70>{number = 1, name = main} -> 2
2016-08-28 22:40:48.056 ARCTest[3619:236163] <NSThread: 0x7fc0c3501b70>{number = 1, name = main} -> 3

5.3 NSOperation

5.4 锁

  • 使用:NSLock, @synchronized(self),或者使用DISPATCH_QUEUE_SERIAL这样的形式来实现锁。

  • 比较:

    NSLock控制麻烦,而且要考虑死锁。

    @synchronized(self)如果很多方法都用self做锁,那么会导致一个长时间执行的方法阻塞其它方法,所以如果用最好不要都使用一个对象来作为锁对象。

    GCD的锁不需要关注实现,而且是深层次实现的,比较高效,并且方法众多,可以时间多种锁的需要。

5.5 多线程编程的选择

  • NSThread功能完善,比较基础,能够完成简单的任务。当需要同步执行或者有一定的依赖要求时编程较为复杂。
  • performSelector缺点很多,首先执行SEL这个东西,需要判定方法是否存在;第二、在ARC下使用如果方法创建并返回一些对象,此时如果不进行一定的处理会导致内存泄露,因为ARC下默认是不做对象的autorelease的。第三、参数传递个数有限,有时需要自己定义个对象传递多个参数。
  • NSOperation,功能强大尤其是对于依赖性的处理使用起来很方便。
  • GCD,其实使用GCD基本可以完成任何任务,包括有依赖性的任务,和NSOperation搭配起来灵活使用吧。
  • GCD和NSOperation的对比:

1)GCD是纯C语言的API,而操作队列则是Object-C的对象。

2)在GCD中,任务用块(block)来表示,而块是个轻量级的数据结构;相反操作队列中的『操作』NSOperation则是个更加重量级的Object-C对象。

3)具体该使用GCD还是使用NSOperation需要看具体的情况

4)需要注意,如果直接使用NSOperation的start方法是直接在调用线程执行的,这意味着可能是在UI线程执行。

  • NSOperation和NSOperationQueue相对GCD的好处有:

1)NSOperationQueue可以方便的调用cancel方法来取消某个操作,而GCD中的任务是无法被取消的(安排好任务之后就不管了)。

2)NSOperation可以方便的指定操作间的依赖关系。

3)NSOperation可以通过KVO提供对NSOperation对象的精细控制(如监听当前操作是否被取消或是否已经完成等)

4)NSOperation可以方便的指定操作优先级。操作优先级表示此操作与队列中其它操作之间的优先关系,优先级高的操作先执行,优先级低的后执行。

5)通过自定义NSOperation的子类可以实现操作重用,

5.6 并发