swift 多线程锁(一) NSLock

news/2024/7/20 20:37:14 标签: swift, 开发语言, ios

在多线程的相关的开发中,必定会有锁的应用,这是因为如果多个线程极有可能会同时读取或者修改一个对象的值,那这时候很可能会出问题,比如读取的数值不对,或者出现之前对象的值已经被释放而引发野指针的问题

卖票问题

我们先看已经经典的售票问题,假设我们总共有500张票,有4个网络渠道同步售卖

不加锁

先看不加锁的情况,并发4个线程来同时卖票,代码如下:

swift">class ViewController: UIViewController {
    var num = 10

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        DispatchQueue.global().async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "1",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "2",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        for _ in 0...100 {
            self.buy()
        }
        print("main thread完成任务")

    }

    func buy(){
        if num > 0{
            num -= 1
            print("出票一张: 剩余: \(num)")
        }
    }

}

打印结果如下:

出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 7
出票一张: 剩余: 6
出票一张: 剩余: 5
出票一张: 剩余: 4
出票一张: 剩余: 3
出票一张: 剩余: 2
出票一张: 剩余: 1
出票一张: 剩余: 0

注意看,这里的剩余票数不对,且只有10张票,出票了12次。

另外需要说明的是,因为是多线程的问题,所以随着并发次数增多,那错误会更多,比如10000张票,分10个渠道,分别尝试出票1000次,那结果会更加混乱,感兴趣的读者可以尝试

锁的介绍

这里介绍下锁的概念,所谓的锁,就是当你操作这个变量的时候,把权限门锁上,等你开了锁之后,其他人才能操作这个变量,即保证同一时间只有一个操作行为

我们先试用比较基础的NSLock来加锁,NSLock是互斥锁,对应的是自旋锁。

互斥锁是指当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。

自旋锁是指线程在这一过程中保持执行,忙等待可以操作,比如每1ms来查看是否可以操作对象,该线程不会挂起,会一直占用cpu切片,但是没有互斥锁的线程切换行为

这两种锁各有利弊,但是一般还是推荐使用互斥锁,因为自旋锁用不好非常容易占用CPU资源

NSLock

我们使用NSLock来进行加锁,首先我们定义一个锁 ,然后在出票前我们加锁,出票结束后我们解锁,代码如下:

swift">class ViewController: UIViewController {
    var num = 10

    let lockLock = NSLock()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        DispatchQueue.global().async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "1",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "2",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        for _ in 0...100 {
            self.buy()
        }
        print("main thread完成任务")

    }

    func buy(){
        lockLock.lock()
        if num > 0{
            num -= 1
            print("出票一张: 剩余: \(num)")
        }
        lockLock.unlock()
    }

}

输出日志如下:

出票一张: 剩余: 9
出票一张: 剩余: 8
出票一张: 剩余: 7
出票一张: 剩余: 6
出票一张: 剩余: 5
出票一张: 剩余: 4
出票一张: 剩余: 3
出票一张: 剩余: 2
出票一张: 剩余: 1
出票一张: 剩余: 0

这里可以看到,顺序和剩余票数都对了

NSLock源码

open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
#if os(Windows)
        InitializeSRWLock(mutex)
        InitializeConditionVariable(timeoutCond)
        InitializeSRWLock(timeoutMutex)
#else
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
    }
    
    deinit {
#if os(Windows)
        // SRWLocks do not need to be explicitly destroyed
#else
        pthread_mutex_destroy(mutex)
#endif
        mutex.deinitialize(count: 1)
        mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
    }
    
    open func lock() {
#if os(Windows)
        AcquireSRWLockExclusive(mutex)
#else
        pthread_mutex_lock(mutex)
#endif
    }

    open func unlock() {
#if os(Windows)
        ReleaseSRWLockExclusive(mutex)
        AcquireSRWLockExclusive(timeoutMutex)
        WakeAllConditionVariable(timeoutCond)
        ReleaseSRWLockExclusive(timeoutMutex)
#else
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
#endif
    }

    open func `try`() -> Bool {
#if os(Windows)
        return TryAcquireSRWLockExclusive(mutex) != 0
#else
        return pthread_mutex_trylock(mutex) == 0
#endif
    }
    
    open func lock(before limit: Date) -> Bool {
#if os(Windows)
        if TryAcquireSRWLockExclusive(mutex) != 0 {
          return true
        }
#else
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
#endif

#if os(macOS) || os(iOS) || os(Windows)
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
        guard var endTime = timeSpecFrom(date: limit) else {
            return false
        }
#if os(WASI)
        return true
#else
        return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
#endif
    }

    open var name: String?
}

这里可以看到,NSLock其实就是对pthread_mutex的封装,后面讲递归锁的时候,我们还会再仔细看这里的代码

NSLock注意事项

lock会挂起线程,要注意配对使用

