Objective-C基础之九(深刻理解多线程)

什么是线程、多线程?

在学习iOS多线程应用以前,咱们先来学习一下什么是线程?php

  • 线程是操做系统可以进行运算调度的最小单位,它被包含在进程之中,是进程的实际运做单位,一条线程指的是进程中一个单一顺序的控制流。
  • 系统中正在运行的每个应用程序都是一个进程,系统会为每一个进程分配独立的内存空间。而一个进程中的全部任务都是在线程中执行的,所以每一个进程至少得有一个线程,这也就是咱们日常所说的主线程。
  • 一个进程能够开启多条线程,多条线程并行执行不一样的任务,这就是多线程
  • 说到多线程,就不得不提CPUCPU在任意时刻只能执行一条机器指令。而线程只有获取到CPU的使用权才能执行指令。
  • 多线程并发运行,实际上是CPU(单核)快速在多条线程之间调度,因为调度线程的时间足够快,因此就形成了多线程并发执行的假象。调度线程的时间其实就是CPU分配给每一个线程能够运行的一段时间,称为时间片。同时为了提升CPU的执行效率,系统采用了时间片轮转调度算法来进行线程调度。

以上线程调度说的是单核设备,多核设备能够经过并行来同时执行多个线程ios

iOS中常见的多线程方案

在iOS中有四种多线程方案,对好比下算法

方案 简介 语言 线程生命周期 使用频率
pthread 一套通用的多线程API
适用于Unix\Linux\Windows等系统
跨平台\可移植
使用难度大
C 开发者手动管理 几乎不用
NSThread 底层是pthread
使用更加面向对象
使用方便,能够执行操做线程对象
OC 开发者手动管理 偶尔使用
GCD 替代NSThread
充分利用设备的多核
C 自动管理 经常使用
NSOperation 对GCD的封装
使用更加面向对象
增长了一些使用功能
OC 自动管理 经常使用

pthread

pthread是基于c语言的一套多线程API,正是由于底层是C语言,因此pthread可以在不一样的操做系统上使用,移植性很强。可是pthread使用起来特别麻烦,并且须要手动管理线程的声明周期,所以基本不多使用,此处也不作过多介绍。swift

NSThread

NSThread是苹果官方提供的一套操做线程的API,它是面向对象的,而且是轻量级的,使用灵活。可是和pthread同样,NSThread也须要开发者手动管理线程的生命周期。所以也不多使用,可是NSThread提供了一些很是实用的方法数组

#pragma mark - 线程建立
//获取当前线程
 +(NSThread *)currentThread; 
//建立线程后自动启动线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
//线程休眠,可设置休眠结束时间
+ (void)sleepUntilDate:(NSDate *)date;
//线程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//取消线程
- (void)cancel;
//启动线程
- (void)start;
//退出线程
+ (void)exit;
// 得到主线程
+ (NSThread *)mainThread;
//初始化方法
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
//是否正在执行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
//是否执行完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
//是否取消线程
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//线程启动
- (void)start NS_AVAILABLE(10_5, 2_0);


#pragma mark - 线程通讯
//与主线程通讯
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
  // equivalent to the first method with kCFRunLoopCommonModes
//与其余子线程通讯
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
  // equivalent to the first method with kCFRunLoopCommonModes
//隐式建立并启动线程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);
复制代码

NSThread的用法也很是简单,这里不作介绍,有兴趣的同窗能够根据系统提供的API去进行尝试。安全

NSThread在日常开发中也有使用,例如咱们常用[NSThread currentThread]来获取当前线程,使用[NSThread mainThread]来获取主线程。线程保活也是基于NSThread和RunLoop来实现的。bash

GCD(重点介绍)

GCD是苹果为解决多核设备并行运算而提出的解决方案,它会合理的利用CPU多核的特性。而且GCD可以自动管理线程的生命周期(好比建立线程、任务调度、销毁线程等等),咱们只须要告诉GCD具体要执行的任务,不须要编写任何关于线程的代码。同时GCD结合block使用更加简洁,所以在多线程开发中,GCD是首选。网络

任务和队列

在学习GCD以前,首先来学习两个比较重要的概念:任务队列多线程

任务

任务其实就是咱们须要执行的操做,在GCD中,咱们一般将须要执行的操做放在block中。执行任务有两种方式:同步异步并发

  • 同步:同步表示任务调用一旦开始,那么调用者必须等到任务返回以后,才能进行后续操做。同步任务是在当前线程中执行,不会开辟新的线程。
  • 异步:异步则表示任务一调用就会当即返回,不会阻碍调用者执行下一步操做。而任务实际是在新开辟的线程中执行。

所以,同步和异步最大的区别就是:是否具备开辟新线程的能力。

队列

在GCD中,队列主要分为两种:串行队列并发队列

  • 串行队列:表示同一时间只会有一个任务执行,执行完毕后才会执行下一个任务。串行队列只会开启一个线程执行任务。
  • 并发队列:表示同一时间有多个任务在执行。这也就意味着并发队列能够开启多个线程同时执行任务。

串行队列并发队列任务的插入方式都遵循FIFO(先进先出)原则,也就是新的任务总会插入到队列的末尾,可是串行队列中先进入队列的任务会先执行,而且等到任务执行完以后才会执行后面的任务。而并发队列则会同时执行队列中的多个任务,而且任务之间不会相互等待,任务的执行顺序和执行过程也不可预测。

GCD用法

GCD的使用步骤其实很简单,主要分为两个步骤

  • 建立队列
  • 向队列中添加任务(同步任务或异步任务)

建立队列

GCD中的队列有两种,串行队列并发队列,除此以外,GCD还提供了两种特殊的队列,一种是主队列(其实就是一个串行队列),一种是全局队列(并发队列)。

建立队列是经过dispatch_queue_create函数,它有两个参数:

  • 第一个参数是队列的惟一标识,为char *类型,自定义的队列建议使用全局惟一的标识,防止冲突
  • 第二个参数是队列的类型,DISPATCH_QUEUE_SERIAL表示建立串行队列,DISPATCH_QUEUE_CONCURRENT表示建立并发队列。

