美文网首页
个人对GCD信号量的一些误解...

个人对GCD信号量的一些误解...

作者: 健了个平_24 | 来源:发表于2020-03-20 10:50 被阅读0次

    以前认为信号量的初始值就是线程的最大并发数,不可更改的,其实并不然。

    平时开发一般都使用GCD信号量(DispatchSemaphore)来解决线程安全问题:当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。

    经典的多线程安全隐患示例 - 卖票

    • 使用信号量之前:
    var ticketTotal = 15
    let group: DispatchGroup = DispatchGroup()
        
    // 卖票操作
    func __saleTicket(_ saleCount: Int) {
        DispatchQueue.global().async(group: group, qos: .default, flags: []) {
            for _ in 0..<saleCount {
                // 加个延时可以大概率让多条线程同时进行到这一步
                sleep(1) 
                // 卖一张
                self.ticketTotal -= 1 
            }
        }
    }
    
    // 开始卖票
    func startSaleTicket() {
        print("\(Date()) 一开始总共有\(ticketTotal)张")
    
        print("\(Date()) 第一次卖5张票")
        __saleTicket(5)
    
        print("\(Date()) 第二次卖5张票")
        __saleTicket(5)
    
        print("\(Date()) 第三次卖5张票")
        __saleTicket(5)
    
        group.notify(queue: .main) {
            print("\(Date()) 理论上全部卖完了,实际上剩\(self.ticketTotal)张")
        }
    }
    

    打印结果:

    image 明显结果是错的,15张卖了15次却还剩4张,这是多线程操作引发的数据错乱问题。
    • 使用信号量对卖票的操作进行加解🔐:
    func __saleTicket(_ saleCount: Int) {
        DispatchQueue.global().async(group: group, qos: .default, flags: []) {
            for _ in 0..<saleCount {
                // 加个延时可以大概率让多条线程同时进行到这一步
                sleep(1) 
                // 卖一张
                self.semaphore.wait() // 加🔐
                self.ticketTotal -= 1 
                self.semaphore.signal() // 解🔐
            }
        }
    }
    

    打印结果:

    image 结果正确,多线程操作使用信号量就可以实现线程同步以保证数据安全了。

    对信号量的误解

    以前认为信号量的初始值是指线程的最大并发数,不可更改的,直到看到其他文章介绍的一个信号量用法:

    let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
    
    func semaphoreTest() {
        DispatchQueue.global().async {
    
            DispatchQueue.main.async {
                // 从主队列中获取一些信息
                ...
                // 发送信号
                self.semaphore.signal()     
            }
    
            // 开始等待
            self.semaphore.wait() 
            // 等待结束,线程继续
        }
    }
    

    看到这个用法就开始觉得奇怪了,明明初始化为0,不就是线程最大并发数为0吗?不就是不能有线程可以工作吗?按道理应该会一直阻塞住这个子线程才对,那这种用法有什么意义呢?

    对信号量的重新认识

    众所周知,semaphore.wait()是减1操作,不过这个减1操作的前提是信号量是否大于0:

    1. 如果大于0,线程可以继续往下跑,然后紧接在semaphore.wait()这句过后,才会真正对信号量减1;
    2. 如果等于0,就会让线程休眠,加入到一个都等待这个信号的线程队列当中,当信号量大于0时,就会唤醒这个等待队列中靠前的线程,继续线程后面代码且对信号量减1,也就确保了信号量大于0才减1,所以不存在信号量小于0的情况(除非在初始化时设置为负数,不过这样做的话当使用时程序就会崩溃)。

    semaphore.signal()是对信号量的加1操作,后来经过测试发现,通过semaphore.signal()可以任意添加信号量,所以初始化的信号量并非不可更改的,是可以随意更改的

    • 验证的🌰:
    let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // 0
    
    func semaphoreTest() {
        semaphore.signal() // 0 + 1 = 1
        semaphore.signal() // 1 + 1 = 2
        semaphore.signal() // 2 + 1 = 3
    
        semaphore.wait() // 3 - 1 = 2
        print("\(Date()) \(Thread.current) hello_1")
    
        semaphore.wait() // 2 - 1 = 1
        print("\(Date()) \(Thread.current) hello_2")
    
        semaphore.wait() // 1 - 1 = 0
        print("\(Date()) \(Thread.current) hello_2")
        
        // 延迟3秒去另一个线程异步添加信号量
        DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
            print("\(Date()) \(Thread.current) 信号量+1")
            let result = self.semaphore.signal() // 0 + 1 = 1
            print("\(Date()) \(Thread.current) result: \(result)");
            /*
             * PS: signal() 会返回一个结果,文档解释为:
             * This function returns non-zero if a thread is woken. Otherwise, zero is returned. 如果线程被唤醒,则此函数返回非零。否则,返回零。
             * 这里执行后会有一条线程被唤醒,所以返回1,前面的3次signal()返回的都是0,说明没有线程被唤醒,不过信号量的确是有+1的。
             */
        }
    
        semaphore.wait() // 等于0就”卡住“当前线程
        print("\(Date()) \(Thread.current) hello_4") // 1 - 1 = 0
    }
    

    打印结果:

    image 即便信号量初始为0,也可以手动添加信号量,所以前3句马上打印;而最后1句由于没有信号了,线程进入休眠无法执行,然后3秒后在另一条线程添加了信号量,这条线程才被唤醒去打印最后一句。

    证明了信号量是可以自己维护的,只是“看不见”(没有API获取)。

    • 伪代码解释前面介绍的用法:
    // 初始化信号量为0,假设 semaphoreCount 是代表信号量的一个数字
    let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // semaphoreCount = 0
    
    func semaphoreTest() {
        DispatchQueue.global().async {
            //【1】开始执行任务1
    
            DispatchQueue.main.async {
                //【4】开始执行任务2
                ...
                //【5】任务2结束,信号量加1,发送信号,唤醒等待靠前的线程
                self.semaphore.signal() // semaphoreCount + 1 = 1    
            }
    
            //【2】任务1需要等待任务2执行完才继续,判断有无信号量
            self.semaphore.wait() //【3】判断信号量,发现 semaphoreCount == 0,这里”卡住“(休眠)
            
            //【6】能来到这里,说明信号量至少为1,唤醒了这条线程,同时对信号量减1
            // semaphoreCount - 1 = 0
    
            // 减1后如果等于0,那么其他还在等这个信号量的线程只能继续等,而这条线程会继续往下执行。 
            //【7】任务1继续
        }
    }
    

    总结

    1. GCD信号量的初始值的确是线程的最大并发数,不过这个并发数不是不能修改的,可以通过semaphore.signal()任意添加的,相当于是有个隐藏的semaphoreCount来控制能有多少条线程能同时工作;
    2. 这个semaphoreCount至少要有 1 才可以执行代码,只要是 0,semaphore.wait()就会让线程休眠等着直到semaphoreCount大于 0 才唤醒;
    3. 由于没有API获取这个semaphoreCount,所以一定要注意:用过多少次semaphore.wait()就记得也要用多少次semaphore.signal(),保证使用配对,不然线程会永远休眠。

    知道这些后,以后GCD信号量除了可以加解🔐外,也可以做到让当前线程等待别的线程了,也就是说可以控制线程的执行时机喔~

    GCD其它一些需要自己维护配对次数的函数

    这些函数也是没有相应API获取次数,需要自己维护:

    • 队列组:group.enter()group.leave()
    • 定时器:timer.resume()timer.suspend()
      • PS:不要在暂停suspend状态下执行cancel(),否则会崩溃,所以记得在cancel()前确定 timer 是在运行resume状态下。

    相关文章

      网友评论

          本文标题:个人对GCD信号量的一些误解...

          本文链接:https://www.haomeiwen.com/subject/nfmeyhtx.html