线程安全(二)

原文连接html

以前写过一篇线程安全,简单介绍了保护数据安全的多种方式,以及其中一部分方式的原理。基于此基础,本文将介绍如何避免锁的性能浪费,以及如何实现无锁安全结构缓存

避免锁的性能浪费

为了不多个线程对数据的破坏,在使用锁保障线程安全的状况下,存在几个影响锁性能的重要因素:安全

  • 上下文切换
  • 临界区资源耗时

若是可以减小这些因素的损耗,就能有效的提升锁的性能多线程

自旋锁

一般来讲,当一个线程获取锁失败后,会被添加到一个等待队列的末尾,而后休眠。直到锁被释放后,依次唤醒访问临界资源。休眠时会发生线程的上下文切换,当前线程的寄存器信息会被保存到磁盘上,考虑到这些状况,能作的有两点:函数

  1. 换一个更快的磁盘
  2. 改用自旋锁

自旋锁采用死循环等待锁释放来替代线程的休眠和唤醒,避免了上下文切换的代价。当临界的代码足够短,使用自旋锁对于性能的提高是立竿见影的工具

锁粒度

粒度是指颗粒的大小性能

对于锁来讲,锁的粒度大小取决于锁保护的临界区的大小。锁的粒度越小,临界区的操做就越小,反之亦然,因为临界区执行代码时间致使的损耗问题我称做粒度锁问题。举个例子,假如某个修改元素的方法包括三个操做:查找缓存->查找容器->修改元素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进行了加锁,但其实是锁住了collectioncaches的操做,因此避免这种隐藏的粒度锁问题的方案是以容器元素单位构建锁:包括全局锁独立锁两种:

  • 全局锁

    构建一个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的修改是不受多线程干扰的,那么就能保证删除元素的安全

CAS

compare and swap是计算机硬件提供的一种原子操做,它会比较两个值是否相等,而后决定下一步的执行指令,iOS对于这种操做的支持须要导入<libkern/OSAtomic.h>文件。

bool	OSAtomicCompareAndSwapPtrBarrier( void *oldVal, void *newVal, void * volatile *theVal )
复制代码

函数会在oldValtheVal相同的状况下将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函数,就能保证在这句代码前的指令不会被打乱执行,保证正确的代码顺序

最后

很方,这个月想了不少想写的内容,而后发现别人都写过,尴尬的一笔。果真仍是本身太鶸了,最后随便赶工了一篇全是水货的文章,瑟瑟发抖

关注个人公众号获取更新信息
相关文章
相关标签/搜索