【iOS开发】—— 初识锁

【iOS开发】—— 初识锁

  • 线程安全
  • 锁的种类
    • 自旋锁
      • 定义
      • 原理
      • 自旋锁缺点
      • OSSpinLock(自旋锁)
    • 互斥锁
      • os_unfair_lock
      • pthread_mutex
      • NSLock
      • NSRecusiveLock
      • Semaphore信号量
      • @synchronized
  • 总结
    • 两种之间的区别和联系:

线程安全

当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。 而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。 在iOS中, UIKit是绝对线程安全的,因为UIKit都是在主线程操作的,单线程没有线程当然没有线程安全问题,但除此之外,其他都要考虑线程安全问题

iOS解决线程安全的途径其原理大同小异,都是通过锁来使关键代码保证同步执行,从而确保线程安全性,这一点和多线程的异步执行任务是不冲突的。

注: 不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了

下方我们就详细讲解iOS相关锁,本博客采用一个经典的售票例子:

此处展示的是不加锁(即不考虑线程安全)的情况:

#import <UIKit/UIKit.h>@interface ViewController : UIViewController
@property (nonatomic, assign) NSInteger ticketCount;@end#import "ViewController.h"@interface ViewController ()@end
int cnt = 0;
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.self.ticketCount = 50;__weak typeof (self) weakSelf = self;//一号售卖口dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[weakSelf saleTick];});//二号售卖口dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[weakSelf saleTick];});
}- (void)saleTick {while (1) {if (self.ticketCount > 0) {self.ticketCount--;cnt++;NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);[NSThread sleepForTimeInterval:0.2];} else {NSLog(@"所有车票已售完,共计售出%d张", cnt);break;}}
} 
@end

运行结果的部分截图:
在这里插入图片描述
从上图可以发现,输出的顺序是乱序的,而且还显示卖出了54张票。对于上述情况,我们就可以通过加锁来实现修正错误问题。

锁的种类

iOS中的锁有两大类:自旋锁、互斥锁。

自旋锁

定义

自旋锁是一种同步机制,用于在多线程环境中保护共享资源的访问。它通过循环忙等待的方式,而不是阻塞线程,来实现对共享资源的互斥访问。

原理

线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。

自旋锁缺点

  • 调用者在未获得锁的情况下,一直运行--自旋,所以占用着CPU资源,如果不能在很短的时间内获得锁,会使CPU效率降低。所以自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。

OSSpinLock(自旋锁)

OSSpinLock是在libkern库中,使用之前需要引入头文件<libkern/OSAtomic.h>,使用时会出现警告⚠️。
在这里插入图片描述
这是因为OSSpinLock存在缺陷,从iOS10开始已经不建议使用了。官方建议使用os_unfair_lock来替代。
下面是使用os_unfair_lock的实例:

// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
#import "ViewController.h"#import <os/lock.h>@interface ViewController ()@property (nonatomic, assign) os_unfair_lock spinLock;
@end- (void)saleTick {while (1) {// 加锁OSSpinLockLock(&_spinLock);if (self.ticketCount > 0) {self.ticketCount--;cnt++;NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);[NSThread sleepForTimeInterval:0.2];} else {NSLog(@"所有车票已售完,共计售出%d张", cnt);break;}// 解锁OSSpinLockUnlock(&_spinLock);}
} 
@end

运行结果:
在这里插入图片描述
结果就是按照顺序非常规范地卖出了这50张票。
刚才提到了OSSpinLock存在缺陷,其实它的缺陷主要存在两点:

  • OSSpinLock不会记录持有它的线程信息,当发生优先级反转的时候,系统找不到低优先级的线程,导致系统可能无法通过提高优先级解决优先级反转问题
  • 高优先级线程使用自旋锁忙等待的时候一直在占用CPU时间片,导致低优先级线程拿到时间片的概率降低。

值得注意的是: 自旋锁和优先级反转没有关系,但是正因为有上面两点,所以自旋锁会导致优先级反转问题更难解决,甚至造成更为严重的线程等待问题,所以苹果就废除了OSSpinLock,转而推荐人们使用os_unfair_lock来替代,由于os_unfair_lock是一个互斥锁,所以我们将对其的讲解放到互斥锁中去。

互斥锁

保证在任何时候,都只有一个线程访问对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
互斥锁原理
线程会从sleep(加锁)——> running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销,所以效率是要低于自旋锁的。

互斥锁分为两种: 递归锁、非递归锁

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁。

os_unfair_lock

上面讲过现在苹果采用os_unfair_lock来代替不安全的OSSpinLock,且由于os_unfair_lock会休眠而不是忙等,所以属于 互斥锁 ,且是非递归互斥锁,下面来看一下它的用法:

os_unfair_lock 在os库中,使用之前需要导入头文件<os/lock.h>

//创建一个锁
os_unfair_lock unfairLock;
//初始化
unfairLock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&unfairLock);
//解锁
os_unfair_lock_unlock(&unfairLock);

