ObjC 多线程简析(一)-多线程简述和线程锁的基本应用

在iOS开发中,常常会遇到将耗时操做放在子线程中执行的状况。api

通常状况下咱们会使用NSThread、NSOperation和GCD来实现多线程的相关操做。初次以外pthread也能够用于多线程的相关开发。数组

pthread提供了一套C语言的api,它是跨平台的,须要开发人员自行管理线程的生命周期;NSThread提供了一套OC的api,使用更加简单,可是线程的生命周期也是须要开发人员本身管理的;GCD也提供了C语言的api,它充分利用了CPU多核处理事件的能力,而且能够本身管理线程的生命周期;NSOperation是对GCD作了一层OC的封装,更加面向对象,生命周期也由其自动管理。安全

本篇主要使用GCD来介绍iOS开发中的多线程状况,以及实现线程同步的些许方式。bash

GCD的基本使用

基本概念

GCD提供了同步和异步处理事情的能力,分别调用dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)建立任务。同步只能在当前线程中执行,并不会建立一个新的线程。只有异步才会建立新的线程,任务也是在新的线程中执行的。多线程

GCD实现多线程一般须要依赖一个队列,而GCD提供了串行和并行队列。串行队列是指任务一个接着一个执行,下一个任务的执行必须等待上一个任务执行结束。并行队列则能够同时执行多个任务,可是并发任务的执行须要依赖于异步函数(dispatch_async)。并发

任务和队列

GCD的多线程技术须要往函数中添加一个队列,那么这四种状况排列组合将会出现什么状况呢?可使用下表进行表示:app

GCD任务和队列

当使用dispatch_sync的时候不管是并发队列仍是串行队列或者主线程,全都不会开启新的线程,而且都是串行执行任务。异步

当使用dispatch_async的时候,除了在主线程的状况下,全都会开启新的线程,而且只有在并发队列的时候才会并行执行任务。async

队列组

GCD提供了队列组的api,能够实如今一个队列组中控制队列中任务的执行顺序:函数

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务2");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"任务3");
    });
复制代码

线程同步

尽管多线程提供了能充分利用线程处理事情的能力,好比多任务下载、处理耗时操做等。可是当多条线程操做同一块资源的时候就可能会出现不合理的现象(数据错乱,数据安全),这是由于多线程执行的顺序和时间是不肯定的。因此当一条线程拿到资源进行操做的时候,下一条线程拿到可能仍是以前的资源。因此线程同步就是让多线程在同一时间只有一条线程在操做资源。

在实现线程同步的时候,咱们首先想到的应该是给资源操做任务加锁。那么ObjC中提供了哪些线程锁呢?

OSSpinLock

OSSpinLock是一种自旋锁。自旋锁在加锁状态下,等待锁的线程会处于忙等的状态,一直占用着CPU的资源。OSSpinLock目前已经再也不安全,api中也再也不建议使用它,由于它可能出现优先级反转的问题。

优先级反转的问题就是,当优先级比较高的线程在等待锁,它须要继续往下执行,因此优先级低的占用着锁的线程就无法将锁释放。

OSSpinLock存在于libkern/OAtomic.h中,经过定义咱们能够看出它是一个int32_t类型的(定义:typedef int32_t OSSpinLock;)。使用OSSpinLock的时候须要对锁进行初始化,而后再操做数据以前进行加锁,操做数据以后进行解锁。

// 初始化OSSpinLock
_osspinlock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&_osspinlock);

// 操做数据
// ...

// 解锁
OSSpinLockUnlock(&_osspinlock);
复制代码

os_unfair_lock

iOS10以后apple废弃了OSSpinLock使用os/lock中定义的os_unfair_lock。经过汇编来看os_unfair_lock并非一种自旋锁,在加锁状态下,等待锁的线程会处于休眠状态,不占用CPU资源。

一样使用os_unfair_lock的时候也须要初始化。

// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;

// 加锁
os_unfair_lock_lock(&(_osunfairLock));

// 操做数据
// ...

// 解锁
os_unfair_lock_unlock(&(_osunfairLock));
复制代码

pthread_mutex

