It used to be thought that the initial value of the semaphore was the maximum number of concurrent threads and could not be changed, but it is not.

Common development uses GCD DispatchSemaphore to address thread safety issues: When multiple threads access the same resource, it is easy to cause data confusion and data security issues.

A classic example of a multithreaded safety hazard – selling tickets

  • Before using a semaphore:
var ticketTotal = 15
let group: DispatchGroup = DispatchGroup(a)// Sell tickets
func __saleTicket(_ saleCount: Int) {
    DispatchQueue.global().async(group: group, qos: .default, flags: []) {
        for _ in 0..<saleCount {
            // Add a delay to make it possible for multiple threads to do this at the same time
            sleep(1) 
            / / sell one
            self.ticketTotal -= 1}}}// Start selling tickets
func startSaleTicket(a) {
    print("\(Date()) In the beginning there were\(ticketTotal)Zhang. "")

    print("\(Date()) Five tickets for the first time.")
    __saleTicket(5)

    print("\(Date()) Five tickets for the second time.")
    __saleTicket(5)

    print("\(Date()) Five tickets for the third time.")
    __saleTicket(5)

    group.notify(queue: .main) {
        print("\(Date()(Theoretically all sold out, practically all left\ [self.ticketTotal)Zhang. "")}}Copy the code

Print result:

Obviously the result is wrong, 15 sold 15 times but there are 4 left, this is the multi-threaded operation caused by data confusion.

  • Use a semaphore to add to the operation of selling tickets 🔐 :
func __saleTicket(_ saleCount: Int) {
    DispatchQueue.global().async(group: group, qos: .default, flags: []) {
        for _ in 0..<saleCount {
            // Add a delay to make it possible for multiple threads to do this at the same time
            sleep(1) 
            / / sell one
            self.semaphore.wait() / / add 🔐
            self.ticketTotal -= 1 
            self.semaphore.signal() // 解🔐}}}Copy the code

Print result:

The result is correct, multithreaded operations using semaphore can achieve thread synchronization to ensure data security.

Misunderstandings about semaphores

It was previously thought that the initial value of a semaphore was the maximum number of concurrent threads and was not changeable until we saw another article about a semaphore usage:

let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)

func semaphoreTest(a) {
    DispatchQueue.global().async {

        DispatchQueue.main.async {
            // Get some information from the main queue.// Send a signal
            self.semaphore.signal()     
        }

        // Start to wait
        self.semaphore.wait() 
        // The wait is over and the thread continues}}Copy the code

The maximum number of concurrent threads is 0. The maximum number of concurrent threads is 0. Isn’t there a thread that can work? It should always block the child thread, so what’s the point of this usage?

A new understanding of semaphore

Semaphore.wait () is known to subtract 1, but only if the semaphore is greater than zero:

  1. If it is greater than zero, the thread can continue to run down and then immediately aftersemaphore.wait()After this sentence, you will really reduce the semaphore by 1;
  2. If it is equal to zero, it will put the thread to sleep and join aThe queue of threads waiting for this signalWhen the semaphore is greater than zero, it willWake up the thread at the top of the waiting queue, continue the thread code and subtract 1 from the semaphore. This ensures that the semaphore is greater than zero, so there is no case where the semaphore is less than zero (Unless you set it to negative when you initialize it, which would crash the program when you use it).

Semaphore.signal () adds one to the semaphore, and later tests show that semaphore.signal() allows you to add any semaphore you want, so the initial semaphore is not immutable, but mutable.

  • Verified 🌰 :
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) / / 0

func semaphoreTest(a) {
    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")
    
    // Delay another thread by 3 seconds to add the semaphore asynchronously
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
        print("\(Date()) \(Thread.current)Semaphore + 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. If the thread is awakened, this function returns nonzero. Otherwise, return zero. Signal () returns 0, indicating that no thread has been awakened, but the semaphore does have +1. * /
    }

    semaphore.wait() // If the value is 0, the current thread is "stuck"
    print("\(Date()) \(Thread.current) hello_4") // 1-1 = 0
}
Copy the code

Print result:

Another thread

Demonstrates that semaphores are self-maintainable, just “invisible” (no API access).

  • Pseudocode explains the usage described earlier:
// Initialize the semaphore to 0, assuming semaphoseries is a number representing the semaphore
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // semaphoreCount = 0

func semaphoreTest(a) {
    DispatchQueue.global().async {
        // [1] Start task 1

        DispatchQueue.main.async {
            //【4】 Start task 2.[5] When task 2 is over, the semaphore is increased by 1 and a signal is sent to wake up the thread waiting first
            self.semaphore.signal() // semaphoreCount + 1 = 1    
        }

        // [2] Task 1 needs to wait for task 2 to complete before continuing to determine whether there is a semaphore
        self.semaphore.wait() [3] Semaphoseries == 0; semaphoseries == 0;
        
        // [6] If you can get here, it means that the semaphore is at least 1, which wakes up the thread. At the same time, the semaphore is reduced by 1
        // semaphoreCount - 1 = 0

        // If one is equal to zero, other threads waiting for the semaphore will continue to wait, and this thread will continue to execute.
        // [7] Continue task 1}}Copy the code

conclusion

  1. It is true that the initial value of the GCD semaphore is the maximum number of concurrent threads, but it is not unchangeablethroughsemaphore.signal()Any addIt’s like there’s a hiddensemaphoreCountTo control how many threads can work simultaneously;
  2. thissemaphoreCountYou have to have at least one to execute the code, as long as it’s zero,semaphore.wait()It will let the thread sleep untilsemaphoreCountGreater than 0 to wake up;
  3. Because there’s no API to get thissemaphoreCount, so be sure to pay attention to:How many timessemaphore.wait()I remember how many times I have to use itsemaphore.signal(), make sure to use pairing, otherwise the thread will sleep forever.

With this in mind, the GCD semaphore can be added to 🔐 as well as make the current thread wait for another thread, which means you can control the execution time of the thread

GCD has some other functions that need to maintain the number of pairs themselves

These functions also have no corresponding API fetching times and need to be maintained by themselves:

  • Queue group:group.enter()group.leave()
  • Timer:timer.resume()timer.suspend()
    • PS: Don’t pausesuspendExecute in statecancel(), otherwise, it willcollapseSo remember beforecancel()Before making sure the timer is runningresumeCondition.