实际使用方法:

- (void)saleTick {while (1) {//OSSpinLockLock(&_spinklock);os_unfair_lock_lock(&_lock);if (self.ticketCount > 0) {self.ticketCount--;cnt++;NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);[NSThread sleepForTimeInterval:0.2];} else {NSLog(@"所有车票已售完,共计售出%d张", cnt);os_unfair_lock_unlock(&_lock);break;}os_unfair_lock_unlock(&_lock);//OSSpinLockUnlock(&_spinklock);}
}

运行结果:
在这里插入图片描述
对于它的定义:
这是对于已经废弃的OSSpinkLock的替换,这个函数不会在争用时旋转,而是在内核中等待被解锁唤醒。与OSSpinLock一样,这个函数并不强制公平或锁排序一例如,解锁程序可能会在唤醒的服务程序获得获得锁的机会之前立即重新获得锁。这可能有利于性能的提高,但也可能导致等待者短缺。不是旋转(忙等),而是休眠,等待被唤醒,所以os_unfair_lock理应是互斥锁。

pthread_mutex

pthread_mutex就是 互斥锁 本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠,另外pthread_mutex也是非递归的锁。

使用时我们需要先引用这个头文件:#import <pthread.h>
具体使用如下:

// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁 
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);

结果如下:
在这里插入图片描述
结果就是按照顺序非常规范地卖出了这50张票。

NSLock

我们的Foundation框架内部也是有一把NSLock锁的,使用起来非常方便,基于互斥锁pthroad_mutex封装而来,是一把互斥非递归锁。
使用如下:

//初始化NSLock
NSLock *lock = [[NSLock alloc] init];
//加锁
[lock lock];
...
//线程安全执行的代码
...
//解锁
[lock unlock];

实际使用(在卖票例子中):

