iOS 基础原理:多线程的锁

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 锁的概念
  • 锁的性能
  • 经典的存钱-取钱同步问题
  • 方案一:OSSpinLock自旋锁
  • 方案二:os_unfair_lock互斥锁
  • 方案三:pthread_mutex互斥锁
  • 方案四:pthread_mutex递归锁
  • 方案五:pthread_mutex条件锁
  • 方案六:NSLock锁
  • 方案七:NSRecursiveLock锁
  • 方案八:NSCondition条件锁
  • 方案九:NSConditionLock
  • 方案十:dispatch_semaphore信号量
  • 方案十一:@synchronized
  • 方案十二:atomic
  • 方案十三:pthread_rwlock读写锁
  • 方案十四:dispatch_barrier_async异步栅栏
  • 方案十五:dispatch_group_t调度组
  • 方案十六:addDependency操作依赖
  • Demo
  • 参考文献

锁的概念

由于线程共享了进程的资源空间,如果是多线程读写资源,就会出现同时对同一资源进行操作的情况,这样就会发生数据的读写错乱和不一致现象。为了保持数据的一致性,不至于出现多个线程同时修改相同资源的情况,我们需要为资源操作进行加锁处理,在一个线程访问资源时,对该资源进行加锁,确保在同一时间只有一个线程对资源进行操作,当线程对资源的操作结束之后,对该资源解除锁定,允许下一个线程对资源进行访问。

锁是最常用的同步工具。一段代码段在同一个时间只能允许被一个线程访问,比如一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码解锁后,B线程才能访问加锁代码。不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。使用锁的场景如下载解压缩,排序显示,加载多张图片然后在都下载完成后合成一张整图等等。


锁的性能

iOS开发中常用的锁有如下几种:

  • @synchronized 关键字加锁
  • NSLock 对象锁
  • NSCondition 条件锁
  • NSConditionLock 条件锁
  • NSRecursiveLock 递归锁
  • pthread_mutex 互斥锁(C语言)
  • dispatch_semaphore 信号量实现加锁(GCD)
  • OSSpinLock 自旋锁(不建议使用)

分别使用8种方式加锁、解锁1千万次,比较一下其开发性能:

CFTimeInterval timeBefore;
CFTimeInterval timeCurrent;
NSUInteger i;
NSUInteger count = 1000*10000;//执行一千万次
@synchronized
id obj = [[NSObject alloc] init];;
timeBefore = CFAbsoluteTimeGetCurrent();

for(i = 0; i < count; i++) {
    @synchronized(obj) {
        
    }
}