pthread_mutex是属于pthreadapi中的,mutex属于互斥锁。在加锁状态下,等待锁的线程会处于休眠状态,不会占用CPU的资源。

mutex初始化的时候须要传入一个锁的属性(int pthread_mutex_init(pthread_mutex_t * __restrict,const pthread_mutexattr_t * _Nullable __restrict);),若是传NULL就是默认状态PTHREAD_MUTEX_DEFAULT也就是PTHREAD_MUTEX_NORMAL

pthread_mutex状态pthread_mutexattr_t的定义:

/* * 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
复制代码

初始化mutex以后再须要加锁的时候调用pthread_mutex_lock(),解锁的时候调用pthread_mutex_unlock();

另外pthread_mutex中有一个销毁锁的方法int pthread_mutex_destroy(pthread_mutex_t *);在不须要锁的时候一般须要调用一下,将mutex锁的地址做为参数传入。

pthread_mutex 递归锁

在开发中时常遇到递归调用的状况,若是在一个函数中进行了加锁和解锁操做,而后在解锁以前递归。那么递归的时候线程会发现已经加锁了,会一直在等待锁被释放。这样递归就无法继续往下进行,锁也永远不会被释放,就形成了死锁的现象。

为了解决这个问题,pthread_mutex的属性中提供了将pthread_mutex变为递归锁的属性。’

递归锁就是同一条线程能够对一把锁进行重复加锁,而不一样线程却不能够。这样每一次递归都会加一次锁,因此互不冲突,当递归结束以后会从后往前以此解锁。不一样线程的时候,递归锁会判断这条线程正在等待的锁与加锁的不是一条线程,因此不会进行加锁,而是在等待锁被释放。

建立递归锁的时候须要初始化一个pthread_mutexattr_t属性:

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_pthreadMutex, &attr);
pthread_mutexattr_destroy(&attr);
复制代码

属性使用完也须要进行销毁,调用pthread_mutexattr_destroy函数实现。

pthread_mutex 条件

当两条线程在操做同一资源,可是一条线程的执行,须要依赖另外一条线程的执行结果的时候,因为默认多线程的访问时间和顺序是不固定的,因此不容易实现。pthread_mutex提供了执行条件的额api,使用pthread_cond_init()初始化一个条件。

在须要等待的地方使用pthread_cond_wait();等待信号的到来,此时线程会进入休眠状态而且放开mutex锁,等待信号到来的时候会被唤醒而且对mutex加锁。信号发送使用pthread_cond_signal()来告诉等待的线程,本身的线程处理完了,依赖的能够开始执行了,等待的线程就会往下继续执行。也可使用pthread_cond_broadcast()进行广播,告诉全部等待的该条件的线程。条件也是须要销毁的,使用pthread_cond_destroy()销毁条件。

好比两条线程操做一个数组,a线程负责删除数组,b线程负责往数组中添加元素。a线程删除元素的条件是数组中必须有元素存在。

代码以下:

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

@interface ViewController ()

@property (nonatomic, assign) pthread_mutex_t pthreadMutex;
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, assign) pthread_cond_t cond;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutex_init(&_pthreadMutex, NULL);
    pthread_cond_init(&_cond, NULL);
    [self testCond];
}

- (void)testCond {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}

- (void)addObject {
    pthread_mutex_lock(&_pthreadMutex);
    NSLog(@"end start %@", _array);
    [self.array addObject:@"test"];
    NSLog(@"end add %@", _array);
    pthread_cond_signal(&_cond);
    pthread_mutex_unlock(&_pthreadMutex);
}

- (void)removeObject {
    pthread_mutex_lock(&_pthreadMutex);
    pthread_cond_wait(&_cond, &_pthreadMutex);
    NSLog(@"start remove %@", _array);
    [self.array removeLastObject];
    NSLog(@"end remove %@", _array);
    pthread_mutex_unlock(&_pthreadMutex);
}

- (void)dealloc {
    pthread_cond_destroy(&_cond);
    pthread_mutex_destroy(&_pthreadMutex);
}
复制代码

NSLock和NSRecursiveLock

NSLock和NSRecursiveLock是对pthread_mutex普通锁和递归锁的OC封装。更加面向对象,使用也比较简单。它使用了NSLocking协议来生命加锁和解锁的方法。因为上面已经对pthread_mutex进行了简单的介绍,NSLock和NSRecursiveLock的api都是OC的也比较简单。这里再也不赘述,只是说明有这样一种实现线程同步的方法。

NSCondition和NSConditionLock

NSCondition是对mutexcond的封装,因为NSCondition也遵循了NSLocking协议,因此他也能够加锁和加锁。使用效果和pthread的cond同样,在等待的时候调用wait,发送信号调用singal

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) NSCondition *cond;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.array = [NSMutableArray array];
    self.cond = [[NSCondition alloc] init];
    [self testCond];
}

- (void)testCond {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}

- (void)addObject {
    [_cond1 lock];
    NSLog(@"end start %@", _array);
    [self.array addObject:@"test"];
    NSLog(@"end add %@", _array);
    [_cond1 signal];
    [_cond unlock];
}

- (void)removeObject {
    [_cond lock];
    [_cond wait];
    NSLog(@"start remove %@", _array);
    [self.array removeLastObject];
    NSLog(@"end remove %@", _array);
    [_cond unlock];
}
复制代码

NSConditionLock是对NSCondition的又一层封装。NSConditionLock能够添加条件,经过- (instancetype)initWithCondition:(NSInteger)condition;初始化并添加一个条件,条件是NSInteger类型的。解锁的时候是按照这个条件进行解锁的。依然是上述例子:

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) NSConditionLock *lock2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.array = [NSMutableArray array];
    self.lock2 = [[NSConditionLock alloc] initWithCondition:1];
    [self testCond];
}

- (void)testCond {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}


- (void)addObject {
    [_lock2 lock];
    NSLog(@"end start %@", _array);
    [self.array addObject:@"test"];
    NSLog(@"end add %@", _array);
    [_lock2 unlockWithCondition:2];
}

- (void)removeObject {
    [_lock2 lockWhenCondition:2];
    NSLog(@"start remove %@", _array);
    [self.array removeLastObject];
    [_lock2 unlock];
}
复制代码

dispatch_semaphore_t

GCD提供了一个信号量的方式也能够解决线程同步的问题。

使用dispatch_semaphore_create();建立一个信号量,使用dispatch_semaphore_wait()等待信号的到来,使用dispatch_semaphore_signal()发送一个信号。

dispatch_semaphore_wait()会根据第二个参数dispatch_time_t timeout判断超时时间,通常咱们会设置为DISPATCH_TIME_FOREVER一直等待信号的到来。若是此时信号量的值大于0,那么就让信号量的值减1,而后继续往下执行代码,而若是信号量的值小于等于0,那么就会休眠等待,直到信号量的值变成大于0,再就让信号量的值减1,而后继续往下执行代码。dispatch_semaphore_signal()发送一个信号,而且让信号量加1。

经典的买票例子:

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketSemaphore = dispatch_semaphore_create(1);
    [self ticket];
}

- (void)saleTicket {
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    int oldTicketCount = _ticketCount;
    sleep(.2);
    oldTicketCount --;
    _ticketCount = oldTicketCount;
    NSLog(@"剩余的票数%d",_ticketCount);
    dispatch_semaphore_signal(_ticketSemaphore);
}

- (void)ticket {
    self.ticketCount = 20;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

复制代码

串行队列

使用GCD的串行队列实现线程同步,原理是由于串行队列必须一个接着一个执行,只有在执行完上一个任务的状况下,下一个任务才会继续执行。

使用dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);建立一条串行队列,将多线程任务都放到这条串行队列当中执行。

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketSemaphore = dispatch_semaphore_create(1);
    [self ticket];
}

- (void)saleTicket {
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    int oldTicketCount = _ticketCount;
    sleep(.2);
    oldTicketCount --;
    _ticketCount = oldTicketCount;
    NSLog(@"剩余的票数%d",_ticketCount);
    dispatch_semaphore_signal(_ticketSemaphore);
}

- (void)ticket {
    self.ticketCount = 20;
    
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}
复制代码

@synchronized

@synchronized是对mutex递归锁的封装。须要传递一个obj,@synchronized(obj)内部会生成obj对应的递归锁,而后进行加锁、解锁操做。

使用:

// 建立一个初始化一次的obj
- (NSObject *)lock {
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    return lock;
}

// 加锁买票经典案例
- (void)saleTicket {
    @synchronized([self lock]) {
        int oldTicketCount = _ticketCount;
        sleep(.2);
        oldTicketCount --;
        _ticketCount = oldTicketCount;
        NSLog(@"剩余的票数%d",_ticketCount);
    }
}

- (void)ticket {
    self.ticketCount = 20;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 递归锁
- (void)test {
    @synchronized ([self lock]) {
        NSLog(@"%s",__func__);
        [self test];
    }
}
复制代码

atomic

在OC中定义属性一般会指定属性的原子性也就是使用nonatomic关键字定义非原子性的属性,而其默认为atomic原子性

atomic用于保证属性setter、getter的原子性操做,至关于在getter和setter内部加了线程同步的锁,可是它并不能保证使用属性的过程是线程安全的。

读写安全

当咱们多项城操做一个文件的时候,若是同时进行读写的话,会形成读的内容不彻底等问题。因此咱们常常会在多线程读写文件的时候,实现多读单写的方案。即在同一时间能够有多条线程在读取文件内容,可是只能有一条线程执行写文件的操做。

下面经过模拟对文件的读写操做而且经过pthread_rwlock_tdispatch_barrier_async来实现文件读写的线程安全。

pthread_rwlock_t

使用pthread_rwlock_t的时候,须要调用pthread_rwlock_init()进行初始化。而后在读的时候调用pthread_rwlock_rdlock()对读操做进行加锁。在写的时候调用pthread_rwlock_wrlock()对读进行加锁。使用pthread_rwlock_unlock()进行解锁。在用不到锁的时候使用pthread_rwlock_destroy()对锁进行销毁。

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

@interface SecondViewController ()

@property (nonatomic, assign) pthread_rwlock_t lock;

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    pthread_rwlock_init(&_lock, NULL);
    
    for (int i = 0; i < 10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(read) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(write) object:nil] start];
    }

}

- (void)read {
    
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s",__func__);
    pthread_rwlock_unlock(&_lock);
    
}

- (void)write {
    
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s",__func__);
    pthread_rwlock_unlock(&_lock);
    
}

- (void)dealloc {
    pthread_rwlock_destroy(&_lock);
}
复制代码

经过打印结果的时间咱们能够发现,没有同一时间执行读写的操做,只有同一时间读,这样就保证了读写的线程安全。

打印结果以下:

打印结果

dispatch_barrier_async

GCD提供了一个异步栅栏函数,这个函数要求传入的并发队列必须是本身经过dispatch_queue_cretate建立的。

它的原理就是当执行到dispatch_barrier_async的时候就至关于建立了一个栅栏将线程的读写操做隔离开,这个时候只能有一个线程来执行dispatch_barrier_async里面的任务。

当咱们使用它来处理读写安全的操做的时候,使用dispatch_barrier_async来隔离写的操做,就能保证同一时间只能有一条线程对文件执行写的操做。

代码以下:

#import "SecondViewController.h"

@interface SecondViewController ()

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_barrier_async(queue, ^{
            [self write];
        });
    }
}

- (void)read {
    sleep(1);
    NSLog(@"%s",__func__);
}

- (void)write {
    sleep(1);
    NSLog(@"%s",__func__);
}
复制代码

依然经过打印结果的时间分析是否实现了文件的读写安全。下面是打印结果,明显看出文件的读写是安全的。

打印结果:

打印结果

总结

本篇主要介绍了ObjC和iOS开发中经常使用的多线程方案,并经过卖票的经典案例介绍了多线程操做统一资源形成的隐患以及经过线程同步方案解决隐患的几种方法。另外还介绍了文件读写锁以及GCD提供的栅栏异步函数处理多线程文件读写安全的两种用法。

相关文章
相关标签/搜索