建立队列的代码以下:

//建立串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
//建立并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//获取全局并发队列(参数1:队列优先级  参数二:保留字段,通常传0)
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
复制代码

这里须要注意的是:主队列其实就是一个普通的串行队列,任何添加到主队列的任务都会在主线程中执行

同步、异步添加任务

GCD中,添加任务的方式也有两种,使用dispatch_sync建立同步任务和使用dispatch_async建立异步任务。无论是建立同步任务仍是异步任务,都须要指定队列dispatch_queue_t

  • 在串行队列中添加同步和异步任务
//建立串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"任务1");
dispatch_async(serialQueue, ^{
    sleep(3);
    NSLog(@"任务2--%@",[NSThread currentThread]);
});
NSLog(@"任务3");
dispatch_sync(serialQueue, ^{
    sleep(1);
    NSLog(@"任务4--%@",[NSThread currentThread]);
});
NSLog(@"任务5");
复制代码

最终输出的结果以下:

任务1和任务3先打印,以后才会打印任务2。执行完任务2以后,才会执行任务4,而且执行完任务4,最后才会执行任务5。由此就能够验证上文中的结论:

  1. 异步任务不会阻塞当前线程,而且异步任务是在新开辟的线程中执行。(任务1和任务3先执行,任务2后执行)
  2. 同步任务会阻塞当前线程,只有执行完同步任务以后才会执行后面的任务(执行完任务4才会执行任务5)。
  3. 串行队列中的任务遵循FIFO(先进先出)原则,先添加进去的任务先执行,而且前面的任务执行完成以后才会执行后面的任务(先执行任务2,后执行任务4)。
  • 在并发队列中添加同步和异步任务
//建立并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"任务1");
dispatch_async(concurrentQueue, ^{
    NSLog(@"开始任务2");
    sleep(3);
    NSLog(@"任务2--%@",[NSThread currentThread]);
});
NSLog(@"任务3");
dispatch_sync(concurrentQueue, ^{
    NSLog(@"开始任务4");
    sleep(3);
    NSLog(@"任务4--%@",[NSThread currentThread]);
});
NSLog(@"任务5");
复制代码

执行结果以下:

  1. 异步任务不会阻塞当前线程,因此任务1和任务3先执行,任务2后执行
  2. 并发队列中多个任务能够同时执行,所以任务2和任务4并发执行
  3. 异步任务会开辟新的线程,同步任务会在当前线程执行。所以任务2在子线程中执行,任务4在主线程中执行。
  4. 异步任务阻塞当前线程,所以任务4执行完成以后才会执行任务5

任务和队列组合执行效果

队列存在两种:串行队列并发队列,加上系统提供的主队列总共三种队列(此处因为主队列中添加的任务都会在主线程中执行,所以将主队列单独做为一种特殊的队列)。

任务又分为两种:同步任务异步任务,所以队列加任务共有6种组合,所产生的效果及对好比下:

串行队列(手动建立) 主队列 并发队列
同步任务(sync) ●不会开辟新线程
●串行执行任务
产生死锁 ●不会开辟新线程
●串行执行任务
异步任务(async) ●开辟新线程
●串行执行任务
●不会开辟新线程
●串行执行任务
●开辟新线程
●并发执行任务
  • 只有异步任务才会开启新的线程
  • 只有异步任务添加到并发队列中,才会并发执行任务

还要注意一点:当使用sync向主队列中添加同步任务时,会产生死锁。此处暂时不考虑任务嵌套。

死锁的产生

  • 第一种:在主队列中添加同步任务会产生死锁
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"任务1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
}
复制代码

执行图以下

首先,在执行viewDidLoad方法时,实际上是将viewDidLoad添加到主队列中,由于viewDidLoad如今是在队首,因此先执行viewDidLoad方法。

viewDidLoad中有3个任务,都是在主线程中执行,当执行完任务1后,经过dispatch_sync方法又向主队列中添加了任务2(实际上是整个block,这里暂且称为任务2),可是因为同步任务的特性是必现执行完且返回才能执行后面的任务,所以必需要执行完任务2才能执行后面的任务3

此时在主队列中存在两个任务,viewDidLoad任务2任务2想要执行,就必须等待viewDidLoad执行完,而viewDidLoad想要执行完,必需要执行完任务2以及任务3,可是任务3想要执行,就必须执行完任务2,所以任务2在等待viewDidLoad执行完,viewDidLoad又在等待任务2执行完,从而形成死锁。

  • 第二种:在异步任务中嵌套同步任务,而且是添加到串行队列中就会产生死锁
//建立串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{  //此处称为block1
    NSLog(@"任务1");
    dispatch_sync(serialQueue, ^{ //此处称为block2
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
});
复制代码

执行图以下:

首先,经过dispatch_async添加异步任务时会开启新的线程,因此此时block1中的任务是在子线程中执行,同时由于是在串行队列中增长的异步任务,因此block1会被添加到串行队列中去,而且在队首。

在子线程中执行block1中的方法,先执行任务1,而后执行dispatch_sync方法,此时会向串行队列中增长同步任务block2,而且须要等到block2执行完成以后才会执行任务3

此时在串行队列中存在两个任务,block1block2block2想要执行,就必须等待block1执行完,而block1想要执行完,必需要执行完block2以及任务3,可是任务3想要执行,又必须执行完block2,所以block1在等待block2执行完,block2又在等待block1执行完,从而形成死锁。

  • 第三种:同步任务中嵌套同步任务,而且添加到串行队列中
//建立串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    NSLog(@"任务1");
    dispatch_sync(queue, ^{
            NSLog(@"任务2");
    });
    NSLog(@"任务3");
});
复制代码

其实这种死锁的方式和第一种相似,同步任务仍是在主线程执行,只不过被添加到了自定义的串行队列中,所以形成死锁的缘由和第一种基本相同,这里不作介绍。

GCD的其它用法