timeCurrent = CFAbsoluteTimeGetCurrent();
printf("@synchronized used : %f\n", timeCurrent-timeBefore);
NSLock
NSLock *lock = [[NSLock alloc] init];
timeBefore = CFAbsoluteTimeGetCurrent();
for(i=0; i
NSCondition
NSCondition *condition = [[NSCondition alloc] init];
timeBefore = CFAbsoluteTimeGetCurrent();

for(i=0; i
NSConditionLock
NSConditionLock *conditionLock = [[NSConditionLock alloc]init];
timeBefore = CFAbsoluteTimeGetCurrent();
for(i=0; i
NSRecursiveLock
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc]init];
timeBefore = CFAbsoluteTimeGetCurrent();
for(i=0; i
pthread_mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
timeBefore = CFAbsoluteTimeGetCurrent();

for(i=0; i
dispatch_semaphore
dispatch_semaphore_t semaphore =dispatch_semaphore_create(1);
timeBefore = CFAbsoluteTimeGetCurrent();

for(i=0; i
OSSpinLockLock
OSSpinLock spinlock = OS_SPINLOCK_INIT;
timeBefore = CFAbsoluteTimeGetCurrent();

for(i=0; i

输出结果为:

@synchronized used : 2.063288
NSLock used : 0.348233
NSCondition used : 0.318476
NSConditionLock used : 0.982108
NSRecursiveLock used : 0.516823
pthread_mutex used : 0.238920
dispatch_semaphore used : 0.164231
OSSpinLock used : 0.088755

可以发现OSSpinLock的性能最好,但不建议使用。GCDdispatch_semaphore紧随其后。 NSConditionLock@synchronized性能较差。

需要注意的是这里仅仅是对各种锁直接LockUnlock的性能测试,其中部分锁的使用条件上还是有细微的差异的,比如NSLock之类的还有tryLock等方法用于加锁,不同对象锁的功能偏向不一样等等,有兴趣的可以逐个搜索再更深入的研究不同锁之间的区别。另外,一般来说客户端很少会有这么大量的加锁解锁操作,所以日常来说这些锁的性能都是可以满足使用需求的。

锁的性能可以考虑下临界区的时间,自旋锁本质是一个忙等待,在临界区持续在做 while(0)的动作。基于信号量的锁的消耗是在进出临界区所花费的内核态和用户态上下文切换的消耗。

性能从高到低排序
os_unfair_lock 自旋锁
OSSpinLock 自旋锁
dispatch_semaphore 信号量
pthread_mutex 互斥锁
dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
NSLock 普通(互斥)锁
NSCondition 条件锁
pthread_mutex(recursive) 递归锁
NSRecursiveLock 递归锁
NSConditionLock 条件锁
@synchronized 递归锁

经典的存钱-取钱同步问题

假设我们账号里面有100元,每次存钱都存10元,每次取钱都取20元。存5次,取5次。那么就是应该最终剩下50元才对。我们把存在和取钱在不同的线程中访问的时候,如果不加锁,就很可能导致问题。

存钱:每次存钱都存10元
- (void)saveMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 10;
    self.money = oldMoney;
    
    NSLog(@"存10元,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
取钱:每次取钱都取20元
- (void)drawMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20元,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
进行存钱、取钱操作
// 最初账号里面有100元
self.money = 100;

// 全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

// 异步执行全局队列中的任务:存钱,存5次
dispatch_async(queue, ^{
    for (int i = 0; i < 5; i++)
    {
        [self saveMoney];
    }
});

// 异步执行全局队列中的任务:取钱,取5次
dispatch_async(queue, ^{
    for (int i = 0; i < 5; i++)
    {
        [self drawMoney];
    }
});

从输出结果上来看,明显不是预期的那样。这是因为,正常情况下,取20元之后,还剩下80元,然后存10元,剩余90元没问题。但是当我们是不同线程同时操作的时候,可能导致的情况是,正在取钱的时候来存钱了,也就是20元还没取出来就去存钱,误以为当前还是100元,给出结果是100 + 10 = 110 元,然而实际上应该是 100 - 20 + 10 = 90 元,这样就导致了数据的紊乱。

2020-07-16 14:55:19.990567+0800 多线程Demo[60071:18849406] 取20元,还剩80元 - {number = 3, name = (null)}
2020-07-16 14:55:19.990571+0800 多线程Demo[60071:18849404] 存10元,还剩110元 - {number = 7, name = (null)}
2020-07-16 14:55:19.990706+0800 多线程Demo[60071:18849406] 取20元,还剩90元 - {number = 3, name = (null)}
2020-07-16 14:55:19.990696+0800 多线程Demo[60071:18849404] 存10元,还剩100元 - {number = 7, name = (null)}
2020-07-16 14:55:19.990774+0800 多线程Demo[60071:18849406] 取20元,还剩80元 - {number = 3, name = (null)}
2020-07-16 14:55:19.990783+0800 多线程Demo[60071:18849404] 存10元,还剩90元 - {number = 7, name = (null)}
2020-07-16 14:55:19.990840+0800 多线程Demo[60071:18849404] 存10元,还剩100元 - {number = 7, name = (null)}
2020-07-16 14:55:19.990847+0800 多线程Demo[60071:18849406] 取20元,还剩70元 - {number = 3, name = (null)}
2020-07-16 14:55:19.990894+0800 多线程Demo[60071:18849404] 存10元,还剩80元 - {number = 7, name = (null)}
2020-07-16 14:55:19.991257+0800 多线程Demo[60071:18849406] 取20元,还剩60元 - {number = 3, name = (null)}

解决的办法就是给线程加锁,当存钱的时候先去加锁,然后存完了再放开锁。取钱也是一样,这样就保证数据的一致性。举个加锁解锁流程的例子。init 方法初始化了一个 _elements 数组和一个NSLock实例。这个类还有个push:方法,它先获取锁,然后向数组中插入元素、最终释放锁。可能会有许多线程同时调用 push:方法,但是[_elements addObject:element]这行代码在任何时候将只会在一个线程上运行。

- (instancetype)init
{
    self = [super init];
    if (self) {
        _elements = [NSMutableArray array];
        _lock = [[NSLock alloc] init];
    }
    return self;
}
- (void)push:(id)element
{
    [_lock lock];
    [_elements addObject:element];
    [_lock unlock];
}
  1. 线程 A 调用 push: 方法
  2. 线程 B 调用 push: 方法
  3. 线程 B 调用 [_lock lock],因为当前没有其他线程持有锁,线程 B 获得了锁
  4. 线程 A 调用 [_lock lock],但是锁已经被线程 B 占了所以方法调用并没有返回,这会暂停线程 A 的执行
  5. 线程 B 向 _elements 添加元素后调用 [_lock unlock]。当这些发生时,线程 A 的 [_lock lock] 方法返回,并继续将自己的元素插入 _elements

方案一:OSSpinLock自旋锁

在互斥锁中,如果锁被线程保持,其他尝试加锁的线程会被阻塞不再占用CPU资源,等待锁被释放之后再唤起其他线程进行加锁,而对于自旋锁来讲,如果自旋锁已经被别的线程保持,调用线程就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这样就会导致线程一直处于忙等状态,"自旋"一词就是因此而得名,所以自旋锁在执行单元保持锁的时间很短时,很有优势,不用频繁地休眠唤起线程从而拥有比较高的效率。

由于自旋锁的效率会比互斥锁高,使用的频率非常高,尤其是ReactiveCocoa大量使用了这种锁。当然这种锁也并不是绝对安全的,YY大神的文章不再安全的 OSSpinLock 说这个自旋锁存在优先级反转问题,有兴趣的可以详细了解一下。正式因为自旋锁存在线程安全问题,所以在iOS10.0之后的api中开始废弃自旋锁,使用os_unfair_lock来进行替换。

使用说明

需要导入头文件
#import 
初始化自旋锁
OSSpinLock lock = OS_SPINLOCK_INIT;
尝试加锁(如果不需要等待,就直接加锁,返回true。如果需要等待,就不加锁,返回false)
BOOL res = OSSpinLockTry(lock);
加锁与解锁
OSSpinLockLock(lock);
OSSpinLockUnlock(lock);

解答本题

声明自旋锁属性
@property (assign, nonatomic) OSSpinLock moneyLock;
初始化自旋锁
self.moneyLock = OS_SPINLOCK_INIT;
存钱操作
- (void)saveMoney
{
    OSSpinLockLock(&_moneyLock);//加锁
    [super saveMoney];
    OSSpinLockUnlock(&_moneyLock);//解锁
}
取钱操作
- (void)drawMoney
{
    OSSpinLockLock(&_moneyLock);//加锁
    [super drawMoney];
    OSSpinLockUnlock(&_moneyLock);//解锁
}

由输出可知,能保证线程安全,数据没有错乱。

2020-07-16 15:27:02.262793+0800 多线程Demo[60201:18867775] 存10元,还剩110元 - {number = 5, name = (null)}
2020-07-16 15:27:02.262937+0800 多线程Demo[60201:18867775] 存10元,还剩120元 - {number = 5, name = (null)}
2020-07-16 15:27:02.263043+0800 多线程Demo[60201:18867775] 存10元,还剩130元 - {number = 5, name = (null)}
2020-07-16 15:27:02.263153+0800 多线程Demo[60201:18867775] 存10元,还剩140元 - {number = 5, name = (null)}
2020-07-16 15:27:02.263215+0800 多线程Demo[60201:18867775] 存10元,还剩150元 - {number = 5, name = (null)}
2020-07-16 15:27:02.263800+0800 多线程Demo[60201:18867774] 取20元,还剩130元 - {number = 6, name = (null)}
2020-07-16 15:27:02.263878+0800 多线程Demo[60201:18867774] 取20元,还剩110元 - {number = 6, name = (null)}
2020-07-16 15:27:02.263956+0800 多线程Demo[60201:18867774] 取20元,还剩90元 - {number = 6, name = (null)}
2020-07-16 15:27:02.264034+0800 多线程Demo[60201:18867774] 取20元,还剩70元 - {number = 6, name = (null)}
2020-07-16 15:27:02.264119+0800 多线程Demo[60201:18867774] 取20元,还剩50元 - {number = 6, name = (null)}

OSSpinLock已经被废弃掉了

'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from  instead
废弃原因

如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它就会处于忙等状态从而占用大量CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成无法释放lock,这就是优先级反转。这并不只是理论上的问题,开发者已经遇到很多次这个问题,于是苹果工程师停用了 OSSpinLock

废弃举例解释说明

开启了thread1(优先级最高)、thread2(优先级最低)这两条线程来执行相同任务,如果thread2先进来执行,就会先加锁准备执行任务。这时候thread1刚好进来了,发现线程已经被加过锁了那它只能忙等。忙等相当于是在while循环等待,这也是需要消耗CPU分配的资源的。由于thread1优先级最高肯定会分配到更多的资源,这样可能会造成thread2没有资源可被利用无法继续执行自己的代码,没法继续执行也就没办法解锁了,thread2的这把锁就无法释放了。thread2无法释放,thread1一直在忙等,最终就造成死锁。解决这种情况需要把忙等该为休眠,就是等待的这个线程让他休眠,而这种技术在os_unfair_lock中实现了。


方案二:os_unfair_lock互斥锁

os_unfair_lock用于取代不安全的OSSpinLock , 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

什么情况使用互斥锁比较划算?
  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

使用说明

需要导入头文件
#import 
初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
尝试加锁(如果不需要等待,就直接加锁,返回true。如果需要等待,就不加锁,返回false)
BOOL res = os_unfair_lock_trylock(&lock);
加锁与解锁
os_unfair_lock_lock(&lock);
os_unfair_lock_unlock(&lock);

解答本题

声明互斥锁属性
@property (nonatomic ,assign) os_unfair_lock moneyLock;
初始化互斥锁属性
self.moneyLock = OS_UNFAIR_LOCK_INIT;
存钱操作
- (void)saveMoney
{
    os_unfair_lock_lock(&_moneyLock);//加锁
    [super saveMoney];
    os_unfair_lock_unlock(&_moneyLock);//解锁
}
取钱操作
- (void)drawMoney
{
    os_unfair_lock_lock(&_moneyLock);//加锁
    [super drawMoney];
    os_unfair_lock_unlock(&_moneyLock);//解锁
}

输出结果显示同样实现了保证数据安全的效果。

2020-07-16 15:38:15.271635+0800 多线程Demo[60261:18875123] 存10元,还剩110元 - {number = 8, name = (null)}
2020-07-16 15:38:15.271781+0800 多线程Demo[60261:18875123] 存10元,还剩120元 - {number = 8, name = (null)}
2020-07-16 15:38:15.271862+0800 多线程Demo[60261:18875123] 存10元,还剩130元 - {number = 8, name = (null)}
2020-07-16 15:38:15.271968+0800 多线程Demo[60261:18875123] 存10元,还剩140元 - {number = 8, name = (null)}
2020-07-16 15:38:15.272050+0800 多线程Demo[60261:18875123] 存10元,还剩150元 - {number = 8, name = (null)}
2020-07-16 15:38:15.272124+0800 多线程Demo[60261:18875120] 取20元,还剩130元 - {number = 7, name = (null)}
2020-07-16 15:38:15.272196+0800 多线程Demo[60261:18875120] 取20元,还剩110元 - {number = 7, name = (null)}
2020-07-16 15:38:15.272301+0800 多线程Demo[60261:18875120] 取20元,还剩90元 - {number = 7, name = (null)}
2020-07-16 15:38:15.272825+0800 多线程Demo[60261:18875120] 取20元,还剩70元 - {number = 7, name = (null)}
2020-07-16 15:38:15.272945+0800 多线程Demo[60261:18875120] 取20元,还剩50元 - {number = 7, name = (null)}

方案三:pthread_mutex互斥锁

pthread_mutex叫做互斥锁,等待锁的线程会处于休眠状态。主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和尝试加锁pthread_mutex_trylock()三个操作。pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

使用说明

需要导入头文件
#import 
初始化
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, &attr);
销毁锁
// 销毁属性
pthread_mutexattr_destroy(&attr);
// delloc时候,需要销毁锁
pthread_mutex_destroy(&_moneyMutexLock);
锁的类型
#define PTHREAD_MUTEX_NORMAL        0   //一般的锁
#define PTHREAD_MUTEX_ERRORCHECK    1   //错误检查
#define PTHREAD_MUTEX_RECURSIVE     2  //递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL  //默认

当类型是PTHREAD_MUTEX_DEFAULT的时候,相当于NULL,上面的使用可以直接等价于:

pthread_mutex_init(mutex, NULL);

解答本题

声明互斥锁属性
@property (assign, nonatomic) pthread_mutex_t moneyMutexLock;
初始化互斥锁属性
[self initMutexLock:&_moneyMutexLock];

- (void)initMutexLock:(pthread_mutex_t *)mutex
{
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
}
delloc时候,需要销毁锁
- (void)dealloc
{
    pthread_mutex_destroy(&_moneyMutexLock);
}
存钱操作
- (void)saveMoney
{
    pthread_mutex_lock(&_moneyMutexLock);//加锁
    [super saveMoney];
    pthread_mutex_unlock(&_moneyMutexLock);//解锁
}
取钱操作
- (void)drawMoney
{
    pthread_mutex_lock(&_moneyMutexLock);//加锁
    [super drawMoney];
    pthread_mutex_unlock(&_moneyMutexLock);//解锁
}

输出结果显示同样实现了保证数据安全的效果。

2020-07-16 15:57:32.337886+0800 多线程Demo[60325:18886591] 存10元,还剩110元 - {number = 5, name = (null)}
2020-07-16 15:57:32.338195+0800 多线程Demo[60325:18886591] 存10元,还剩120元 - {number = 5, name = (null)}
2020-07-16 15:57:32.338277+0800 多线程Demo[60325:18886591] 存10元,还剩130元 - {number = 5, name = (null)}
2020-07-16 15:57:32.338374+0800 多线程Demo[60325:18886591] 存10元,还剩140元 - {number = 5, name = (null)}
2020-07-16 15:57:32.338459+0800 多线程Demo[60325:18886591] 存10元,还剩150元 - {number = 5, name = (null)}
2020-07-16 15:57:32.338550+0800 多线程Demo[60325:18886592] 取20元,还剩130元 - {number = 6, name = (null)}
2020-07-16 15:57:32.338627+0800 多线程Demo[60325:18886592] 取20元,还剩110元 - {number = 6, name = (null)}
2020-07-16 15:57:32.338700+0800 多线程Demo[60325:18886592] 取20元,还剩90元 - {number = 6, name = (null)}
2020-07-16 15:57:32.338788+0800 多线程Demo[60325:18886592] 取20元,还剩70元 - {number = 6, name = (null)}
2020-07-16 15:57:32.338852+0800 多线程Demo[60325:18886592] 取20元,还剩50元 - {number = 6, name = (null)}

买火车票问题

总共有100张火车票,开启两个线程,北京和上海两个窗口同时卖票,卖一张票就减去库存,使用锁,保证北京和上海卖票的库存是一致的。

声明属性
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
NSMutableArray *tickets;
生成100张票
for (int i = 0; i < 100; i++)
{
    [tickets addObject:[NSNumber numberWithInt:i]];
}
线程1:北京卖票窗口
// 创建线程1: 定义一个pthread_t类型变量
pthread_t thread1;
// 开启线程1: 执行任务
pthread_create(&thread1, NULL, run, NULL);
// 设置子线程1的状态设置为detached,该线程运行结束后会自动释放所有资源
pthread_detach(thread1);
线程2:上海卖票窗口
// 创建线程2: 定义一个pthread_t类型变量
pthread_t thread2;
// 开启线程2: 执行任务
pthread_create(&thread2, NULL, run, NULL);
// 设置子线程2的状态设置为detached,该线程运行结束后会自动释放所有资源
pthread_detach(thread2);
线程调用的卖票方法
void * run(void *param)
{
    while (true)
    {
        //锁门,执行任务
        pthread_mutex_lock(&mutex);
        
        if (tickets.count > 0)
        {
            NSLog(@"剩余票数%ld, 卖票窗口%@", tickets.count, [NSThread currentThread]);
            [tickets removeLastObject];
            [NSThread sleepForTimeInterval:0.2];
        }
        else
        {
            NSLog(@"票已经卖完了");

            //开门,让其他任务可以执行
            pthread_mutex_unlock(&mutex);

            break;
        }
        
        //开门,让其他任务可以执行
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

输出结果为:

2020-08-21 11:21:51.679196+0800 Demo[38057:3948746] 剩余票数100, 卖票窗口{number = 5, name = (null)}
2020-08-21 11:21:51.883712+0800 Demo[38057:3948746] 剩余票数99, 卖票窗口{number = 5, name = (null)}
2020-08-21 11:21:52.084056+0800 Demo[38057:3948746] 剩余票数98, 卖票窗口{number = 5, name = (null)}
.......
2020-08-21 11:22:07.731459+0800 Demo[38057:3948746] 剩余票数21, 卖票窗口{number = 5, name = (null)}
2020-08-21 11:22:07.935146+0800 Demo[38057:3948747] 剩余票数20, 卖票窗口{number = 6, name = (null)}
2020-08-21 11:22:08.138617+0800 Demo[38057:3948747] 剩余票数19, 卖票窗口{number = 6, name = (null)}
........
2020-08-21 11:22:11.585928+0800 Demo[38057:3948747] 剩余票数2, 卖票窗口{number = 6, name = (null)}
2020-08-21 11:22:11.790698+0800 Demo[38057:3948747] 剩余票数1, 卖票窗口{number = 6, name = (null)}
2020-08-21 11:22:11.996076+0800 Demo[38057:3948747] 票已经卖完了
2020-08-21 11:22:11.996341+0800 Demo[38057:3948746] 票已经卖完了

方案四:pthread_mutex递归锁

pthread_mutex除了有互斥锁,还有递归锁。递归锁允许同一个线程对一把锁进行重复加锁。递归锁除了解决重复上锁导致的死锁问题,也可以解决递归方法调用引起的多线程问题。

递归方法调用引起的多线程问题
methodA调用了methodB
- (void)methodA
{
    [_lock lock];
    [self methodB];
    [_lock unlock];
}
methodB调用了methodA
- (void)methodB
{
    [_lock lock];
    [self methodA];
    [_lock unlock];
}
递归锁解决递归方法调用引起的多线程问题
methodA
- (void)methodA
{
    [_recursiveLock lock];
    [self methodB];
    [_recursiveLock unlock];
}
methodB
- (void)methodB
{
    [_recursiveLock lock];
    // 操作逻辑
    [_recursiveLock unlock];
}

使用说明

需要导入头文件
#import 
初始化
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
销毁属性
pthread_mutexattr_destroy(&attr);
锁的类型
#define PTHREAD_MUTEX_NORMAL        0   //一般的锁
#define PTHREAD_MUTEX_ERRORCHECK    1   // 错误检查
#define PTHREAD_MUTEX_RECURSIVE     2  //递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL  //默认

Demo演示

声明递归锁属性
@property (assign, nonatomic) pthread_mutex_t MutexLock;
初始化递归锁属性
[self initMutexLock:&_MutexLock];

- (void)initMutexLock:(pthread_mutex_t *)mutex
{
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
}
delloc时候,需要销毁锁
- (void)dealloc
{
    pthread_mutex_destroy(&_MutexLock);
}
第一次进入递归方法直接加锁,第二次进来已经加锁了,还能递归继续加锁
- (void)otherTest
{
    pthread_mutex_lock(&_MutexLock);
    NSLog(@"加锁 %s",__func__);
    
    static int count = 0;
    if (count < 5)
    {
        count++;
        NSLog(@"count:%d", count);
        [self otherTest];
    }
    
    NSLog(@"解锁 %s",__func__);
    pthread_mutex_unlock(&_MutexLock);
}

由结果可知,连续加锁五次,是因为每次都递归加锁。所以解锁时候,也需要层层解锁。

2020-07-16 16:14:32.617478+0800 多线程Demo[60438:18898572] 加锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.617593+0800 多线程Demo[60438:18898572] count:1
2020-07-16 16:14:32.617671+0800 多线程Demo[60438:18898572] 加锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.617736+0800 多线程Demo[60438:18898572] count:2
2020-07-16 16:14:32.617797+0800 多线程Demo[60438:18898572] 加锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.617850+0800 多线程Demo[60438:18898572] count:3
2020-07-16 16:14:32.617911+0800 多线程Demo[60438:18898572] 加锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.617964+0800 多线程Demo[60438:18898572] count:4
2020-07-16 16:14:32.618020+0800 多线程Demo[60438:18898572] 加锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.618133+0800 多线程Demo[60438:18898572] count:5
2020-07-16 16:14:32.618302+0800 多线程Demo[60438:18898572] 加锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.618408+0800 多线程Demo[60438:18898572] 解锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.618530+0800 多线程Demo[60438:18898572] 解锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.618621+0800 多线程Demo[60438:18898572] 解锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.618758+0800 多线程Demo[60438:18898572] 解锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.618962+0800 多线程Demo[60438:18898572] 解锁 -[MutexRecursiveLockDemo otherTest]
2020-07-16 16:14:32.619100+0800 多线程Demo[60438:18898572] 解锁 -[MutexRecursiveLockDemo otherTest]

方案五:pthread_mutex条件锁

为了演示条件锁的作用,就用生产者消费者案例来展示效果。

使用说明

需要导入头文件
#import 
初始化条件
@property (assign, nonatomic) pthread_cond_t cond; 
pthread_cond_init(&_cond, NULL);
销毁条件
pthread_cond_destroy(&_cond);
数据为空就等待(进入休眠放开mutex锁,被唤醒后会再次对mutex加锁)
pthread_cond_wait(&_cond, &_mutex);
激活一个等待该条件的线程
pthread_cond_signal(&_cond);
激活所有等待该条件的线程
pthread_cond_broadcast(&_cond);

Demo演示

声明属性
@property (assign, nonatomic) pthread_mutex_t mutex; // 锁
@property (assign, nonatomic) pthread_cond_t cond; //条件
@property (strong, nonatomic) NSMutableArray *data; //数据源
初始化属性
// 初始化锁
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_mutex, &attr);

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

// 初始化数据源
self.data = [NSMutableArray array];
销毁锁和条件
- (void)dealloc
{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
}
开启两个线程分别执行生产者-消费者方法
[[[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start];
线程1为消费者:删除数组中的元素
- (void)remove
{
    pthread_mutex_lock(&_mutex);// 加锁
    
    // 数据为空就等待
    if (self.data.count == 0)
    {
        NSLog(@"remove方法:数据为空就等待(进入休眠放开mutex锁,被唤醒后会再次对mutex加锁)");
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    // 删除元素
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    pthread_mutex_unlock(&_mutex);// 解锁
}
线程2为生产者:往数组中添加元素
- (void)add
{
    pthread_mutex_lock(&_mutex);// 加锁
    sleep(1);
    
    // 添加元素
    [self.data addObject:@"xiejiapei"];
    NSLog(@"添加了元素");
    
    // 激活一个等待该条件的线程
    pthread_cond_signal(&_cond);
    
    // 激活所有等待该条件的线程
    // pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);// 解锁
}

由结果可知,打印完remove方法:数据为空就等待之后,等待了一秒钟,添加元素之后,放开锁,才去删除元素。

2020-07-16 16:41:02.729032+0800 多线程Demo[60545:18913091] remove方法:数据为空就等待(进入休眠,放开mutex锁,被唤醒后,会再次对mutex加锁)
2020-07-16 16:41:03.733362+0800 多线程Demo[60545:18913092] 添加了元素
2020-07-16 16:41:03.733847+0800 多线程Demo[60545:18913091] 删除了元素

方案六:NSLock锁

使用说明

NSLock遵循 NSLocking 协议,lock 方法是加锁,unlock 是解锁,tryLock是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

加锁与解锁
- (void)lock;
- (void)unlock;
尝试加锁,如果加锁失败,就返回NO,加锁成功就返回YES
- (BOOL)tryLock;
在给定的时间内尝试加锁,加锁成功就返回YES,如果过了时间还没加上锁,就返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;

解决本题

初始化锁
self.lock =[[NSLock alloc] init];
存钱操作
- (void)saveMoney
{
    [self.lock lock];// 加锁
    [super saveMoney];
    [self.lock unlock];// 解锁
}
取钱操作
- (void)drawMoney
{
    [self.lock lock];// 加锁
    [super drawMoney];
    [self.lock unlock];// 解锁
}

输出结果显示同样实现了效果,但是语法却非常简洁。

2020-07-16 17:00:50.814750+0800 多线程Demo[60639:18925432] 存10元,还剩110元 - {number = 6, name = (null)}
2020-07-16 17:00:50.814893+0800 多线程Demo[60639:18925432] 存10元,还剩120元 - {number = 6, name = (null)}
2020-07-16 17:00:50.814983+0800 多线程Demo[60639:18925432] 存10元,还剩130元 - {number = 6, name = (null)}
2020-07-16 17:00:50.815052+0800 多线程Demo[60639:18925432] 存10元,还剩140元 - {number = 6, name = (null)}
2020-07-16 17:00:50.815111+0800 多线程Demo[60639:18925432] 存10元,还剩150元 - {number = 6, name = (null)}
2020-07-16 17:00:50.815202+0800 多线程Demo[60639:18925433] 取20元,还剩130元 - {number = 5, name = (null)}
2020-07-16 17:00:50.815270+0800 多线程Demo[60639:18925433] 取20元,还剩110元 - {number = 5, name = (null)}
2020-07-16 17:00:50.815340+0800 多线程Demo[60639:18925433] 取20元,还剩90元 - {number = 5, name = (null)}
2020-07-16 17:00:50.815682+0800 多线程Demo[60639:18925433] 取20元,还剩70元 - {number = 5, name = (null)}
2020-07-16 17:00:50.815756+0800 多线程Demo[60639:18925433] 取20元,还剩50元 - {number = 5, name = (null)}

买火车票问题

声明属性
var tickets: [Int] = [Int]()
let lock = NSLock.init()
生成100张票
for i in 0..<100
{
    tickets.append(i)
}
卖票窗口
let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)

//北京卖票窗口
que.async
{
    self.saleTicket()
}

//上海卖票窗口
que.async
{
    self.saleTicket()
}
实现卖票方法
func saleTicket()
{
    while true
    {
        //关门,执行任务
        lock.lock()
        
        if tickets.count > 0
        {
            print("剩余票数", tickets.count, "卖票窗口", Thread.current)
            tickets.removeLast()
            Thread.sleep(forTimeInterval: 0.2)
        }
        else
        {
            print("票已经卖完了")
            
            //开门,让其他任务可以执行
            lock.unlock()
            
            break
        }
        
        //开门,让其他任务可以执行
        lock.unlock()
    }
}

输出结果为:

剩余票数 100 卖票窗口 {number = 6, name = (null)}
剩余票数 99 卖票窗口 {number = 7, name = (null)}
剩余票数 98 卖票窗口 {number = 6, name = (null)}
剩余票数 97 卖票窗口 {number = 7, name = (null)}
剩余票数 96 卖票窗口 {number = 6, name = (null)}
剩余票数 95 卖票窗口 {number = 7, name = (null)}
.................
剩余票数 4 卖票窗口 {number = 6, name = (null)}
剩余票数 3 卖票窗口 {number = 7, name = (null)}
剩余票数 2 卖票窗口 {number = 6, name = (null)}
剩余票数 1 卖票窗口 {number = 7, name = (null)}
票已经卖完了
票已经卖完了

方案七:NSRecursiveLock锁

  • lock(或者lockBeforeDate:)必须与unlock方法成对出现,如果多次lock会造成死锁
  • 只有在unlock之后才能再次进行lock操作
  • 必须在同一线程中进行加锁解锁操作

例如下面的操作如果使用NSLock就会造成死锁:

NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value:%d", value);
            RecursiveBlock(value - 1);
        }
        [lock unlock];
    };
    RecursiveBlock(2);
});

这是因为第一次加锁之后,还未执行解锁就进入了递归的下一层,而再次请求加锁,该操作阻塞了当前线程,导致解锁的操作永远不被执行从而形成死锁。为了解决这一问题,iOS中出现了另一种锁——NSRecursiveLock

NSRecursiveLock是递归锁,与NSLock不同的是,NSRecursiveLock允许在同一线程中重复加锁,NSRecursiveLock会记录加锁与解锁的次数,当两者平衡时才会释放锁,其他的线程才可以再对同一资源进行加锁。

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value:%d", value);
            RecursiveBlock(value - 1);
        }
        [lock unlock];
    };
    RecursiveBlock(2);
});