- (void)saleTick {while (1) {//OSSpinLockLock(&_spinklock);//os_unfair_lock_lock(&_lock);[self.lock lock];if (self.ticketCount > 0) {self.ticketCount--;cnt++;NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);[NSThread sleepForTimeInterval:0.2];} else {NSLog(@"所有车票已售完,共计售出%d张", cnt);//os_unfair_lock_unlock(&_lock);[self.lock unlock];break;}//OSSpinLockUnlock(&_spinklock);//os_unfair_lock_unlock(&_lock);[self.lock unlock];}
}

运行结果如下:
在这里插入图片描述
结果就是按照顺序非常规范地卖出了这50张票。

如果对非递归锁强行使用递归调用,就会在调用时发生线程阻塞,而并非是死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。

苹果官方文档的描述如下::
在这里插入图片描述
可以看到在同一线程上调用两次NSLock的lock方法将会永久锁定线程。同时也重点提醒向NSLock对象发生解锁消息时,必须确保消息时从发送初始锁定消息的同一个线程发送的,否则就会产生未知问题。

非递归互斥锁导致线程阻塞的例子:

- (void)saleTickWithNSLock {while(1) {// 加锁[lock lock];if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖self.ticketSurplusCount--;cnt++;NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);[NSThread sleepForTimeInterval:0.2];} else { // 如果已卖完,关闭售票窗口NSLog(@"所有火车票均已售完,共售出%d张票", cnt);// 解锁break;}// 解锁}
}

运行结果如下:
在这里插入图片描述
可以看到,因为我们对当前这个线程在执行lock操作后还未unlock的情况下,又进行了NSLock的重复lock加锁操作,所以当前线程发生了阻塞,只进行了一次卖票操作就再不执行其他操作了。

NSRecusiveLock

NSRecursiveLock使用和NSLock类似,不过NSRecursiveLock是递归互斥锁。

//初始化NSLock
NSRecusiveLock *recusiveLock = [[NSRecusiveLock alloc] init];
//加锁
[recusiveLock lock];
...
//线程安全执行的代码
...
//解锁
[recusiveLock unlock];

下面我们举一个NSRecursiveLock递归使用的例子:

#import "ViewController.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>
@interface ViewController ()//@property (nonatomic, assign) OSSpinLock spinklock;
//@property (nonatomic, assign) os_unfair_lock lock;
//@property (nonatomic, strong) NSLock* lock;
@property (nonatomic, strong) NSRecursiveLock* recursiveLlock;@endint cnt;
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.self.ticketCount = 50;__weak typeof (self) weakSelf = self;//self.spinklock = OS_SPINLOCK_INIT;//self.lock = OS_UNFAIR_LOCK_INIT;//self.lock = [[NSLock alloc] init];self.recursiveLlock = [[NSRecursiveLock alloc] init];//    //一号售卖口
//    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [weakSelf saleTick];
//    });
//
//    //二号售卖口
//    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [weakSelf saleTick];
//    });for (int i = 0; i < 10; ++i) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[weakSelf saleTick];});}}- (void)saleTick {while (1) {//OSSpinLockLock(&_spinklock);//os_unfair_lock_lock(&_lock);//[self.lock lock];[self.recursiveLlock lock];if (self.ticketCount > 0) {self.ticketCount--;cnt++;NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);[NSThread sleepForTimeInterval:0.2];} else {NSLog(@"所有车票已售完,共计售出%d张", cnt);//os_unfair_lock_unlock(&_lock);//[self.lock unlock];[self.recursiveLlock unlock];break;}//OSSpinLockUnlock(&_spinklock);//os_unfair_lock_unlock(&_lock);//[self.lock unlock];[self.recursiveLlock unlock];}
}@end

结果如下:
在这里插入图片描述
可以看到向同一个线程多次获取递归锁NSRecusiveLock并不会导致程序死锁,而是正常的线程安全地加锁执行。

苹果官方文档的描述如下:
在这里插入图片描述

Semaphore信号量

Semaphore信号量也可以解决线程安全问题,GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能:计数小于 0 时需要等待,不可通过。计数为 0 或大于 0 时,不用等待可通过。计数大于 0 且计数减 1 时不用等待,可通过。

Dispatch Semaphore 提供了三个方法:

dispatch_semaphore_create://创建一个 Semaphore 并初始化信号的总量
dispatch_semaphore_signal://发送一个信号,让信号总量加 1
dispatch_semaphore_wait://可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意:
信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务。
  • 保证线程安全,为线程加锁。

@synchronized

@synchronized可能是日常开发中用的比较多的一种递归互斥锁,因为它的使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低。

使用方法如下:

@synchronized (obj) {}

下面我们来探索一下@synchronized的源码:

  • 通过汇编能发现@synchronized就是实现了objc_sync_enter和 objc_sync_exit两个方法。
  • 通过符号断点能知道这两个方法都是在objc源码中的。
  • 通过clang也能得到一些信息。
#pragma clang assume_nonnull endint main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; { id _rethrow = 0; id _sync_obj = (id)__null; objc_sync_enter(_sync_obj);
try {struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}~_SYNC_EXIT() {objc_sync_exit(sync_exit);}id sync_exit;} _sync_exit(_sync_obj);NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_main_59328a_mi_0);} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}~_FIN() { if (rethrow) objc_exception_throw(rethrow); }id rethrow;} _fin_force_rethow(_rethrow);}
}}return 0;
}

总结

两种之间的区别和联系:

1.区别:

  1. 等待机制互斥锁是阻塞锁,当锁被其他线程占用时,请求线程会被阻塞;自旋锁是忙等待锁,请求线程会循环忙等待,不断检查锁的状态。
  2. CPU占用自旋锁是忙等待,当线程持有自旋锁时间较长时,其他等待线程会一直忙等待,浪费CPU资源互斥锁是阻塞,当线程请求锁时,会被阻塞,释放CPU资源给其他线程
  3. 适用场景自旋锁适用于多核心CPU、共享资源占用时间较短的情况;互斥锁适用于共享资源占用时间较长的情况。

2.联系

  1. 保护共享资源:自旋锁和互斥锁都用于保护共享资源,确保多线程环境下对共享资源的访问安全。
  2. 互斥性质:自旋锁和互斥锁都是互斥的,同一时间只能有一个线程持有锁,其他线程必须等待
  3. 锁的操作:自旋锁和互斥锁都具有获取锁和释放锁的操作,线程在获取锁后可以访问共享资源,完成操作后释放锁,让其他线程获取锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/691373.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【微服务】spring aop实现接口参数变更前后对比和日志记录