刚才讲到NSLock是互斥锁,会挂起线程,那么如果在上面的代码最后,增加两次lock,那么主线程就会被挂起,增加代码如下:

swift">lockLock.lock()

lockLock.lock()

print("main thread完成任务")

这里的main thread完成任务永远不会执行了,非常恐怖

一个办法可以避免造成死锁,使用try函数,尤其是在主线程中加锁的时候,try函数的介绍如下:
Attempts to acquire a lock and immediately returns a Boolean value that indicates whether the attempt was successful.

if lockLock.try() {
    lockLock.lock()
}
if lockLock.try() {
    lockLock.lock()
}
if lockLock.try() {
    lockLock.lock()
}
print("main thread完成任务")

这样子就不会发生死锁

NSLock是非递归锁

所谓递归非递归,是指是否支持嵌套,比如Lock中如果还有这个lock,那是否会出问题,我们尝试下,比如用户1,他因为各种原因,会在买票行为过程前后加锁解锁,我们试下

swift">DispatchQueue(label: "1",attributes: .concurrent).async {
            print("用户1开始买票")
            self.lockLock.lock()
            for _ in 0...10 {
                self.buy()
            }
            self.lockLock.unlock()
            print("用户1结束买票")
        }

我这里打印的结果是这样:

用户1开始买票
出票一张: 剩余: 9
出票一张: 剩余: 8
出票一张: 剩余: 7

这里就发生了死锁,用户1永远不会结束买票,因为它执行买票需要锁解除,而刚开始已经锁了,后续的解锁只能等购买行为结束才能解锁,购买行为需要等这个锁解除,就死循环了

怎么解决呢? 递归锁来解决,后续再增加这部分内容,所以这里要强调,lock一定要针对最小颗粒来进行解锁,避免死锁问题。 什么是最小颗粒?就是操作的最小函数,把操作相关的都封装函数,然后进行操作,不要在外部使用锁

参考文章和源码资料

  • https://juejin.cn/post/7000298226855182349
  • NSLock源码 https://github.com/apple/swift-corelibs-foundation
  • https://www.jianshu.com/p/777c28eface5

http://www.niftyadmin.cn/n/5265843.html

相关文章

IDEA设置查看JDK源码

问题 我们在查看JDK源码时,可能会遇到这种情况,步入底层查看JDK源码时,出现一堆var变量,可读性非常之差,例如笔者最近想看到nio包下的SocketChannelImpl的write方法,结果看到这样一番景象: pu…

facebook广告怎么降低预算优化效果

在Facebook广告中降低预算优化效果的方法有以下几种: 细分受众群体:根据不同受众群体的兴趣和行为特征,将广告投放精细划分为更小的群体,以更精准地触达目标受众,提高转化率和降低转化成本。优化广告素材:…

炒股怎么做杠杆?安全正规的融资融券了解一下!

加杠杆炒股是指放大投资资金进行股票交易,比如自有资金100万,向证券公司融资100万,那么投资者炒股的本金就有200万。当股市行情好的时候可以放大我们的收益! 目前我国股票加杠杆通过融资融券来实现,这个是唯一安全正规…

CentOS 7部署vsftpd

(1)概述 vsftpd是Linux上一个非常流行的FTP服务器软件。它使用简单,功能强大,安全性高。本文将介绍如何在CentOS 7上部署vsftpd服务器。 (2)安装vsftpd 使用yum命令安装vsftpd: yum install…

如何连接到 Azure SQL 数据库(下)

在《如何连接到 Azure SQL 数据库(上)》中,我们已经了解到了以下内容↓↓↓ 开始之前:Azure 连接凭据和防火墙 如何检索 Azure 连接凭据如何配置服务器防火墙使用 SQL Server Management Studio 连接到 Azure使用 dbForge Studio…

MongoDB——模糊查询的两种方法

方法一:类似于结构性数据库的like db.users.find({fname: /zhangsan/}); 对应mysql的like用法:select * from users where fname like %zhangsan%; (1)如果要模糊查询以什么开头,方法如下: db.users.fi…

华为云CodeArts Pipeline常见问答汇总

1.【Pipeline】CodeArts Pipeline流水线如何传递参数至CodeArts Build编译构建任务 答参考文档 https://support.huaweicloud.com/pipeline_faq/pipeline_faq_0004.html https://support.huaweicloud.com/usermanual-pipeline/pipeline_10_0005.html https://support.hu…

【Android】MVC与MVP的区别,MVP网络请求实践

一、MVC模式 目录 一、MVC模式二、MVP模式 1、MVP的简单应用 1.1 导入相关依赖包并设置权限1.2 实现Model1.2 实现Presenter1.3 实现View1.4分析项目结构和绑定过程1.5效果展示 2、MVP结合RxJava 一、MVC模式 MVC(Model(模型)——View(视图)——Controller(控制…