方案八:NSCondition条件锁

使用说明

NSCondition是对mutexcond的封装。NSCondition是一个比较特殊的锁,它可以实现两个不同线程之间调度,它具有两个功能:锁定资源和线程检查器。当条件不满足时会阻塞当前线程等待另一线程发送信号使得条件满足时,才会激活线程继续执行操作。

等待
- (void)wait;
在给定时间之前等待
- (BOOL)waitUntilDate:(NSDate *)limit;
激活一个等待该条件的线程
- (void)signal;  
激活所有等待该条件的线程
- (void)broadcast; 

Demo演示

初始化条件锁
// 初始化条件锁
self.condition = [[NSCondition alloc] init];
// 初始化数据源
self.data = [NSMutableArray array];
开启两个线程分别执行生产者-消费者方法
[[[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start];
线程1:删除数组中的元素
- (void)remove
{
    [self.condition lock];// 加锁
    
    // 数据为空就等待
    if (self.data.count == 0)
    {
        NSLog(@"remove方法:数据为空就等待(进入休眠,放开mutex锁,被唤醒后,会再次对mutex加锁)");
        [self.condition wait];
    }
    
    // 删除元素
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    [self.condition unlock];// 解锁
}
线程2:往数组中添加元素
- (void)add
{
    [self.condition lock];// 加锁
    sleep(1);
    
    // 添加元素
    [self.data addObject:@"xiejiapei"];
    NSLog(@"添加了元素");
    
    // 激活一个等待该条件的线程
    [self.condition signal];
    
    // 激活所有等待该条件的线程
    // [self.condition broadcast];
    
    [self.condition unlock];// 解锁
}

输出结果为:

2020-07-16 17:16:14.141622+0800 多线程Demo[60770:18938478] remove方法:数据为空就等待(进入休眠,放开mutex锁,被唤醒后,会再次对mutex加锁)
2020-07-16 17:16:18.849376+0800 多线程Demo[60770:18938479] 添加了元素
2020-07-16 17:16:18.849668+0800 多线程Demo[60770:18938478] 删除了元素

方案九:NSConditionLock

使用说明

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。NSConditionLockNSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法。

NSConditionLock 可以称为条件锁,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition:并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值。

- (void)lockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

Demo演示

主线程中:

NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];

线程1:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lockWhenCondition:1];
    NSLog(@"线程1");
    sleep(2);
    [lock unlock];
});