栅栏方法:dispatch_barrier_async

栅栏方法主要是在多组操做之间增长栅栏,从而分割多组操做,使得各组操做之间顺序执行。例如:有两组操做,须要执行完第一组操做以后再执行第二组操做,此时就须要用到dispatch_barrier_async,代码以下:

//建立并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    //任务组一
    for (int i = 0; i < 5; i++) {
        dispatch_async(concurrentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"执行组一任务%d",i);
        });
    }
    //栅栏方法
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"栅栏方法");
    });
    
    //任务组二
    for (int i = 0; i < 5; i++) {
        dispatch_async(concurrentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"执行组二任务%d",i);
        });
    }
复制代码

前提是全部的任务都须要添加到同一个队列中

执行结果以下:

能够看出任务组一中的5个任务并发执行,执行完成以后会先执行栅栏函数,最后才会执行任务组二中的全部操做,具体以下图:

还有一点须要注意的是,这个函数传入的并发队列必须是经过dispatch_queue_create手动建立的,若是传入的是一个串行或者一个全局的并发队列,那么这个函数的效果等同于dispatch_async函数

队列组:dispatch_group

队列组是一个很是实用的功能,它能够在一组异步任务都执行完成以后,再执行下一步操做。例如:有多个接口,须要等到全部的接口返回结果以后再到主线程更新UI。

队列组有三种使用方法:

  • 第一种:dispatch_group_async配合dispatch_group_notify
- (void)testGroup1{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"网络任务1:%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"网络任务2:%@", [NSThread currentThread]);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"主线程更新UI:%@", [NSThread currentThread]);
    });
}
复制代码
  • 第二种:dispatch_group_enterdispatch_group_leavedispatch_group_notify搭配使用
- (void)testGroup2{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"网络任务1:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"网络任务2:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"主线程更新UI:%@", [NSThread currentThread]);
    });
}
复制代码
  • 第三种:dispatch_group_asyncdispatch_group_wait结合使用
- (void)testGroup3{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"网络任务1:%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"网络任务2:%@", [NSThread currentThread]);
    });
    //等待上面的任务所有完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"主线程更新UI:%@", [NSThread currentThread]);
    });
}
复制代码

以上三种方式的执行结果相同,以下:

信号量:dispatch_semaphore

信号量就是一种用来控制访问资源的数量的标识,当咱们设置了一个信号量,在线程访问以前加上信号量的处理,就能够告知系统按照咱们设定的信号量数量来执行多个线程。信号量实际上是用计数来实现的,若是信号量计数小于0,则会一直等待,阻塞线程。若是信号量计数为0或者大于0,则不等待且计数-1。

GCD提供了三个方法来帮助咱们使用信号量

函数 做用
dispatch_semaphore_create 建立信号量,初始值能够为0
dispatch_semaphore_signal 发送信号,信号量计数+1
dispatch_semaphore_wait 若是信号量>0,则使信号量-1,执行后续操做
若是信号量<=0,则会阻塞当前线程,直到信号量>0

示例代码以下:

- (void)testSemaphore{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //信号量初始为0
    dispatch_semaphore_t seq = dispatch_semaphore_create(0);

    NSLog(@"任务1");
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任务2");
        //信号量+1
        dispatch_semaphore_signal(seq);
    });
    //此时信号量小于0,因此一直等待,当信号量>=0时执行后续代码
    dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER);
    
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任务3");
        //信号量+1
        dispatch_semaphore_signal(seq);
    });
    //信号量-1
    dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER);
    NSLog(@"任务4");
}
复制代码

执行结果以下:

首先会执行任务1,而后往并发队列中添加异步任务,以后执行dispatch_semaphore_wait时,信号量-1,此时信号量小于0(初始为0),所以线程被阻塞,一直在此处等待。当任务2执行完成后,会调用dispatch_semaphore_signal,此时信号量+1,程序继续往下执行。

所以,信号量也能够用来实现多个异步任务顺序执行,以及多个异步任务所有执行结束以后统一执行某些操做的需求。

NSOperation

NSOperation实际上是对GCD更高一层的封装,彻底面向对象,使用起来比GCD更加简单易用,代码的可读性也更高。而且NSOperation也提供了一些GCD没有提供的更加实用的功能。好比:

  • 能够设置任务(operation)之间的依赖,用来控制多个异步任务顺序执行
  • 能够设置任务(operation)的优先级
  • 能够取消任务(operation)
  • 能够设置线程的最大并发数

NSOperation的子类

NSOperation是一个抽象类,不能直接使用。想要使用他的功能,就要使用它的子类NSInvocationOperationNSBlockOperation。也能够自定义NSOperation的子类。

NSBlockOperation

NSBlockOperation是将任务存放到block中,在合适的时机进行调用。

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务1:%@", [NSThread currentThread]);
}];
[operation1 start];
复制代码

而且NSBlockOperation还能够经过addExecutionBlock:方法添加额外操做,而且经过addExecutionBlock:添加的任务和经过blockOperationWithBlock:添加的任务能够在不一样的线程中并发执行。

- (void)testBlock{
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"主任务:%@",[NSThread currentThread]);
    }];
    [op addExecutionBlock:^{
        NSLog(@"附加任务1:%@",[NSThread currentThread]);
    }];
    [op addExecutionBlock:^{
        NSLog(@"附加任务2:%@",[NSThread currentThread]);
    }];
    [op start];
}
复制代码

执行结果以下:

经过blockOperationWithBlock:建立的任务默认会在当前线程中同步执行,可是当blockOperationWithBlock:addExecutionBlock:同时使用,而且addExecutionBlock:添加的任务足够多时,blockOperationWithBlock:建立的任务也会在子线程中执行。

经过addExecutionBlock:添加任务必定会开辟新的线程,在新线程中执行附加任务。

NSInvocationOperation

NSInvocationOperation能够指定target和selector

- (void)testOp{
    NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(opeartion) object:nil];
    [invocationOp start];
}

- (void)opeartion{
    NSLog(@"任务%@", [NSThread currentThread]);
}
复制代码

