【iOS】线程同步&读写安全技术(锁、信号量、同步串行队列)


多线程安全隐患

资源共享:一块资源可能会被多个线程共享

当多个线程可能会访问同一块资源(对象、变量、文件)时,易引起数据错乱和数据安全问题

比如很经典的存取钱和买票问题:

存钱取钱问题

在这里插入图片描述

初始余额有1000元,存1000元取500元,按理说应剩下1500元,可两条线程同时对余额进行操作的结果就是数据错乱,最后剩下500元

以下是代码实现:

/*
 存取钱问题
 */

- (void)saveMoney {
    
    NSInteger oldMoney = self.money;
    sleep(.5);
    oldMoney += 50;
    self.money = oldMoney;
    
    NSLog(@"存50元还剩%ld元 --- %@", oldMoney, [NSThread currentThread]);
    
}

- (void)drawMoney {
    
    NSInteger oldMoney = self.money;
    sleep(.5);
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20元还剩%ld元 --- %@", oldMoney, [NSThread currentThread]);
    
}

- (void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; ++i) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; ++i) {
            [self drawMoney];
        }
    });
}

多条线程同时存取钱导致数据错乱:

在这里插入图片描述

卖票问题

在这里插入图片描述

两边同时卖出一张票,应剩下998张票,两条线程同时对票数操作的结果就是数据错乱,剩下999张

代码实现:

/*
 卖票问题
 */

// sleep()让多个线程尽可能拿到相同的值
// 卖1张票
- (void)saleTicket {
    
    NSInteger oldTicketCount = self.ticketsCount;
    sleep(.7);
    oldTicketCount--;
    self.ticketsCount = oldTicketCount;
    
    NSLog(@"还剩%ld张票 --- %@", oldTicketCount, [NSThread currentThread]);
    
}

// 每条线程卖5张票
- (void)saleTickets {
    self.ticketsCount = 15;
    
    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];
        }
    });
    
}

多条线程同时卖票导致数据错乱:

在这里插入图片描述

解决方案

线程同步:让线程按照预定的先后顺序来执行

加锁是为了保证当前正在访问的只有我这条线程,一加锁,别的线程就没办法访问了(就会忙等待或是休眠),一解锁,别的线程才能去访问
或者使用GCD中的信号量、同步串行队列也可以实现线程同步(协同步调),下面展开分析

1. 锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程就不会执行,当上一个线程的任务执行完毕,下一个线程会立即执行
加锁也就是保证同一时间只有一个线程在执行

锁分两大类,自旋锁互斥锁
锁的归类其实基本的锁就包括了三类: ⾃旋锁、互斥锁、读写锁,其他的⽐如条件锁、递归锁、信号量都是上层的封装和实现

自旋锁: 下一个线程反复检查上一个线程是否解锁,等待过程中线程保持执行(while),因此是一种忙等待
所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是一直地不停循环在那里,直到被锁资源释放锁

互斥锁: 下一个线程在等待上一个线程解锁的过程中(即获取锁失败)处于休眠状态,当互斥锁被释放,线程被唤醒执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪)
所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时CPU可以调度其他线程工作,直到被锁资源释放锁,此时会唤醒休眠线程

自旋锁

自旋锁优缺点

优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度(唤醒线程、切换线程)、CPU时间片轮转等耗时操作,所以如果等待锁的时间非常短暂,就没必要让线程睡眠,不然更耗时间,自旋锁的效率远高于互斥锁

缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用

OSSpinLock
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁(如果需要等待就不加锁,直接返回false,如果不需要等待就加锁,返回true)
bool flag = OSSpinLockTry(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

在协调同步这些不同的线程时,为什么要使用同一把锁?
如果每条线程下都创建一个新锁,那么这个锁开始永远都是未加锁状态,故下一个线程不会等待上一个线程结束(线程不会阻塞),而是直接进行,锁不一样,意味着不同线程可以同时进行

初始化同一把锁,有以下几种办法:

静态局部变量

{
static OSSpinLock _lock = OS_SPINLOCK_INIT;

// 如果初始化是动态调用函数的,就不能使用static
// 因为函数调用是在运行时确定的,而statis右边的变量必须要在编译时确定
static OSSpinLock _lock = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    _lock = test();
});
}

静态全局变量

// 属性
@property (nonatomic, assign)OSSpinLock lock;
self.ticketLock = OS_SPINLOCK_INIT;

// 静态全局变量
static OSSpinLock lock_;
// 让此全局变量只初始化一次
+ (void)initialize {
    if (self == [OSSpinLockClass class]) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            lock_ = OS_SPINLOCK_INIT;
        });
    }
}

注意:OSSpinLock目前已不再安全,Apple已不推荐使用

在这里插入图片描述

原因是可能会出现优先级反转问题如果等待锁的线程优先级较高,它会一直占用着CPU资源,前面优先级低的线程就无法释放锁

os_unfair_lock

此自旋锁的出现就是为了解决优先级反转问题

#import <os/lock.h>
// 初始化
os_unfair_lock ticketLock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(lock);