线程2:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);//以保证让线程2的代码后执行
    if ([lock tryLockWhenCondition:0]) {
        NSLog(@"线程2");
        [lock unlockWithCondition:2];
        NSLog(@"线程2解锁成功");
    } else {
        NSLog(@"线程2尝试加锁失败");
    }
});

线程3:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);//以保证让线程2的代码后执行
    if ([lock tryLockWhenCondition:2]) {
        NSLog(@"线程3");
        [lock unlock];
        NSLog(@"线程3解锁成功");
    } else {
        NSLog(@"线程3尝试加锁失败");
    }
});

线程4:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(3);//以保证让线程2的代码后执行
    if ([lock tryLockWhenCondition:2]) {
        NSLog(@"线程4");
        [lock unlockWithCondition:1];
        NSLog(@"线程4解锁成功");
    } else {
        NSLog(@"线程4尝试加锁失败");
    }
});

输出结果:

线程2
线程2解锁成功
线程3
程3解锁成功
线程4
线程4解锁成功
线程1

上述代码中,初始化的条件锁条件为condition=0,只有线程2满足condition=0的条件可以加锁,而线程1不满足加锁条件,会阻塞线程;线程3需要休眠2s之后执行,线程4需要在休眠3s之后执行,而且线程3,线程4也不满足加锁条件。当线程2执行解锁操作之后condition=2,这时线程3,线程4都满足条件,而且线程解锁之后condition=2,所以线程3早于线程4执行,当线程4执行结束以后,设置condition=1,满足线程1加锁条件,线程1不再堵塞线程开始加锁,执行线程中的后续操作。从中也可以看出,合理地使用NSConditionLock条件锁可以实现任务之间的依赖。