目录 一、前言 二、spring aop概述 2.1 什么是spring aop 2.2 spring aop特点 2.3 spring aop应用场景 三、spring aop处理通用日志场景 3.1 系统日志类型 3.2 微服务场景下通用日志记录解决方案 3.2.1 手动记录 3.2.2 异步队列es 3.2.3 使用过滤器或拦截器 3.2.4 使…

Windows环境下编译 aom 源码详细过程

AV1 AV1是一种开源的视频编码格式&#xff0c;由开放媒体联盟&#xff08;AOMedia Video 1&#xff0c;简称AOMedia或AOM&#xff09;开发。AV1旨在提供比现有的视频编码格式如H.264和H.265更好的压缩效率&#xff0c;同时保持或提高视频质量。AV1的编码效率显著高于H.264&…

差分约束 C++ 算法例题

差分约束 差分约束 是一种特殊的 n 元一次不等式组&#xff0c;m 个约束条件&#xff0c;可以组成形如下的格式&#xff1a; { x 1 − x 1 ′ ≤ y 1 x 2 − x 2 ′ ≤ y 2 ⋯ x m − x m ′ ≤ y m \begin{cases} x_1-x_1^{} \le y_1 \\ x_2-x_2^{} \le y_2 \\ \cdots \\ x_…

【机器学习】 技术栈和开发环境搭建

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 博客目录 技术栈编程语言库框架编辑器项目IDE …

行业分析---马斯克的Tesla

1 背景 在前面的博文《行业分析---我眼中的Apple Inc.》中&#xff0c;笔者曾介绍过苹果公司的财报和商业。依然本着提升自己看公司的能力&#xff0c;尝试去分析相对熟悉的公司&#xff0c;看懂它的商业。在之前的博客《自动驾驶---Tesla之FSD简介》中&#xff0c;笔者也简单介…

UE5C++ FString做为参数取值时报错error:C4840

问题描述 用来取FString类型的变量时报错&#xff1a; 问题解决 点击错误位置&#xff0c;跳转到代码&#xff1a; void AMyDelegateActor::TwoParamDelegateFunc(int32 param1, FString param2) {UE_LOG(LogTemp, Warning, TEXT("Two Param1:%d Param2:%s"), param…

带你探索CA和SSL证书

目录 一、什么是CA&#xff1f; 二、什么是SSL证书&#xff1f; 三、SSL证书分类和文件种类&#xff1f; 3.1 证书的分类&#xff1a; 3.2证书格式&#xff1a; 四、SSL和TSL 五、PSK介绍 六、nginx配置介绍 一、什么是CA&#xff1f; CA是证书的签发机构&#xff0c;它是…

书生作业:RAG

视频&#xff1a;https://www.bilibili.com/video/BV1QA4m1F7t4/ 教程&#xff1a;https://github.com/InternLM/Tutorial/blob/camp2/huixiangdou/readme.md 作业&#xff1a;https://github.com/InternLM/Tutorial/blob/camp2/huixiangdou/homework.md 项目地址&#xff1a;h…

【Vue基础】Vue在组件数据传递详解

Vue核心基础-CSDN博客 先回顾Vue特性&#xff1a; Vue.js 是一个用于构建用户界面的渐进式框架&#xff0c;具有许多强大的特性。以下是一些主要的 Vue 特性&#xff1a; 响应式数据&#xff1a;Vue 使用双向绑定来实现数据的响应式更新。当数据发生变化时&#xff0c;视图会自…

【0003day】VOSviewer分析

这个软件也可以用知网&#xff0c;也可以用web of science。 首先&#xff0c;需要创建数据。这个数据如何创建&#xff0c;需要参考对应的教程。&#xff08;本文以web of science为平台来做分析。&#xff09; 首先&#xff0c;创建对应的数据库。 一直下一步 让后选择完…

【全开源】JAVA同城组局同城找搭子系统源码支持微信小程序微信公众号H5 APP

让你周末不孤单 发布活动&#xff1a;用户可以发布自己想要进行的活动&#xff0c;包括活动类型、时间、地点等信息&#xff0c;方便其他用户查找和参与。搜索搭档&#xff1a;用户可以根据活动类型、时间、地点等信息&#xff0c;搜索附近的搭档&#xff0c;快速找到志同道合…

基于springboot的代驾管理系统的设计与实现

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…