默认状况下,NSInvocationOperation在调用start方法的时候不会开启线程,会在当前线程同步执行,只有当operation被添加到NSOperationQueue中才会开启新线程异步执行操做。

NSOperation依赖设置

NSOperation能够设置任务之间的依赖,使任务按照预约的依赖顺序执行

- (void)testOp{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务一:%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务二:%@",[NSThread currentThread]);
    }];
    
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testOp) object:nil];
    //任务二依赖任务一
    [op2 addDependency:op1];
    //任务三依赖任务二
    [op3 addDependency:op2];
    [queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
}

- (void)methond3{
    NSLog(@"任务三:%@",[NSThread currentThread]);
}
复制代码

本来三个任务是并发执行,可是添加完依赖以后就变成了顺序执行,以下:

此时由于3个任务顺序执行,因此只需开辟一条线程便可。

NSOperationQueue

NSOperation中也有队列的概念,就是NSOperationQueue,一般NSOperationQueueNSOperation会结合使用,一旦NSOperation被添加到NSOperationQueue时,会自动开辟新的线程异步执行

- (void)testOperation{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务1:%@", [NSThread currentThread]);
    }];
    [operation1 start];
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务2, %@", [NSThread currentThread]);
    }];
    [queue addOperation:operation2];
}
复制代码

执行结果以下:

能够看到,任务1没有添加到NSOperationQueue中,在主线程中执行,任务2添加到NSOperationQueue中,在子线程中执行。

注意:NSOperation添加到NSOperationQueue后会自动执行start方法,无需手动调用。

  • NSOperationQueue设置任务最大并发数
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//设置最大并发数
queue.maxConcurrentOperationCount = 1;
for (int i = 0; i < 5; i++) {
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务%d,%@",i, [NSThread currentThread]);
    }];
    [queue addOperation:op];
}
复制代码

代码中将最大并发数设置为1,任务就会顺序执行,结果以下:

  • NSOperationQueue能够取消/挂起/恢复队列操做
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//挂起任务
queue.suspended = YES;
//恢复任务
queue.suspended = NO;
//取消队列中全部任务(已经开始的没法取消)
[queue cancelAllOperations];
复制代码
  • NSOperationQueue能够经过如下方式获取主队列和当前队列
//获取当前队列
[NSOperationQueue currentQueue];
//获取主队列
[NSOperationQueue mainQueue];
复制代码

NSOperation总结

NSOperation属性和方法总结

  • 操做优先级
//设置操做优先级
@property NSOperationQueuePriority queuePriority;
复制代码
  • 操做状态判断
//操做是否正在执行
@property (readonly, getter=isExecuting) BOOL executing;
//操做是否完成
@property (readonly, getter=isFinished) BOOL finished;
//操做是不是并发执行
@property (readonly, getter=isConcurrent) BOOL concurrent; 
//操做是不是异步执行
@property (readonly, getter=isAsynchronous) BOOL asynchronous;
//操做是否准备就绪
@property (readonly, getter=isReady) BOOL ready;
复制代码
  • 取消操做
//操做是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
//取消操做
- (void)cancel;
复制代码
  • 操做同步
//添加任务依赖
- (void)addDependency:(NSOperation *)op;
//移除任务依赖
- (void)removeDependency:(NSOperation *)op;
//获取当前任务的全部依赖
@property (readonly, copy) NSArray<NSOperation *> *dependencies;

//阻塞任务执行线程,直到该任务执行完成
- (void)waitUntilFinished;

//在当前任务执行完成以后调用completionBlock
@property (nullable, copy) void (^completionBlock)(void);
复制代码

NSOperationQueue属性和方法总结

  • 添加任务
//添加单挑任务
- (void)addOperation:(NSOperation *)op;
//添加多个任务
- (void)addOperations:(NSArray<NSOperation *> *)ops;
//直接向队列中添加一个NSBlockOperation类型的操做
- (void)addOperationWithBlock:(void (^)(void))block;
//在队列中的全部任务都执行完成以后会执行barrier block,相似栅栏
- (void)addBarrierBlock:(void (^)(void))barrier;
复制代码
  • 最大并发数
//设置最大并发数
@property NSInteger maxConcurrentOperationCount;
复制代码
  • 队列状态
//挂起\恢复队列操做  YES:挂起  NO:恢复
@property (getter=isSuspended) BOOL suspended;
//取消队列中全部操做
- (void)cancelAllOperations;
//阻塞当前线程,直到队列中的操做所有执行完
- (void)waitUntilAllOperationsAreFinished;
复制代码
  • 获取队列
//获取当前队列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;
//获取主队列
@property (class, readonly, strong) NSOperationQueue *mainQueue;
复制代码

多线程存在的安全隐患

在单线程条件下,任务都是串行执行,因此不存在安全问题,多线程可以极大的提升程序运行效率,可是多线程也存在隐患。当多个线程访问同一块资源时,很是容易引起数据错乱和数据安全问题。例如:如今有两条线程同时访问和修改同一个变量,以下:

线程A和线程B同时读取Integer的值,都为17,而后又同时对Integer的值+1,以后在修改Integer的值时因为线程A和线程B并发执行,所以两个线程会同时将Integer的值改成18,从而致使数据错乱。解决办法就是使用线程同步技术,就是让线程按预约的前后顺序依次执行。常见的线程同步技术是:加锁。以修改Integer的值为例,使用线程同步技术后结果以下:

线程A在访问Integer前先进行加锁操做,此时线程B没法访问Integer,而后线程A读取Integer的值,改成18,而后进行解锁,此时线程B就可以访问Integer,先进行加锁,读取Integer值为18,而后修改成19,最后再解锁。所以,使用加锁技术,就可以解决多线程的安全问题。

iOS线程同步方案

iOS中常见的线程同步技术有如下几种,咱们以一个简单的Demo来对比一下这几种线程同步技术。

示例:假设如今银行帐户上有5000元,使用多线程,分屡次在银行帐户上存钱取钱,保证最后银行存款正确。