方案十:dispatch_semaphore信号量

原理解释

semaphore叫做”信号量”。信号量的初始值,可以用来控制线程并发访问的最大数量。信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1;如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。简单地说就是洗手间只有一个坑位,外面进来一个人把门关上,其他人排队,这个人把门打开出去之后,可以再进来一个人。

通常等待信号量和发送信号量的函数是成对出现的。并发执行任务的时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务……如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。


解决本题

初始化信号量
// 金额
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;

// 初始化先设置1,然后每次取钱存钱之前都调用dispatch_semaphore_wait,取钱存钱之后都调用dispatch_semaphore_signal
self.moneySemaphore = dispatch_semaphore_create(1);
存钱操作
- (void)saveMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);// 等待信号量
    
    [super saveMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);// 发送信号量
}
取钱操作
- (void)drawMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);// 等待信号量
    
    [super drawMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);// 发送信号量
}

输出结果为:

2020-07-16 17:47:30.413564+0800 多线程Demo[60921:18957636] 存10元,还剩110元 - {number = 4, name = (null)}
2020-07-16 17:47:30.413720+0800 多线程Demo[60921:18957635] 取20元,还剩90元 - {number = 5, name = (null)}
2020-07-16 17:47:30.413849+0800 多线程Demo[60921:18957636] 存10元,还剩100元 - {number = 4, name = (null)}
2020-07-16 17:47:30.413995+0800 多线程Demo[60921:18957635] 取20元,还剩80元 - {number = 5, name = (null)}
2020-07-16 17:47:30.414084+0800 多线程Demo[60921:18957636] 存10元,还剩90元 - {number = 4, name = (null)}
2020-07-16 17:47:30.414168+0800 多线程Demo[60921:18957635] 取20元,还剩70元 - {number = 5, name = (null)}
2020-07-16 17:47:30.414252+0800 多线程Demo[60921:18957636] 存10元,还剩80元 - {number = 4, name = (null)}
2020-07-16 17:47:30.414349+0800 多线程Demo[60921:18957635] 取20元,还剩60元 - {number = 5, name = (null)}
2020-07-16 17:47:30.414720+0800 多线程Demo[60921:18957636] 存10元,还剩70元 - {number = 4, name = (null)}
2020-07-16 17:47:30.414797+0800 多线程Demo[60921:18957635] 取20元,还剩50元 - {number = 5, name = (null)}