若最后没有解锁,那么其他线程就拿不到锁,即死锁

atomic

自旋锁的实际应用,自动生成的setter方法会根据修饰符不同调用不同方法,最后统一调用reallySetProperty方法,其中就有一段关于atomic修饰词的代码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

getter方法也是如此:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

可以看到属性如果被atomic修饰,setter/getter操作会进行加spinlock锁处理

注意:atomic关键字只能保证setter、getter操作的线程安全,并不能保证使用属性的过程是安全的

比如不能保证self.index+1也是安全的,如果改成self.index=i(单纯调用setter方法)是能保证setter方法的线程安全的

互斥锁

互斥锁又分为递归锁、非递归锁

pthread_mutex_t
#import <pthread/pthread.h>

// PTHREAD_MUTEX_INITIALIZER宏定义是一个结构体:{_PTHREAD_MUTEX_SIG_init, {0}}
// 下面两段代码与结构体基本语法不符(结构体必须在定义变量的同时赋值,静态初始化)
self.mutex = PTHREAD_MUTEX_INITIALIZER;
    
pthread_mutex_t mutex;
mutex = PTHREAD_MUTEX_INITIALIZER;
    
// 静态初始化,是正确的语法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

另有方法初始化锁:

// 初始化锁
pthread_mutex_init(&mutex, NULL);
// 加锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁锁
pthread_mutex_destroy(&mutex);

初始化一个mutex递归锁

// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

// 初始化递归锁
// 第二个参数传NULL,等效于PTHREAD_MUTEX_DEFAULT:pthread_mutex_init(mutex, NULL);
pthread_mutex_init(&mutex, &attr);

// 销毁属性
pthread_mutexattr_destroy(&attr);

锁属性attributes的类型type

在这里插入图片描述

如果加锁的任务执行有递归调用,那么是不是会发生堵塞?还没等解锁就又要同步加锁
这里给换用递归锁即可解决,递归锁允许同步加锁,即只允许同一个线程下对一把锁进行重复加锁,不会出现阻塞,最后解锁次数与加锁次数相同即可

void test(void) {
    pthread_mutex_lock(&mutex);
    NSLog(@"%s", __func__);
    test();  //递归
    pthread_mutex_unlock(&mutex);
}
条件pthread_cond_t(线程检查器)

条件condition用于解决类似于生产者 - 消费者模式的问题,实现跨线程执行

// 初始化条件
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);

// 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&cond, &mutex);
// 唤醒一个正在等待该条件的线程(发送一个信号)
pthread_cond_signal(&cond);
// 唤醒所有正在等待该条件的线程
pthread_cond_broadcast(&cond);
// 销毁条件
pthread_cond_destroy(&cond);

此模式的情景是,要等待生产者产出商品才能消费,如果无商品存在,就无法消费:

void consume(void) {
	pthread_mutex_lock(&mutex);
	if (/*无商品*/) {
		// 等待,放开锁,让生产线程执行
		pthread_cond_wait(&cond, &mutex);
	}
	pthread_mutex_unlock(&mutex);
}

void produce(void) {
	pthread_mutex_lock(&mutex);
	// 产出了商品
	pthread_mutex_unlock(&mutex);
	// 有了商品后,唤醒一个正在等待该条件的consume线程(发送一个信号)
	pthread_cond_signal(&cond);
}
NSLock&NSRecursiveLock(递归锁)

NSLock是对pthread_mutex_t普通锁的封装,所以它是一个非递归锁
如果对NSLock强行使用递归调用,就会在调用时发生堵塞,并非死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。(因为不会查询缓存)

@interface NSLock : NSObject <NSLocking>
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end

@protocol NSLocking
// 加锁
- (void)lock;
// 解锁
- (void)unlock;
@end

OC对象的初始化就无需多言了

NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
注意:NSRecursiveLock虽然有递归性,但没有多线程特性

- (void)test {
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^block)(int);
            
            block = ^(int value) {
                [lock lock];
                if (value > 0) {
                    NSLog(@"value——%d", value);
                    block(value - 1);
                }
                [lock unlock];
            };
            block(10);
        });
    }
}

因为for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁,也就是找不到解锁的出口

线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁

此时我们可以通过@synchronized对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性
@synchronized是符合递归和多线程特性的

同一线程可以多次获取而不会导致死锁的锁

NSCondition(条件锁)&NSConditionLock

NSCondition是对pthread_mutex_t和pthread_cond_t条件的封装

可以加锁的同时也可等待或唤醒线程:

@interface NSCondition : NSObject <NSLocking>
- (void)wait;  //等待
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;  // 唤醒一个线程
- (void)broadcast;  // 唤醒所有线程
@property (nullable, copy) NSString *name;
@end

NSConditionLock是对NSCondition的进一步封装,可以设置具体值