若是咱们使用多线程可是不使用线程同步技术的话,代码以下:

- (void)moneyTest{
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:2];
            totalMoney+=100;
            NSLog(@"存100,帐户余额:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:2];
            totalMoney-=200;
            NSLog(@"取200,帐户余额:%d",totalMoney);
       
        }
    });
}
复制代码

若是按正常的流程,通过5次存钱和5次取钱,帐户余额应该最终变为4500元,可是最终执行的结果缺大不相同,以下:

整个过程当中帐户余额的计算都有问题,同时,通过10次存取以后,帐户余额还剩4700元,这就是多线程使用带来的弊端。

如今,咱们就用如下技术来解决存钱取钱的问题

如下各类锁的实现能够在GNUstep中找到相应实现,虽然GNUstep不是官方源码,可是也具备必定的参考价值。

OSSpinLock

OSSpinLock叫作“自旋锁”,顾名思义,线程在等待解锁的过程当中会处于忙等状态,而且一直会占用CPU资源。

OSSpinLock如今已经再也不安全,由于它会出现优先级反转的问题,即优先级低的线程首先得到锁,进行加锁操做,CPU会给它分配资源来执行后续任务,若是此时有高优先级的线程进入,那么CPU会优先给高优先级的线程分配资源,此时低优先级线程得不到资源没法释放锁,而高优先级线程因为在等待低优先级线程解锁,并且是处于忙等状态,一直占用着CPU资源。所以就致使优先级反转的问题。

OSSpinLock具体Api以下:

#import <libkern/OSAtomic.h>

//初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁(若是须要等待,就不加锁,直接返回false,若是不须要等待就加锁,而且返回true)
bool result = OSSpinLockTry(&lock);
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnlock(&lock)
复制代码

回到上述Demo,对存钱取钱操做进行加锁,以下:

- (void)moneyTest{
    //初始化锁
    __block OSSpinLock lock = OS_SPINLOCK_INIT;
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            OSSpinLockLock(&lock);  //加锁
            [NSThread sleepForTimeInterval:.1];
            totalMoney+=100;
            OSSpinLockUnlock(&lock);//解锁
            NSLog(@"存100,帐户余额:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            OSSpinLockLock(&lock);  //加锁
            [NSThread sleepForTimeInterval:.1];
            totalMoney-=200;
            OSSpinLockUnlock(&lock);//解锁
            NSLog(@"取200,帐户余额:%d",totalMoney);
       
        }
    });
}
复制代码

运行结果以下:

能够看到,整个过程按照顺序依次执行,先进行存钱,后进行取钱,最终帐户余额为4500元,解决了数据错乱的问题。

os_unfair_lock

os_unfair_lock被用来取代OSSpinLock,而且从iOS 10开始支持os_unfair_lock。等待锁的线程会处于休眠状态(不一样于OSSpinLock的忙等状态),不会占用CPU资源。所以,使用os_unfair_lock不会致使优先级反转的问题。

os_unfair_lockApi以下:

#import <os/lock.h>

//初始化锁
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
bool result = os_unfair_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

复制代码

使用方式同OSSpinLock

pthread_mutex

pthread_mutex称为互斥锁,即当一个线程得到某一共享资源的使用权以后,会将该资源进行加锁,若是此时有其它线程想要获取该资源的锁,那么它将会被阻塞进入睡眠状态,直到该资源被解锁后才会唤醒。若是有多个线程尝试获取该资源的锁,那么它们都会进入睡眠状态,一旦该资源被解锁,这些线程就都会被唤醒,可是真正能得到资源使用权的是第一个被唤醒的线程。

使用互斥锁的线程在等待锁的过程当中会处于休眠状态,不会占用CPU资源

pthread_mutex的Api以下:

#import <pthread.h>

/*
 * Mutex type attributes
 */
#define PTHREAD_MUTEX_NORMAL 0 //默认类型,普通锁
#define PTHREAD_MUTEX_ERRORCHECK 1 //检错锁
#define PTHREAD_MUTEX_RECURSIVE 2 //递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
    
    
//初始化锁属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
//初始化锁,第二个参数能够传NULL,就是使用默认的属性
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
    
//尝试加锁
pthread_mutex_trylock(&mutex);
//加锁
pthread_mutex_lock(&mutex);
//解锁
pthread_mutex_unlock(&mutex);
    
//销毁
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
复制代码

普通锁

使用pthread_mutex对存钱取钱进行加锁,以下:

- (void)moneyTest{
    //初始化锁
    __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            pthread_mutex_lock(&mutex);  //加锁
            [NSThread sleepForTimeInterval:.1];
            totalMoney+=100;
            pthread_mutex_unlock(&mutex);//解锁
            NSLog(@"存100,帐户余额:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            pthread_mutex_lock(&mutex);  //加锁
            [NSThread sleepForTimeInterval:.1];
            totalMoney-=200;
            pthread_mutex_unlock(&mutex);//解锁
            NSLog(@"取200,帐户余额:%d",totalMoney);
       
        }
    });
    //在不使用锁时须要调用此方法对锁进行销毁
    //pthread_mutexattr_destroy(&mutex);
}

复制代码

递归锁

在初始化锁时,咱们能够指定锁的类型为PTHREAD_MUTEX_RECURSIVE,此时咱们就建立了一个递归锁递归锁是指同一个线程能够屡次得到某一个共享资源的锁(屡次进行加锁操做),别的线程想要获取该资源锁,就必须等待该线程释放全部次数的锁。下面咱们就建立一个递归函数的Demo来了解一下递归锁的使用:

#import "XLMutexRecursiveTest.h"
#import <pthread.h>

@interface XLMutexRecursiveTest ()

@property(nonatomic, assign)pthread_mutex_t mutex;

@end

@implementation XLMutexRecursiveTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        //递归锁:容许同一个线程对同一把锁进行重复加锁
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        //pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        //初始化锁
        pthread_mutex_init(&_mutex, &attr);
        //销毁属性
        pthread_mutexattr_destroy(&attr);
    }
    return self;
}