火车票问题

卖票窗口同前
var tickets: [Int] = [Int]()

let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)

//生成100张票
for i in 0..<100
{
    tickets.append(i)
}

//北京卖票窗口
que.async
{
    self.saleTicket()
}

//上海卖票窗口
que.async
{
    self.saleTicket()
}
存在一个坑位
let semp = DispatchSemaphore.init(value: 1)
实现卖票方法
func saleTicket()
{
    while true
    {
        //占坑,坑位减一
        semp.wait()
        
        if tickets.count > 0
        {
            print("剩余票数", tickets.count, "卖票窗口", Thread.current)
            tickets.removeLast()
            Thread.sleep(forTimeInterval: 0.2)
        }
        else
        {
            print("票已经卖完了")
            
            //释放占坑,坑位加一
            semp.signal()
            
            break
        }
        
        //释放坑位,坑位加一
        semp.signal()
    }
}

输出结果是:

剩余票数 100 卖票窗口 {number = 6, name = (null)}
剩余票数 99 卖票窗口 {number = 4, name = (null)}
剩余票数 98 卖票窗口 {number = 6, name = (null)}
剩余票数 97 卖票窗口 {number = 4, name = (null)}
剩余票数 96 卖票窗口 {number = 6, name = (null)}
剩余票数 95 卖票窗口 {number = 4, name = (null)}
.................
剩余票数 4 卖票窗口 {number = 6, name = (null)}
剩余票数 3 卖票窗口 {number = 4, name = (null)}
剩余票数 2 卖票窗口 {number = 6, name = (null)}
剩余票数 1 卖票窗口 {number = 4, name = (null)}
票已经卖完了
票已经卖完了

信号量还可以控制线程数量

初始化的时候可以设置多条线程。针对火车票问题,在不使用信号量的情况下,运行一段时间就会崩溃,这是由于多线程同时操作tickets票池的removeLast去库存的方法引起的,所以我们需要考虑线程安全问题。

创建信号量,设置每次最多三条线程执行
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
self.semaphore = dispatch_semaphore_create(3);
创建多条线程
for (int i = 0; i < 10; I++)
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
线程调用的方法

如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码。如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码。

- (void)test
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test方法 - %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

由结果可知,每次最多三条线程执行。

2020-07-16 17:56:26.732081+0800 多线程Demo[60970:18963768] test方法 - {number = 7, name = (null)}
2020-07-16 17:56:26.732141+0800 多线程Demo[60970:18963766] test方法 - {number = 5, name = (null)}
2020-07-16 17:56:26.732125+0800 多线程Demo[60970:18963767] test方法 - {number = 6, name = (null)}

2020-07-16 17:56:28.732922+0800 多线程Demo[60970:18963770] test方法 - {number = 9, name = (null)}
2020-07-16 17:56:28.732922+0800 多线程Demo[60970:18963769] test方法 - {number = 8, name = (null)}
2020-07-16 17:56:28.732922+0800 多线程Demo[60970:18963771] test方法 - {number = 10, name = (null)}

