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

请注意,本文编写于 156 天前,最后修改于 109 天前,其中某些信息可能已经过时。

在iOS开发中,经常会遇到将耗时操作放在子线程中执行的情况。

一般情况下我们会使用NSThread、NSOperation和GCD来实现多线程的相关操作。初次之外pthread也可以用于多线程的相关开发。

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

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

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的多线程技术需要往函数中添加一个队列,那么这四种情况排列组合将会出现什么情况呢?可以使用下表进行表示:

GCD任务和队列
GCD任务和队列

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

当使用dispatch_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提供的栅栏异步函数处理多线程文件读写安全的两种用法。

Comments

添加新评论