线程安全(二)

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

避免锁的性能浪费

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

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

如果能够减少这些因素的损耗,就能有效的提高锁的性能

自旋锁

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

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

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

锁粒度

粒度是指颗粒的大小

对于锁来说,锁的粒度大小取决于锁保护的临界区的大小。锁的粒度越小,临界区的操作就越小,反之亦然,由于临界区执行代码时间导致的损耗问题我称作粒度锁问题。举个例子,假如某个修改元素的方法包括三个操作:查找缓存->查找容器->修改元素

- (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();
} 

实际上整个修改操作之中,只有最后的修改元素存在安全问题需要加锁,并且如果加锁的临界区代码执行时间过长可能导致有更多的线程需要等待锁,增加了锁使用的损耗。因此加锁代码应当尽量的短小简单:

- (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;@endSLLockItem *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已经不再指向Bif (!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函数,就能保证在这句代码前的指令不会被打乱执行,保证正确的代码顺序

最后

很方,这个月想了很多想写的内容,然后发现别人都写过,尴尬的一笔。果然还是自己太鶸了,最后随便赶工了一篇全是水货的文章,瑟瑟发抖

网络安全成长路线图

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成hei客大神,这个方向越往后,需要学习和掌握的东西就会越来越多,以下是学习网络安全需要走的方向:

# 网络安全学习方法

​ 上面介绍了技术分类和学习路线,这里来谈一下学习方法:
​ ## 视频学习

7bf4d.png#pic_center)

# 网络安全学习方法

​ 上面介绍了技术分类和学习路线,这里来谈一下学习方法:
​ ## 视频学习

​ 无论你是去B站或者是油管上面都有很多网络安全的相关视频可以学习,当然如果你还不知道选择那套学习,我这里也整理了一套和上述成长路线图挂钩的视频教程,完整版的视频已经上传至CSDN官方,朋友们如果需要可以点击这个链接免费领取。网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]中提到了两种方式来保证C++中的线程安全。第一种方式是使用静态局部变量,通过在getInstance()函数中返回一个静态局部变量的引用来实现单例模式。这种方式利用了C++11中的线程安全特性,保证了在多线程环境下只有一个实例被创建。第种方式是使用std::call_once函数,通过在getSingleton()函数中使用std::call_once来保证只有一个线程能够创建实例。这种方式也是利用了C++11中的线程安全特性,保证了在多线程环境下只有一个实例被创建。\[1\] 另外,引用\[2\]和引用\[3\]中的代码展示了使用互斥锁和原子操作来保证线程安全。互斥锁可以通过lock()和unlock()函数来实现对共享资源的互斥访问,从而保证线程安全。原子操作是一种特殊的操作,可以保证在多线程环境下对共享资源的原子性操作,从而避免了竞态条件的发生,保证了线程安全。\[2\]\[3\] 因此,C++中可以通过使用静态局部变量、std::call_once函数、互斥锁和原子操作等方式来保证线程安全。具体选择哪种方式取决于具体的需求和场景。 #### 引用[.reference_title] - *1* [C++实现单例模式,保证线程安全](https://blog.csdn.net/weixin_46027612/article/details/127785419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [c++线程安全问题](https://blog.csdn.net/m0_56104219/article/details/126907574)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值