- (void)recursiveTask{
    
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"recursiveTask");
    static int count = 0;
    if (count < 5) {
        count++;
        [self recursiveTask];
    }
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc{
    pthread_mutex_destroy(&_mutex);
}

@end
复制代码

首先建立普通锁PTHREAD_MUTEX_NORMAL,而后实例化XLMutexRecursiveTest实例进行调用

XLMutexRecursiveTest *recursiveTest = [[XLMutexRecursiveTest alloc] init];
[recursiveTest recursiveTask];
复制代码

执行以后发现程序会一直卡死在第一次打印NSLog的地方。这是由于当首次执行recursiveTask方法时会对_mutex进行加锁,而后执行NSLog,当count < 5时,会再次执行recursiveTask方法,此时会发现_mutex已经被加锁了,所以第二次执行的recursiveTask方法会一直在等待解锁,而第一次执行的recursiveTask方法想要解锁,就必需要等第二次的任务执行完成,所以就形成了死锁

下面咱们将锁改为递归锁,从新执行,会发现全部的任务都正常打印了,以下

注意:在不使用pthread_mutex时要调用pthread_mutexattr_destroypthread_mutex_destroy对锁及其属性进行销毁。

条件变量

条件变量是在多线程中用来实现“等待->唤醒”逻辑的经常使用方式,相似于GCD中的信号量。条件变量是利用一个全局共享变量来进行线程同步。它主要分为三步:

  • 线程一等待条件变量的条件成立而被挂起
  • 线程二使条件变量成立
  • 唤醒线程一

而且为了防止资源竞争,一般将条件变量和互斥锁结合使用。由于条件变量一般是多个线程或者进程的共享变量,因此就极有可能产生资源竞争,因此在使用条件变量以前须要对其加上互斥锁。pthread_mutex关于条件变量使用的Api以下:

//初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
//初始化条件
pthread_cond_t condt;
pthread_cond_init(&condt, NULL);
//等待条件(此时会进入休眠状态,同时对mutex进行解锁,被再次唤醒后,会对mutex再次加锁)
pthread_cond_wait(&condt, &mutex);
//激活一个等待该条件的线程
pthread_cond_signal(&condt);
//激活全部等待该条件的线程
pthread_cond_broadcast(&condt);
        
//销毁
pthread_cond_destroy(&condt);
pthread_mutex_destroy(&mutex);
复制代码

条件变量比较典型的应用即是生产者-消费者模式,下面就模拟生产者-消费者来建立一个简单的Demo了解一下条件变量互斥锁的使用,代码以下:

#import "XLMutexConditionLockTest.h"
#import <pthread.h>

@interface XLMutexConditionLockTest ()
//杯子余量
@property(nonatomic, strong)NSMutableArray *cupsRemain;
@property(nonatomic, assign)pthread_mutex_t mutex;
@property(nonatomic, assign)pthread_cond_t condt;

@end

@implementation XLMutexConditionLockTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        //初始化锁
        pthread_mutex_init(&_mutex, NULL);
        //初始化条件
        pthread_cond_init(&_condt, NULL);
    }
    return self;
}

- (void)testSaleAndProduce{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self _saleCup];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self _produceCup];
    });
}

//出首杯子
- (void)_saleCup{
    pthread_mutex_lock(&_mutex);
    if (self.cupsRemain.count == 0) {
        //若是杯子余量为0,则等待生产杯子
        NSLog(@"当前无可用杯子库存");
        pthread_cond_wait(&_condt, &_mutex);
    }
    //此时有可出售的杯子
    [self.cupsRemain removeLastObject];
    NSLog(@"售出一个杯子");
    pthread_mutex_unlock(&_mutex);
}

//生产杯子
- (void)_produceCup{
    pthread_mutex_lock(&_mutex);
    
    //睡眠两秒,模拟生产过程
    sleep(2);
    [self.cupsRemain addObject:@"yellow cup"];
    NSLog(@"生产了一个黄色杯子");
    //通知条件变量成立
    pthread_cond_signal(&_condt);
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc{
    //销毁
    pthread_cond_destroy(&_condt);
    pthread_mutex_destroy(&_mutex);
}

@end
复制代码

执行结果以下:

能够发现,虽然_produceCup方法睡眠2s执行,可是_saleCup方法仍是等待_produceCup执行完成以后再执行。由此能够总结出整个条件变量的流程以下:

  • 首先,前后在不一样线程调用saleCup和produceCup的方法。
  • saleCup所在线程先获取mutex,对其进行加锁,而后判断是否有库存,此时库存为0,因此调用pthread_cond_wait方法,pthread_cond_wait方法主要分为三步:
    • 将当前线程放入等待条件知足的线程队列中。
    • mutex进行解锁
    • 挂起(阻塞)当前线程,等待被唤醒。(此时pthread_cond_wait函数并未返回)
  • 调用produceCup方法,因为mutex此时已经被解锁,因此produceCup所在线程能够对其进行加锁,而后向数组中增长一个元素,以后调用pthread_cond_signal方法激活saleCup所在线程,最后调用pthread_mutex_unlock方法对mutex解锁。
  • 接收到pthread_cond_signal信号后,saleCup所在线程被激活,同时pthread_cond_wait函数返回,在pthread_cond_wait函数返回时会自动对mutex进行再次加锁。
  • 移除数组中的最后一个元素,最后对mutex进行解锁。

dispatch_semaphore

dispatch_semaphore叫作信号量,前面讲GCD的时候也介绍过。dispatch_semaphore是经过设置一个全局的信号量,来控制线程并发访问的最大数量。假设信号量初始值为1,那么表明同时只容许1条线程访问资源,以此来保证线程同步。使用方式以下:

- (void)testDispatch{
    //设置信号初始值
    int semaphoreValue = 1;
    //初始化信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(semaphoreValue);
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            //若是此时信号量<=0,那么dispatch_semaphore_wait会让线程处于休眠等待状态,直到信号量>0
            //若是信号量>0,则执行dispatch_semaphore_wait会使信号量-1
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [NSThread sleepForTimeInterval:.1];
            totalMoney+=100;
            //会对信号量进行+1操做
            dispatch_semaphore_signal(semaphore);
            NSLog(@"存100,帐户余额:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//信号量-1
            [NSThread sleepForTimeInterval:.1];
            totalMoney-=200;
            dispatch_semaphore_signal(semaphore);
            NSLog(@"取200,帐户余额:%d",totalMoney);//信号量+1
       
        }
    });
    
}
复制代码

