iOS 多线程之线程锁Swift-Demo示例总结
线程锁是什么
在前面的文章中总结过多线程,总结了多线程之后,线程锁也是必须要好好总结的东西,这篇文章构思的时候可能写的东西得许多,只能挤时间一点点的慢慢的总结了,知道了线程之后要了解线程锁就得先了解一下什么是“线程锁”。
“线程锁”一段代码在同一个时间内是只能被一个线程访问,为了避免在同一时间内有多个线程访问同一段代码就有了“锁”的概念,比如说,线程A在访问着一段代码,进入这段代码之后我们加了一个“锁”。这个时候线程B又来访问了,由于有了锁线程B就会等待线程A访问结束之后解开了“锁”线程B就可以接着访问这段代码了,这样就避免了在同一时间内多个线程访问同一段代码!
相信上面的解释应该理解了“锁”的概念,也知道它是为了什么产生的,我们再举一个例子,一个房子一个人(线程)进去之后就把门锁上了,另一个人(线程)来了之后就在等待前面的人(线程)出来,等前面的人出来之后把门打开,它才可以进入房间。这样的解释相信应该明白了“锁”的概念,但是我们还是得强调一点,就是在这个“锁”和“解锁”之间不要有太多的“事”(执行代码,也就是任务)做,不然会造成过长时间的等待!就是去了多线程真正的含义和所用!
下面我们一个个的来解释我们常用的线程锁。
NSLock
NSLock是最简单的互斥锁,下面的NSCondition、NSConditionLock以及NSRecursiveLock都是遵守了NSLocking协议的,我们就放在一起说,包括我们现在说的NSLock也是,我们看看这个NSLock里面具体都有什么,先看看它代码里面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public protocol NSLocking { public func lock() public func unlock() } open class NSLock : NSObject , NSLocking { open func ` try `() -> Bool open func lock(before limit: Date) -> Bool @available (iOS 2.0, *) open var name: String? } |
我们分别说说上面的方法:
1、lock和unlock 就是这个类最常用的两个方法,“加锁”和“解锁”的方法。
2、try()方法是尝试加锁,失败是不会阻塞线程的,如果获取锁失败就不会执行加锁代码。
3、lock(before limit: Date) 这个方法是在后面参数传入的时间内没有获取到线程锁就阻塞线程,要是到期还没有获取到线程锁就唤醒线程,这时候返回值是NO。
下面是我们Demo中具体的使用的例子代码,后面枷锁的方式都是继承自下面LockObject这个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | class LockObject { var dataSourceArray : Array < String > = Array () /// 开始时间 var startTime = CFAbsoluteTimeGetCurrent () init () { addDataSource () } private func addDataSource () { for i in 0 .. < 1000 { self . dataSourceArray . append ( "indexPath = \( i )" ) } } func removeDataSource (){ /// 模拟开启三条线程去删除最外面的数据 for _ in 0 .. < 3 { DispatchQueue . global (). async { self . removeFirstObjectWithDataSource () } } } /// 删除第一个数据 func removeFirstObjectWithDataSource (){ while ( true ) { if ( dataSourceArray . count > 0 ) { /// Thread 4: EXC_BAD_ACCESS (code=1, address=0x123e30008) dataSourceArray . removeFirst () } else { let nowTime = CFAbsoluteTimeGetCurrent () let resultString = "操作开始时间:" + String ( describing : self . startTime ) + "\n结束时间:" + String ( describing : nowTime ) + "\n整个操作用时:" + String ( nowTime - self . startTime ) + "ms" print ( "没有加任何锁:" ) print ( resultString ) return } } } } |
由于数据竞争的原因,上面的代码在 dataSourceArray.removeFirst() 这里闪退了,下面是我们定义的NSLock锁的代码,它是能正常运行的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class NSLockObject : LockObject { /*各种锁*/ let lock = NSLock () /// 删除第一个数据 override func removeFirstObjectWithDataSource (){ while ( true ) { if ( dataSourceArray . count > 0 ) { self . lock . lock () dataSourceArray . removeFirst () self . lock . unlock () } else { let nowTime = CFAbsoluteTimeGetCurrent () let resultString = "操作开始时间:" + String ( describing : self . startTime ) + "\n结束时间:" + String ( describing : nowTime ) + "\n整个操作用时:" + String ( nowTime - self . startTime ) + "ms" print ( "NSLock:" ) print ( resultString ) return } } } } |
NSCondition
NSCondition条件锁,首先它也是遵循NSLocking协议的,这点和我们上面说的NSLock是一致的,所以它的加锁和解锁方式和我们前面说的NSLock是一样的,就是lock和unlock方法,你要是简单的使用它来解决线程同步的问题,那他简单的用法和前面写的NSLock也是一样的。但我们要是把NSCondition当NSLock用那就真的是浪费了!NSCondition还有自己的wait和signal用法,这个和后面信号量有点点类似,信号量的我们下面再说,看看NSCondition部分的代码:
NSConditionLock同样实现了NSLocking协议,不过测试一下之后你会发现这个性能比较低。NSConditionLock也能像NSCondition一样能进行线程之间的等待调用,并且还是线程安全的。下面使我们Demo中的代码,写给这里的只是最基本的加锁解锁的代码,先看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class NSConditionLockObject : LockObject { let lock = NSCondition () /// 删除第一个数据 override func removeFirstObjectWithDataSource (){ while ( true ) { if ( dataSourceArray . count > 0 ) { self . lock . lock () dataSourceArray . removeFirst () self . lock . unlock () } else { let nowTime = CFAbsoluteTimeGetCurrent () let resultString = " 操作开始时间:" + String ( describing : self . startTime ) + "\n结束时间:" + String ( describing : nowTime ) + "\n整个操作用时:" + String ( nowTime - self . startTime ) + "ms" print ( "NSCondition:" ) print ( resultString ) return } } } } |
下面的代码是关于NSCondition解决线程同步的问题,注意看代码里面的注释,我们就不再过多阐述了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // 模拟下载数据 - 解决线程同步的问题 func threadSimulateDownLoadData () { thread1 = Thread ( target : self , selector : # selector ( method1 ( sender :)), object : nil ) thread1 ?. start () thread2 = Thread ( target : self , selector : # selector ( method2 ( sender :)), object : nil ) } // 定义两方法,用于两个线程调用 NSCondition 解决线程同步的问题 // 线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步) // 也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的! // 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时, // 任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步 @objc func method1 ( sender : AnyObject ){ for i in 0 .. < 10 { print ( "Thread 1 running \( i )" ) sleep ( 1 ) if i == 2 { thread2 ?. start () //启动线程2 // 本线程(thread1)锁定 condition1 . lock () // 使线程处于等待状态 condition1 . wait () condition1 . unlock () } } print ( "Thread 1 over" ) // 线程2激活 唤醒线程 线程1等待 condition2 . signal () } // 方法2 @objc func method2 ( sender : AnyObject ){ for i in 0 .. < 10 { print ( "Thread 2 running \( i )" ) sleep ( 1 ) if i == 2 { // 线程1激活 线程2等待 condition1 . signal () // 本线程(thread2)锁定 condition2 . lock () condition2 . wait () condition2 . unlock () } } print ( "Thread 2 over" ) } |
NSRecursiveLock
有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决,也就是我们的NSRecursiveLock,它就是递归锁!使用递归锁可以在一个线程中反复获取锁而不造成死锁,在这个过程中也会记录获取锁和释放锁的次数,只有等两者平衡的时候才会释放,下面是我们Demo中的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 递归调用 func removeFromImageArray () - > Void { recursiveLock . lock () if ( imageMutableArray . count > 0 ) { imageMutableArray . removeFirst () self . removeFromImageArray () } recursiveLock . unlock () } // MARK: - removeFromDataImageArray // 模仿递归调用 override func removeFromDataImageArray () - > Void { let dispatchGroup = DispatchGroup . init () let dispatchQueue = DispatchQueue . init ( label : queueLabel , qos : . default , attributes : . concurrent ) dispatchQueue . async ( group : dispatchGroup , qos : . default , flags : DispatchWorkItemFlags . noQoS ) { self . removeFromImageArray () } dispatchGroup . notify ( queue : DispatchQueue . main ) { self . now = CFAbsoluteTimeGetCurrent () let resultString = "操作开始时间:" + String ( describing : self . then ) + "\n结束时间:" + String ( describing : self . now ) + "\n整个操作用时:" + String ( self . now ! - self . then !) + "ms" self . resulttextView . text = resultString } } |
@synchronized
@synchronized你要说它简单,它的用法的确都是比较简单的,要想深了探究一下它的话,它里面的东西还的确是挺多的!但我们是在Swift中来讨论线程锁的,这里也就不能再使用 @synchronized,因为在Swift中它是不在使用了的,相应代替它的是下面下面这两句:objc_sync_enter() 中间是你需要加锁的代码 objc_sync_exit() ,那上面相同的操作我们用这个互斥锁写的话代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // MARK: - removeFromDataImageArray override func removeFromDataImageArray () - > Void { while ( true ) { //互斥锁 objc_sync_enter ( self ) if ( imageMutableArray . count > 0 ) { imageMutableArray . removeFirst () } else { now = CFAbsoluteTimeGetCurrent () let resultString = "操作开始时间:" + String ( describing : then ) + "\n结束时间:" + String ( describing : now ) + "\n整个操作用时:" + String ( now ! - then !) + "ms" DispatchQueue . main . async { self . resulttextView . text = resultString } return } objc_sync_exit ( self ) } } |
dispatch_semaphore_t 信号量
dispatch_semaphore_t是属于GCD里面的东西,在前面总结多线程的时候我们说把它放在我们总结线程锁的时候说,在这里我们就说一些这个信号量,dispatch_semaphore_t 和前面@synchronized一样都是我们OC的写法,在我们的Swift中也不是这样写的,全部的内容都是在DispatchSemaphore中,关于GCD方面API的对比我们在下面做了一张表,大致的说一下:
你看完了这张图的对比以及总结之后,我们说回我们的主题:DispatchSemaphore 可以看到它的主要的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | open class DispatchSemaphore : DispatchObject { } /// dispatch_semaphore extension DispatchSemaphore { // 发送信号,让信号量+1方法 public func signal () - > Int // 等待,让信号量-1方法 public func wait () // 下面两个方法可以设置等待的时间,过了这个时间要是没有让信号量大于或者等于初始化的信号量值的时候 // 就会自己接着往执行代码,相等于我们的锁是失效了的 public func wait ( timeout : DispatchTime ) - > DispatchTimeoutResult public func wait ( wallTimeout : DispatchWallTime ) - > DispatchTimeoutResult } extension DispatchSemaphore { /*! * @function dispatch_semaphore_create * * @abstract * Creates new counting semaphore with an initial value. * * @discussion * Passing zero for the value is useful for when two threads need to reconcile * the completion of a particular event. Passing a value greater than zero is * useful for managing a finite pool of resources, where the pool size is equal * to the value. * * @param value * The starting value for the semaphore. Passing a value less than zero will * cause NULL to be returned. * * @result * The newly created semaphore, or NULL on failure. */ @ available ( iOS 4.0 , *) public /*not inherited*/ init ( value : Int ) } |
OC和Swift的用法是一样的,只是在写法上有一些的区别,这里就不再说OC的了,我们直接看看Swift的代码怎么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // MARK: - startTestBtnAction override func removeFromDataImageArray () - > Void { while ( true ) { /* 也可以直接写: semaPhore.wait() 这里发生一个等待信号,信号量就-1,变成0 ,后面的任务就会处于等待状态, 等到信号量大于等于1的时候在执行,要是信号量不大于或者等于你初始化时候的值,它就一直处于等待状态 当然,你也可以在这里这是等待的时间 semaPhore.wait(timeout: DispatchTime.init(uptimeNanoseconds: UInt64(10))) 但过了这个时间之后在进入就等于是我们的锁失效了。面临的问题也就是相应的崩溃,在删除方法哪里,可以自己试一下 */ _ = semaPhore . wait ( timeout : DispatchTime . distantFuture ) if ( imageMutableArray . count > 0 ) { imageMutableArray . removeFirst () } else { now = CFAbsoluteTimeGetCurrent () let resultString = "操作开始时间:" + String ( describing : then ) + "\n结束时间:" + String ( describing : now ) + "\n整个操作用时:" + String ( now ! - then !) + "ms" DispatchQueue . main . async { self . resulttextView . text = resultString } // 不要忘记在这里加处理,不然return之后是执行不到下面的semaPhore.signal()代码 semaPhore . signal () return } // signal() 方法,这里会使信号量+1 semaPhore . signal () } } |
POSIX
POSIX和我们前面写的dispatch_semaphore_t用法是挺像的,但探究本质的haul它们根本就不是一个东西,POSIX是Unix/Linux平台上提供的一套条件互斥锁的API。你要是在OC的文件中只用的话你需要导入头文件:pthread.h
在Swift中就不用了,但是在使用的时候不管是OC的还是Swift的,代码是一致的,它的几个主要的方法就是下面三个,剩下的具体的代码可以看demo或者是下面基本的方法:
1、pthread_mutex_init 初始化方法
2、pthread_mutex_lock 加锁方法
3、pthread_mutex_unlock 解锁方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | class POSIXController : ThreadLockController { var imageMutableArray : Array < Any > = Array . init () var mutex : pthread_mutex_t = pthread_mutex_t () //初始化pthread_mutex_t类型变量 override func viewDidLoad () { super . viewDidLoad () // 初始化 pthread_mutex_init ( & mutex , nil ) // Do any additional setup after loading the view. for i in 0 ... 1000 { imageMutableArray . append ( "imageArray===" + String ( i )) } print ( "NSLock初始化的数组个数是" , imageMutableArray . count ) } // MARK: - startTestBtnAction override func removeFromDataImageArray () - > Void { while ( true ) { // 加锁 pthread_mutex_lock ( & mutex ) if ( imageMutableArray . count > 0 ) { imageMutableArray . removeFirst () } else { now = CFAbsoluteTimeGetCurrent () let resultString = "操作开始时间:" + String ( describing : then ) + "\n结束时间:" + String ( describing : now ) + "\n整个操作用时:" + String ( now ! - then !) + "ms" DispatchQueue . main . async { self . resulttextView . text = resultString } pthread_mutex_unlock ( & mutex ); return } // 解锁 pthread_mutex_unlock ( & mutex ) } } /* Swift 的deinit函数实际的作用和OC中的dealloc函数是一样的 对象的释放 通知 代理等等的处理都是在这里处理的 */ deinit { pthread_mutex_destroy ( & mutex ); //释放该锁的数据结构 } } |
剩下的还有什么
1、OSSpinLock
首先要提的是OSSpinLock已经出现了BUG,导致并不能完全保证是线程安全,所以这个我们知道,大概的了解一下,具体的问题可以去这里仔细看看:不再安全的 OSSpinLock
2、dispatch_barrier_async/dispatch_barrier_sync
这个我在前面总结GCD的时候说过了这个“栅栏”函数,就不在这里重复说了
3、最后就是Demo的地址了,这个Demo原本是想用Swift试着模仿一下微信的UI的,包括聊天框架那部分,以前写过OC的,这次春被用Swift写一下,主要也是为了用一下Swift,以及看一下4.0它的一些新的特性,不然很久不写,一些东西比较容易遗忘!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话