为什么我的DispatchGroup的等待总是超时?

use*_*985 7 multithreading grand-central-dispatch swift

我有这个代码:

let goAheadAction = UIAlertAction(title: "Go Ahead", style: .default) { _ in
    let dispatchGroup = DispatchGroup()
    dispatchGroup.enter()

    Server.POSTRequest(accessPoint: "/UserServices/forgotPassword", params:["emailAddress" : self.emailAddressTextField.text!], handler: {
            (data: Data?, response: URLResponse?, error: Error?) -> Void in
        // TODO: Only considered successful case
        /*let confirmAlertController = UIAlertController(title: "Email been sent", message: "Please check your mailbox", preferredStyle: UIAlertControllerStyle.alert)

        self.present(confirmAlertController, animated: true, completion: nil)*/

        dispatchGroup.leave()
    })

    let waitResult = dispatchGroup.wait(timeout: DispatchTime(uptimeNanoseconds: 10000000000))

    if waitResult == .success {
        let emailSentAlertController = UIAlertController(title: "Email Sent", message: "We've sent an password reset link to \"\(self.emailAddressTextField.text!)\"", preferredStyle: UIAlertControllerStyle.alert)

        let gotItAction = UIAlertAction(title: "Got it", style: UIAlertActionStyle.cancel, handler: nil)

        emailSentAlertController.addAction(gotItAction)
        self.present(emailSentAlertController, animated: true, completion: nil)
    } else {
        let timeOutAlertController = UIAlertController(title: "Time Out", message: "Failed to request service, reason: \"Time Out\"", preferredStyle: UIAlertControllerStyle.alert)

        let gotItAction = UIAlertAction(title: "Got it", style: UIAlertActionStyle.cancel, handler: nil)

        timeOutAlertController.addAction(gotItAction)
        self.present(timeOutAlertController, animated: true, completion: nil)
    }
}
Run Code Online (Sandbox Code Playgroud)

每次我让dispatchGroup等待,它总是返回超时.怎么了?我把它设置为10000000000纳秒,这应该是10秒吧?我的要求确实在10秒之前回来了.

Rob*_*Rob 5

A couple of thoughts:

  1. As rmaddy said, if you want to wait 10 seconds, the correct syntax would be:

    let waitResult = dispatchGroup.wait(timeout: .now() + 10)
    
    Run Code Online (Sandbox Code Playgroud)

    Your syntax is not going to wait at all, and it's going to look like it instantly timed out.

  2. You shouldn't be waiting at all.

    First, you should never block the main thread. At worst, you could have your app killed by the watchdog process if you do this at the wrong time. At best, it's a horrible UX (i.e. the app will appear to be completely frozen to the end user).

    Second, depending upon how POSTRequest was implemented, you might introduce a new problem. Specifically, if POSTRequest calls its completion handler on the main queue (e.g. the default behavior for network libraries like Alamofire), you could deadlock until with wait timeout on the main thread expires. If you are going to wait (which you shouldn't do anyway), you want to make sure you never wait on the same queue that your completion handler will use.

    Note, if the completion handler isn't running on the same queue that you are waiting, there is no deadlock risk, but you still suffer from the problem I mentioned earlier, namely that you're blocking the main thread, which should always be avoided.

You should instead move all that code inside the closure. And if that closure is running on a background thread, make sure to DispatchQueue.main.async { ... } to dispatch it back to the main queue.

So, the correct pattern is something like the following.

  1. If you want your request to timeout after 10 seconds, build that into the original request. Note the timeoutInterval:

    class func POSTRequest(accessPoint: String, params: [String: String]?, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
        let url = URL(string: baseURLString)!
            .appendingPathComponent(accessPoint)
    
        let request = URLRequest(url: url, timeoutInterval: 10)
    
        URLSession.shared.dataTask(with: request, completionHandler: completionHandler)
    }
    
    Run Code Online (Sandbox Code Playgroud)

    Clearly, you may be doing something else in POSTRequest, so don't get lost in the details there. The key point is to build the timeoutInterval into the request.

  2. Then, make sure your UIAlertAction starts the request and simply puts all of the processing after the POSTRequest inside its completionHandler, thereby eliminating the need for any dispatch group waiting or anything like that:

    let goAheadAction = UIAlertAction(title: "Go Ahead", style: .default) { _ in
        let email = self.emailAddressTextField.text!
        let parameters = ["emailAddress": email]
        Server.POSTRequest(accessPoint: "/UserServices/forgotPassword", params: parameters) { data, response, error in
            DispatchQueue.main.async {
                guard error == nil else {
                    let alert = UIAlertController(title: "Time Out", message: "Failed to request service, reason: \"Time Out\"", preferredStyle: .alert)
                    alert.addAction( UIAlertAction(title: "Got it", style: .cancel))
                    self.present(alert, animated: true)
                    return
                }
    
                let alert = UIAlertController(title: "Email Sent", message: "We've sent an password reset link to \"\(email)\"", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "Got it", style: .cancel))
                self.present(alert, animated: true)
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    This way, no threads are blocked.