初始信号量的值为1,此时,调用dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)方法,会判断信号量是否>0,若是信号量>0则会执行后续的操做,而且将信号量的值-1。若是信号量<=0,那么此方法会使当前线程处于休眠等待状态,直到信号量的值>0。

调用dispatch_semaphore_signal(semaphore)会使信号量+1,两种方法搭配使用就能实现线程同步的效果。

dispatch_queue(DISPATCH_QUEUE_SERIAL)

dispatch_queue(DISPATCH_QUEUE_SERIAL)其实就是一个串行队列,上文也说过,无论往串行队列中添加同步任务仍是异步任务,在执行时都是串行执行任务。使用方式以下

- (void)testDispatchQueue{
    //建立串行队列
    dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
    __block int totalMoney = 5000;
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:.2];
            totalMoney+=100;
            NSLog(@"存100,帐户余额:%d",totalMoney);
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:.2];
            totalMoney-=200;
            NSLog(@"取200,帐户余额:%d",totalMoney);//信号量+1
       
        }
    });
}
复制代码

NSLock && NSRecursiveLock && NSCondition && NSConditionLock

NSLockNSRecursiveLockNSConditionNSConditionLock实际上是对pthread_mutex中普通锁、递归锁和条件变量的封装,使其面向对象,使用起来更加简单。使用方式其实和pthread_mutex差很少,这里不作单独介绍了,只列出经常使用Api

NSLock

@protocol NSLocking

//加锁
- (void)lock;
//解锁
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking>
//尝试加锁
- (BOOL)tryLock;
//给锁设置到期时间
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end

复制代码

NSRecursiveLock

@interface NSRecursiveLock : NSObject <NSLocking> 
//尝试加锁
- (BOOL)tryLock;
//给锁设置到期时间
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
复制代码

NSCondition

NSCondition 实际上是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者。它的加锁和解锁过程同NSLock一致

@interface NSCondition : NSObject <NSLocking>

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@end
复制代码

NSConditionLock

NSConditionLock是对NSCondition的再一次封装,与NSCondition不一样的是NSConditionLock能够设置具体的条件值

@interface NSConditionLock : NSObject <NSLocking> 
//带条件加锁
- (void)lockWhenCondition:(NSInteger)condition;
//尝试加锁
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//带条件解锁
- (void)unlockWithCondition:(NSInteger)condition;
//设置锁到期时间
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end
复制代码

@synchronized

@synchronized内部其实封装了一个mutex递归锁。传入一个obj参数,内部会自动生成obj对应的递归锁,而且存放在哈希表中。经过obj的内存地址到哈希表中能拿到obj对应的递归锁。想要了解@synchronized内部实现,能够下载objc源码,查看objc_sync.mm文件中的objc_sync_enterobjc_sync_exit函数。

@synchronized的使用很简单,以下:

@synchronized (obj) {
    //须要加锁的代码
}
复制代码

@synchronized应用到存钱取钱的案例中,以下:

- (void)testSynchronized{
    __block int totalMoney = 5000;
    NSObject *obj = [NSObject new];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (obj) {
            for (int i = 0; i < 5; i++) {
                [NSThread sleepForTimeInterval:.2];
                totalMoney+=100;
                NSLog(@"存100,帐户余额:%d",totalMoney);
            }
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (obj) {
            for (int i = 0; i < 5; i++) {
                [NSThread sleepForTimeInterval:.2];
                totalMoney-=200;
                NSLog(@"取200,帐户余额:%d",totalMoney);
            }
        }
    });
}
复制代码

传入的obj必须有值,若是obj传nil,则@synchronized(nil)不起任何做用。同时要实现多线程同步的话,就必须传入相同的obj

各类锁性能对比

借用大神ibireme的再也不安全的 OSSpinLock一文中关于各类锁的性能对比图,以下:

锁相关知识补充

经过汇编代码辨别是自旋锁仍是互斥锁

分辨自旋锁和互斥锁的方式,能够根据等待锁的过程当中,线程是休眠仍是忙等状态来区分。若是线程是休眠状态。就是互斥锁,若是是忙等状态,就是自旋锁。在OC中能够跟踪汇编代码来判断一个锁是自旋锁仍是互斥锁。以OSSpinLockos_unfair_lock为例来进行汇编代码跟踪:

#import "XLLockTest.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>

@interface XLLockTest ()
@property(nonatomic, assign)OSSpinLock lock;
@end

@implementation XLLockTest

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = OS_SPINLOCK_INIT;
    }
    return self;
}

- (void)test{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self thread2];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self thread1];
    });
}

- (void)thread1{
    OSSpinLockLock(&_lock);
    NSLog(@"thread1");
    OSSpinLockUnlock(&_lock);
}

- (void)thread2{
    OSSpinLockLock(&_lock);
    sleep(60);
    NSLog(@"thread2");
    OSSpinLockUnlock(&_lock);
}
复制代码

OSSpinLock

断点在thread1方法,调用test方法,使用LLDB指令si一步一步执行汇编代码。首先进入OSSpinLockLock方法

在OSSpinLockLock方法内部调用了_OSSpinLockLockSlow函数

进入_OSSpinLockLockSlow函数,执行si指令,会发现,程序一直在循环执行一段汇编指令,以下:

熟悉汇编的同窗能够看出其实这一段汇编代码就是一个while循环,由此就能够看出OSSpinLock属于自旋锁。

os_unfair_lock