2020-07-16 17:56:30.738444+0800 多线程Demo[60970:18963772] test方法 - {number = 11, name = (null)}
2020-07-16 17:56:30.738491+0800 多线程Demo[60970:18963774] test方法 - {number = 13, name = (null)}
2020-07-16 17:56:30.738492+0800 多线程Demo[60970:18963773] test方法 - {number = 12, name = (null)}

2020-07-16 17:56:32.741099+0800 多线程Demo[60970:18963775] test方法 - {number = 14, name = (null)}

初始化创建了一个信号量,进入线程1时,由于信号量不为0,所以线程1继续执行代码,同时信号量减1,此时信号量变为0。然后线程1休眠5s,在这期间线程2开始执行,由于此时信号量为0,所以线程2被阻塞,等待信号量5s之后线程1激活,执行任务,发送信号使信号量为1,此时线程2获得信号,激活线程开始执行任务,同时将信号量减变为0。线程2执行结束之后发送信号,使信号量重新变为1。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(semaphore, overTime);
    NSLog(@"线程1开始");
    sleep(5);
    NSLog(@"线程1结束");
    dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    dispatch_semaphore_wait(semaphore, overTime);
    NSLog(@"线程2开始");
    dispatch_semaphore_signal(semaphore);
});

输出结果:

线程1开始
线程1结束
线程2开始

方案十一:@synchronized

简单介绍

@synchronized是对mutex(pthread_mutex)同步锁的封装,需要确保在加锁期间,参数不能为空,一般会选用线程所在的控制器。在创建单例对象的时候使用,保证在多线程下创建的对象是唯一的,是加锁技术中使用起来最简单的方案,但性能差。

Objective-C中,我们可以用@synchronized关键字来修饰一个对象,并为其自动加上和解除互斥锁。但是在Swift中,没有与之对应的方法,即@synchronizedSwift中已经(或者是暂时)不存在了。


解决问题

存钱操作
- (void)saveMoney
{
    @synchronized (self) {
        [super saveMoney];
    }
}
取钱操作
- (void)drawMoney
{
    @synchronized (self) {
        [super drawMoney];
    }
}

可见,多线程的数据没有发生错乱。

2020-07-16 18:04:00.026101+0800 多线程Demo[61052:18971471] 存10元,还剩110元 - {number = 5, name = (null)}
2020-07-16 18:04:00.026252+0800 多线程Demo[61052:18971471] 存10元,还剩120元 - {number = 5, name = (null)}
2020-07-16 18:04:00.026354+0800 多线程Demo[61052:18971471] 存10元,还剩130元 - {number = 5, name = (null)}
2020-07-16 18:04:00.026429+0800 多线程Demo[61052:18971471] 存10元,还剩140元 - {number = 5, name = (null)}
2020-07-16 18:04:00.026489+0800 多线程Demo[61052:18971471] 存10元,还剩150元 - {number = 5, name = (null)}
2020-07-16 18:04:00.026572+0800 多线程Demo[61052:18971468] 取20元,还剩130元 - {number = 4, name = (null)}
2020-07-16 18:04:00.026651+0800 多线程Demo[61052:18971468] 取20元,还剩110元 - {number = 4, name = (null)}
2020-07-16 18:04:00.026739+0800 多线程Demo[61052:18971468] 取20元,还剩90元 - {number = 4, name = (null)}
2020-07-16 18:04:00.026905+0800 多线程Demo[61052:18971468] 取20元,还剩70元 - {number = 4, name = (null)}
2020-07-16 18:04:00.027118+0800 多线程Demo[61052:18971468] 取20元,还剩50元 - {number = 4, name = (null)}

锁是如何与传入 @synchronized 的对象关联上的

@synchronized block 在被保护的代码上暗中添加了一个异常处理,为的是同步某对象时如若抛出异常,锁会被释放掉。@synchronized block 会变成 objc_sync_enterobjc_sync_exit 的成对调用,我们不知道这些函数是干啥的,但基于这些事实我们可以认为编译器将会转化为这样的代码。

@synchronized(obj) {
    // do work
}
@try {
    objc_sync_enter(obj);
    // do work
} @finally {
    objc_sync_exit(obj);    
}

sychronized 的每个对象,runtime 都会为其分配一个锁并存储在哈希表中。即@synchronized(obj)内部会生成obj对应的锁,然后进行加锁、解锁操作。拿到需要加锁的对象,传入对象从散列表中取出对应的锁。

static StripeMap sDataLists// 散列表(哈希)
SyncData *data = sDataLists[obj].data
data->mutex.lock()

方案十二:atomic

修饰属性的关键字,对被修饰的对象进行原子操作,但只保证赋值和读取线程安全,不保证使用安全。

@property (atomic) NSMutableArray *mutableArray;
赋值和读取线程安全
self.mutableArray = [NSMutableArray array];
多线程下使用的话就比较危险
[self.mutableArray addObject:obj];

方案十三:pthread_rwlock读写锁

多读单写:读操作可并发,写操作是互斥的。

使用说明

需要导入头文件
#import 
初始化锁
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);

Demo演示

全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
初始化锁
@property (assign, nonatomic) pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);
销毁锁
- (void)dealloc
{
    pthread_rwlock_destroy(&_lock);
}
多读
- (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);
}
并发执行写后读
for (int i = 0; i < 3; i++)
{
    dispatch_async(queue, ^{
        [self write];
        [self read];
    });
}
并发执行写
for (int i = 0; i < 3; i++)
{
    dispatch_async(queue, ^{
        [self write];
    });
}

由结果可知,打印write的时候,方法每次都是一个一个执行的,而read是可以同时执行的,也就是说达到了多读单写的功能,所以被称为读写锁。

2020-07-17 09:46:26.743011+0800 多线程Demo[61491:19029089] -[pthread_rwlockDemo write]
2020-07-17 09:46:27.746012+0800 多线程Demo[61491:19029088] -[pthread_rwlockDemo write]
2020-07-17 09:46:28.749845+0800 多线程Demo[61491:19029087] -[pthread_rwlockDemo write]
2020-07-17 09:46:29.755014+0800 多线程Demo[61491:19029093] -[pthread_rwlockDemo write]
2020-07-17 09:46:30.760453+0800 多线程Demo[61491:19029095] -[pthread_rwlockDemo write]
2020-07-17 09:46:31.761375+0800 多线程Demo[61491:19029090] -[pthread_rwlockDemo write]
2020-07-17 09:46:32.763769+0800 多线程Demo[61491:19029089] -[pthread_rwlockDemo read]
2020-07-17 09:46:32.763774+0800 多线程Demo[61491:19029087] -[pthread_rwlockDemo read]
2020-07-17 09:46:32.763774+0800 多线程Demo[61491:19029088] -[pthread_rwlockDemo read]

方案十四:dispatch_barrier_async异步栅栏

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。注意,这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果。

读写方法
- (void)read
{
    sleep(1);
    NSLog(@"read");
}

