原文连接html
以前写过一篇线程安全,简单介绍了保护数据安全的多种方式,以及其中一部分方式的原理。基于此基础,本文将介绍如何避免锁的性能浪费,以及如何实现无锁安全结构缓存
为了不多个线程对数据的破坏,在使用锁保障线程安全的状况下,存在几个影响锁性能的重要因素:安全
若是可以减小这些因素的损耗,就能有效的提升锁的性能多线程
一般来讲,当一个线程获取锁失败后,会被添加到一个等待队列
的末尾,而后休眠。直到锁被释放后,依次唤醒访问临界资源。休眠时会发生线程的上下文切换,当前线程的寄存器信息会被保存到磁盘上,考虑到这些状况,能作的有两点:函数
自旋锁
自旋锁
采用死循环等待锁释放来替代线程的休眠和唤醒,避免了上下文切换的代价。当临界的代码足够短,使用自旋锁
对于性能的提高是立竿见影的工具
粒度是指颗粒的大小性能
对于锁来讲,锁的粒度大小取决于锁保护的临界区的大小。锁的粒度越小,临界区的操做就越小,反之亦然,因为临界区执行代码时间致使的损耗问题我称做粒度锁问题
。举个例子,假如某个修改元素的方法包括三个操做:查找缓存
->查找容器
->修改元素
:atom
- (void)modifyName: (NSString *)name withId: (id)usrId {
lock();
User *usr = [self.caches findUsr: usrId];
if (!usr) {
usr = [self.collection findUsr: usrId];
}
if (!usr) {
unlock();
return;
}
usr.name = name;
unlock();
}
复制代码
实际上整个修改操做之中,只有最后的修改元素
存在安全问题须要加锁,而且若是加锁的临界区代码执行时间过长可能致使有更多的线程须要等待锁,增长了锁使用的损耗。所以加锁代码应当尽可能的短小简单:spa
- (void)modifyName: (NSString *)name withId: (id)usrId {
User *usr = [self.caches findUsr: usrId];
if (!usr) {
usr = [self.collection findUsr: usrId];
}
if (!usr) {
return;
}
lock();
usr.name = name;
unlock();
}
复制代码
将大段代码
改成小段代码
加锁是一种常见的减小锁性能损耗的作法,所以再也不多提。但接下来要说的是另外一种常见但由于锁粒度形成损耗的问题:设想一下这个场景,在改良后的代码使用中,线程A
对第三个元素进行修改,线程B
对第四个元素进行修改:线程
在两个线程修改user
的过程当中,实际上双方的操做是不冲突,可是线程B
必须等待A
完成修改工做,形成这个现象的缘由是虽然看起来是对usr.name
进行了加锁,但其实是锁住了collection
或caches
的操做,因此避免这种隐藏的粒度锁问题的方案是以容器元素单位构建锁:包括全局锁
和独立锁
两种:
全局锁
构建一个global lock
的集合,用hash
的手段为修改元素对应一个锁:
id l = [SLGlobalLocks getLock: hash(usr)];
lock(l);
usr.name = name;
unlock(l);
复制代码
使用全局锁
的好处包括能够在设计上能够懒加载生成锁,限制bucketCount
来避免建立过多的锁实例,基于hash
的映射关系,锁能够被多个对象获取,提升复用率。但缺点也是明显的,匹配锁的额外损耗,hash
映射可能致使多个锁围观一个锁工做等。事实上@synchronized
就是已存在的全局锁
方案
独立锁
这个方案的名字是随便起的,从设计上要求容器的每一个元素拥有本身的独立锁:
@interface SLLockItem: NSObject
@property (nonatomic, strong) id item;
@property (nonatomic, strong) NSLock *lock;
@end
SLLockItem *item = [self.collection findUser: usrId];
[item.lock lock];
User *usr = item.item;
usr.name = name;
[item.lock unlock];
复制代码
独立锁
保证了不一样元素之间的加锁是绝对独立的,粒度彻底可控,但锁难以复用,容器越长,须要建立的锁实例就越多也是致命的缺点。而且在链式结构中,增删
操做的加锁是要和previous
节点的修改操做发生竞争的,在实现上更加麻烦
无锁化
是彻底抛弃加锁工具,实现多线程访问安全的方案。无锁化
须要去分解操做流程,找出真正须要保证安全的操做,举个例子:存在链表A -> B -> C
,删除B
的代码以下:
Node *cur = list;
while (cur.next != B && cur.next != nil) {
cur = cur.next;
}
if (cur.next == nil) {
return;
}
cur.next = cur.next.next;
复制代码
只要A.next
的修改是不受多线程干扰的,那么就能保证删除元素的安全
compare and swap
是计算机硬件提供的一种原子操做,它会比较两个值是否相等,而后决定下一步的执行指令,iOS
对于这种操做的支持须要导入<libkern/OSAtomic.h>
文件。
bool OSAtomicCompareAndSwapPtrBarrier( void *oldVal, void *newVal, void * volatile *theVal )
复制代码
函数会在oldVal
和theVal
相同的状况下将oldVal
存储的值修改成newVal
,所以删除B
的代码只要保证在A.next
变成nil
以前是一致的就能够保证安全:
Node *cur = list;
while (cur.next != B && cur.next != nil) {
cur = cur.next;
}
if (cur.next == nil) {
return;
}
while (true) {
void *oldValue = cur.next;
if (OSAtomicCompareAndSwapPtrBarrier(oldValue, nil, &cur.next)) {
break;
} else {
continue;
}
}
复制代码
基于上面删除B
的例子,同一时间存在其余线程在A
节点后追加D
节点:
因为CPU
可能在任务执行过程当中切换线程,若是D
节点的修改工做正好在删除任务的中间完成,最终可能致使的是D
节点的误删:
因此上面的CAS
还须要考虑A.next
是否发生了改变:
Node *cur = list;
while (cur.next.value != B && cur.next != nil) {
cur = cur.next;
}
if (cur.next == nil) {
return;
}
while (true) {
void *oldValue = cur.next;
// next已经再也不指向B
if (!OSAtomicCompareAndSwapPtrBarrier(B, B, &cur.next.value)) {
break;
}
if (OSAtomicCompareAndSwapPtrBarrier(oldValue, nil, &cur.next)) {
break;
} else {
continue;
}
}
复制代码
OSAtomicCompareAndSwapPtrBarrier
除了保证修改操做的原子性,还带有barrier
的做用。在如今CPU
的设计上,会考虑打乱代码的执行顺序来获取更快的执行速度,好比说:
/// 线程1执行
A.next = D;
D.next = C;
/// 线程2执行
while (D.next != C) {}
NSLog(@"%@", A.next);
复制代码
因为执行顺序会被打乱,执行的时候变成:
D.next = C;
A.next = D;
while (D.next != C) {}
NSLog(@"%@", A.next);
复制代码
输出的结果可能并非D
,而只要在D.next = C
前面插入一句barrier
函数,就能保证在这句代码前的指令不会被打乱执行,保证正确的代码顺序
很方,这个月想了不少想写的内容,而后发现别人都写过,尴尬的一笔。果真仍是本身太鶸了,最后随便赶工了一篇全是水货的文章,瑟瑟发抖