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

2020-03-20  本文已影响0人  健了个平_24

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

平时开发一般都使用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获取次数,需要自己维护:

上一篇下一篇

猜你喜欢

热点阅读