- (void)write
{
    sleep(1);
    NSLog(@"write");
}
初始化队列
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
异步执行调用读写方法
for (int i = 0; i < 3; i++)
{
    // 读
    dispatch_async(self.queue, ^{
        [self read];
    });
    
    // 写
    dispatch_barrier_async(self.queue, ^{
        [self write];
    });
    
     // 读
    dispatch_async(self.queue, ^{
        [self read];
    });
    
     // 读
    dispatch_async(self.queue, ^{
        [self read];
    });
}

输出结果显示遇到写的操作,就会把其他读或者写都会暂停,也就是说起到了栅栏的作用。

2020-07-17 09:54:38.183629+0800 多线程Demo[61565:19036669] read
2020-07-17 09:54:39.184246+0800 多线程Demo[61565:19036669] write
2020-07-17 09:54:40.186081+0800 多线程Demo[61565:19036669] read
2020-07-17 09:54:40.186082+0800 多线程Demo[61565:19036668] read
2020-07-17 09:54:40.186085+0800 多线程Demo[61565:19036667] read
2020-07-17 09:54:41.189304+0800 多线程Demo[61565:19036669] write
2020-07-17 09:54:42.194755+0800 多线程Demo[61565:19036669] read
2020-07-17 09:54:42.194763+0800 多线程Demo[61565:19036667] read
2020-07-17 09:54:42.194763+0800 多线程Demo[61565:19036668] read
2020-07-17 09:54:43.200178+0800 多线程Demo[61565:19036667] write
2020-07-17 09:54:44.201968+0800 多线程Demo[61565:19036667] read
2020-07-17 09:54:44.202016+0800 多线程Demo[61565:19036668] read

火车票问题

var tickets: [Int] = [Int]()

let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)

//生成100张票
for i in 0..<100
{
    tickets.append(i)
}
卖票窗口
for _ in 0..<51
{
    //北京卖票窗口
    que.async {
        self.saleTicket()
    }
    
    //GCD 栅栏方法,同步去库存
    que.async(flags: .barrier) {
        if self.tickets.count > 0
        {
            self.tickets.removeLast()
        }
    }
    
    //上海卖票窗口
    que.async {
        self.saleTicket()
    }
    
    //GCD 栅栏方法,同步去库存
    que.async(flags: .barrier) {
        if self.tickets.count > 0
        {
            self.tickets.removeLast()
        }
    }
}
卖票方法
func saleTicket()
{
    if tickets.count > 0
    {
        print("剩余票数", tickets.count, "卖票窗口", Thread.current)
        Thread.sleep(forTimeInterval: 0.2)
    }
    else
    {
        print("票已经卖完了")
    }
}

输出结果为:

剩余票数 100 卖票窗口 {number = 3, name = (null)}
剩余票数 99 卖票窗口 {number = 3, name = (null)}
剩余票数 98 卖票窗口 {number = 3, name = (null)}
剩余票数 97 卖票窗口 {number = 3, name = (null)}
.................
剩余票数 59 卖票窗口 {number = 6, name = (null)}
剩余票数 58 卖票窗口 {number = 6, name = (null)}
剩余票数 57 卖票窗口 {number = 6, name = (null)}
剩余票数 56 卖票窗口 {number = 6, name = (null)}
.................
剩余票数 3 卖票窗口 {number = 3, name = (null)}
剩余票数 2 卖票窗口 {number = 3, name = (null)}
剩余票数 1 卖票窗口 {number = 3, name = (null)}
票已经卖完了
票已经卖完了

方案十五:dispatch_group_t调度组

使用说明

创建调度组
dispatch_group_t group = dispatch_group_create();
创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
调度组监听队列,标记开始本次执行
dispatch_group_enter(group);
标记本次请求完成
dispatch_group_leave(group);
调度组都完成了
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //执行刷新UI等操作
});

Demo演示

下载任务1
- (void)downLoadImage1 
{
    sleep(1);
    NSLog(@"%s--%@",__func__,[NSThread currentThread]);
}
下载任务2
- (void)downLoadImage2 
{
     sleep(2);
    NSLog(@"%s--%@",__func__,[NSThread currentThread]);
}
刷新UI
- (void)reloadUI
{
    NSLog(@"%s--%@",__func__,[NSThread currentThread]);
}
创建调度组和队列
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
调度组监听队列,标记开始本次执行,下载图片1
dispatch_group_enter(group);
dispatch_async(queue, ^{
   [self downLoadImage1];
   //标记本次请求完成
   dispatch_group_leave(group);
});
调度组监听队列,标记开始本次执行,下载图片2
dispatch_group_enter(group);
dispatch_async(queue, ^{
    [self downLoadImage2];
    //标记本次请求完成
    dispatch_group_leave(group);
});
调度组都完成了,下载完图片1和2后再进行刷新UI
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
     [self reloadUI];
});

等两个模拟下载图片的操作都完成后,才回到主线程刷新UI。

2020-07-17 10:07:36.550557+0800 多线程Demo[61647:19046062] 下载任务1  -[dispatch_group_tDemo downLoadImage1]--{number = 4, name = (null)}
2020-07-17 10:07:37.553169+0800 多线程Demo[61647:19046060] 下载任务2  -[dispatch_group_tDemo downLoadImage2]--{number = 7, name = (null)}
2020-07-17 10:07:37.553401+0800 多线程Demo[61647:19045549] 刷新UI  -[dispatch_group_tDemo reloadUI]--{number = 1, name = main}

方案十六:addDependency操作依赖

NSOperationNSOperationQueue最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。

var tickets: [Int] = [Int]()

let que = OperationQueue.init()//并发队列
que.maxConcurrentOperationCount = 1

//生成100张票
for i in 0..<100
{
    tickets.append(i)
}
北京卖票窗口
//同步方式去库存取票
let sync1 = BlockOperation.init(block: {
    if self.tickets.count > 0
    {
        self.tickets.removeLast()
    }
})

//北京卖票窗口
let bj = BlockOperation.init(block: {
    self.saleTicket()
})

//卖票操作依赖于取票操作
bj.addDependency(sync1)
上海卖票窗口
let sync2 = BlockOperation.init(block: {
    if self.tickets.count > 0
    {
        self.tickets.removeLast()
    }
})

let sh = BlockOperation.init(block: {
    self.saleTicket()
})
sh.addDependency(sync2)
将操作添加进队列
que.addOperation(sync1)
que.addOperation(bj)
que.addOperation(sync2)
que.addOperation(sh)

输出结果为:

剩余票数 99 卖票窗口 {number = 4, name = (null)}
剩余票数 98 卖票窗口 {number = 5, name = (null)}
剩余票数 97 卖票窗口 {number = 5, name = (null)}
剩余票数 96 卖票窗口 {number = 5, name = (null)
.................
剩余票数 54 卖票窗口 {number = 4, name = (null)}
剩余票数 53 卖票窗口 {number = 4, name = (null)}
剩余票数 52 卖票窗口 {number = 4, name = (null)}
.................
剩余票数 2 卖票窗口 {number = 4, name = (null)}
剩余票数 1 卖票窗口 {number = 4, name = (null)}
票已经卖完了
票已经卖完了
票已经卖完了

Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

参考文章

你可能感兴趣的:(iOS 基础原理:多线程的锁)