@interface NSConditionLock : NSObject <NSLocking>
- (instancetype)initWithCondition:(NSInteger)condition;
@property (readonly) NSInteger condition;
- (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;
@property (nullable, copy) NSString *name;
@end

通过设置condition的值,可以设置锁哥线程间的依赖,控制线程的执行顺序:

self.conditionLock = [[NSConditionLock alloc] initWithCondition: 1];

// 三个线程同时进行
[[[NSThread alloc] initWithTarget: self selector: @selector(p_one) object: nil] start];
[[[NSThread alloc] initWithTarget: self selector: @selector(p_two) object: nil] start];
[[[NSThread alloc] initWithTarget: self selector: @selector(p_three) object: nil] start];

- (void)p_one {
    [self.conditionLock lockWhenCondition: 1];
    NSLog(@"_one");
    [self.conditionLock unlockWithCondition: 2];
}

- (void)p_two {
    [self.conditionLock lockWhenCondition: 2];
    sleep(1);
    NSLog(@"_two");
//    [self.conditionLock unlock];
    [self.conditionLock unlockWithCondition: 3];
}

- (void)p_three {
//    [self.conditionLock lockWhenCondition: 2];
    [self.conditionLock lockWhenCondition: 3];
    NSLog(@"_three");
    [self.conditionLock unlock];
}

打印结果:_one、_two、_three,让本会并发执行的三个线程按照执行了

@synchronized

@synchronized是对mutex递归锁的封装

@synchronized (/*token obj*/) {
    // 任务
}

放进去的这个token对象要是同一个,才能保证同一把锁
一般会传入self、如果是不同实例调用且也要保证一把锁的需求,可传入[self class]
为保证唯一性,一般这样写:

static NSObject* lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    lock = [[NSObject alloc] init];
});
@synchronized (lock) {
    // 任务
}

@synchronized锁
@synchronized底层源码分析

2. 信号量

信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取0/1时的特例。信号量可以有更多的取值空间,控制线程最大并发数量,用来实现更加复杂的同步,而不单单是线程间互斥

// 初始化
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// 加锁
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 解锁
dispatch_semaphore_signal(semaphore);
/*
注: dispatch_semaphore  其他两个功能
1.还可以起到阻塞线程的作用
2.可以实现定时器功能,这里不做过多介绍
*/

控制最大并发数量:

// 控制最大并发数量:5个线程
 self.ticketSemaphore = dispatch_semaphore_create(5);

- (void)otherTest {
    for (int i = 0; i < 20; ++i) {
        [[[NSThread alloc] initWithTarget: self selector: @selector(test) object: nil] start];
    }
}

- (void)test {
    // 如果信号量的值 > 0,就-1,往下执行
    // 如果信号量的值 <= 0,当前线程就会进入休眠等待(知道信号量的值 > 0)
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(3);
    NSLog(@"test --- %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

3. 同步串行队列

dispatch_sync(dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL), ^{
    // 任务
});

读写安全

I/O操作,即文件数据读写操作要实现读写安全,需实现以下需求:

  1. 多读单写(写写互斥)
  2. 不允许读写同时进行(读写互斥)
  3. 读写不能堵塞主线程,不能影响主线程

这就需要读写锁来实现

读写锁

读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对共享资源进⾏写操作

现有两种解决读写安全的方案:

pthread_rwlock_t

// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读加锁
pthread_rwlock_rdlock(&lock);
// 读尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写加锁
pthread_rwlock_wrlock(&lock);
// 写尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁锁
pthread_rwlock_destroy(&lock);

当读写锁在读加锁状态时,所有试图以读模式对它进⾏加锁的线程都可以得到访问权

当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞,必须直到所有的线程释放锁

读模式锁定时可以共享, 以写模式锁住时意味着独占,所以读写锁⼜叫共享-独占锁

异步栅栏函数dispatch_barrier_async

dispatch_queue_t concurrentQueue = dispatch_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);

// 栅栏函数保证读写的互斥
dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"write");
});

// 异步并行保证共享读线程
dispatch_async(concurrentQueue, ^{
        NSLog(@"read");
});

此处的异步栅栏函数传入的并发队列必须是通过create创建的
如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async

锁的性能对比

自旋锁效率肯定远高于互斥锁

以下是自旋锁性能排行:

在这里插入图片描述

总结

  • OSSpinLock不再安全,底层用os_unfair_lock替代
  • atomic只能保证setter、getter时线程安全,所以更多的使用nonatomic来修饰
  • 读写锁更多使用栅栏函数来实现
  • @synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁
  • NSLock、NSRecursiveLock、NSCondition和NSConditionLock底层都是对pthread_mutex的封装
  • NSCondition和NSConditionLock是条件锁,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似
  • 普通场景下涉及到线程安全,可以用NSLock
  • 循环调用时用NSRecursiveLock
  • 循环调用且有线程影响时,请注意死锁,如果有死锁问题请使用@synchronized

日常开发中若需要使用线程锁来保证线程安全,请多考虑一下再选择使用哪个锁,@synchronized并不是最优的选择。作为一名优秀的开发不但能让App正常运行,更要让它优质地运行、优化它的性能

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值