将Demo中的锁换成os_unfair_lock,而后用相同的方式跟踪汇编代码。首先是进入os_unfair_lock_lock方法,方法内部会调用_os_unfair_lock_lock_slow函数

_os_unfair_lock_lock_slow函数内部会调用__ulock_wait函数

在__ulock_wait函数内部会调用syscall,syscall其实就是系统级别的函数,执行完syscall函数以后,当前线程就会进入休眠状态。

由此就能够看出os_unfair_lock属于互斥锁。

自旋锁和互斥锁对比

自旋锁

自旋锁其实就是指当一个线程获取到资源锁以后,其它线程在获取资源锁时,会一直处于忙等状态(busy-waiting)。处于忙等状态的线程会一直处于活跃状态,可是内部并无执行任何有效的任务,只是一直在循环查看资源锁拥有者是否已经释放了锁。

如下状况下适合使用自旋锁

  • 线程等待锁的时间很短
  • 加锁的代码(临界区)常常被调用,可是发生竞争的状况不多。
  • 在CPU资源充足的状况下使用自旋锁效率更高
  • 多核处理器也适合使用自旋锁

互斥锁

互斥锁则是指当一个线程获取到资源锁以后,其它线程在获取资源锁时会被阻塞,进入睡眠状态(sleep-waiting)。线程休眠以后不会占用CPU资源,直到资源锁被释放以后才会唤醒线程。

如下状况下适合使用互斥锁

  • 预计线程等待锁的时间较长
  • 单核处理器适合使用互斥锁
  • 临界区有IO操做时使用互斥锁
  • 临界区代码复杂,或者有较大循环量的时候使用互斥锁
  • 临界区资源竞争状况不少,竞争激烈的状况使用互斥锁

OC中的atomic属性

在OC中可使用atomic或者nonatomic来修饰属性,表明原子性和非原子性。其实通俗一点来讲,使用atomic修饰的属性是线程安全的,而使用nonatomic修饰的属性不是线程安全的。为何说atomic修饰的属性是线程安全的呢?查看objc源码中的objc-accessors.mm文件能够看到atomic的底层实现,经过阅读源码能够发现,atomic修饰属性其实就是给属性的setter和getter方法内部增长了自旋锁,源码以下:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    ......
    if (!atomic) return *slot;
    //从全局的哈希表中获取到自旋锁
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    return objc_autoreleaseReturnValue(value);
}

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
    ......
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        //从全局的哈希表中获取到自旋锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    ......
}
复制代码

可是atomic只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。假设咱们使用atomic修饰NSArray类型的属性

@property(atomic, strong)NSArray *sourceArray;
复制代码

若是多个线程对sourceArray进行添加数据操做,确定会产生内存问题,由于atomic只是针对sourceArray自己的getter和setter方法,若是使用[_sourceArray objectAtIndex:index]时,就不是线程安全的,由于它和sourceArray的setter和getter方法没有关系。想要保证[_sourceArray objectAtIndex:index]的线程安全,就须要对_sourceArray的使用进行加锁操做。

iOS中的读写安全方案

在开发过程当中个,有一种比较特殊的状况,就是在临界区中有I/O操做时,若是咱们使用以上任何一种锁来对临界区进行加锁,那么在同一时间内只能执行一次读或者写的操做。可是多条线程同时执行读的操做是不会有任何数据问题的,只有在多条线程同时执行读写操做时才会形成数据问题。总结来讲,就是要知足如下的几种场景:

  • 同一时间内,只能有一个线程进行写的操做。
  • 同一时间内,容许有多个线程进行读的操做。
  • 同一时间内,不容许同时有读的操做,又有写的操做。

以上的场景就是典型的“多读单写”的操做,常用在文件等数据的读写操做。在iOS中想要实现这种效果,经常使用的方案有如下两种:

  • pthread_rwlock 读写锁
  • dispatch_barrier_async 异步栅栏调用

关于dispatch_barrier_async的使用,上文GCD的部分有详细介绍,此处主要介绍pthread_rwlock的使用。

pthread_rwlock主要Api以下:

- (void)testRwLock{
    //初始化锁
    pthread_rwlock_t rwLock;
    pthread_rwlock_init(&rwLock, NULL);
    //读操做-加锁
    pthread_rwlock_rdlock(&rwLock);
    //读操做-尝试加锁
    pthread_rwlock_tryrdlock(&rwLock);
    //写操做-加锁
    pthread_rwlock_wrlock(&rwLock);
    //读操做-尝试加锁
    pthread_rwlock_trywrlock(&rwLock);
    
    //解锁
    pthread_rwlock_unlock(&rwLock);
    //销毁
    pthread_rwlock_destroy(&rwLock);
}
复制代码

模拟读写操做,代码以下:

#import "XLLockTest.h"
#import <pthread.h>

@interface XLLockTest ()
@property(nonatomic, assign)pthread_rwlock_t rwlock;
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation XLLockTest

- (instancetype)init{
    self = [super init];
    if (self) {
        pthread_rwlock_init(&_rwlock, NULL);
        self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)test{
    for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self readThread];
        });
        
        dispatch_async(self.queue, ^{
            [self writeThread];
        });
    }
}

- (void)readThread{
    pthread_rwlock_rdlock(&_rwlock);
    sleep(1);
    NSLog(@"读操做");
    pthread_rwlock_unlock(&_rwlock);
}

- (void)writeThread{
    pthread_rwlock_wrlock(&_rwlock);
    sleep(1);
    NSLog(@"写操做");
    pthread_rwlock_unlock(&_rwlock);
}

- (void)dealloc{
    pthread_rwlock_destroy(&_rwlock);
}

复制代码

调用XLLockTest的test方法,打印以下:

能够看出,在同一时间内,可能会执行两次读操做,可是只会执行一次写操做。

参考文章

再也不安全的 OSSpinLock

深刻理解iOS开发中的锁

结束语

以上内容纯属我的理解,若是有什么不对的地方欢迎留言指正。

一块儿学习,一块儿进步~~~

相关文章
相关标签/搜索