Function/Code Swift 中的并发设计

Function/Code Design with Concurrency in Swift

我正在尝试在 Swift 中创建我的第一个应用程序,其中涉及向网站发出多个请求。这些请求都是使用块

完成的
var task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in ... } 
task.resume()

据我了解,此块使用与主线程不同的线程。

我的问题是,设计依赖于该块中的值的代码的最佳方法是什么?例如,理想的设计(但是由于执行这些块的线程不是主线程而不可能)是

func prepareEmails() {
    var names = getNames()
    var emails = getEmails()
    ...
    sendEmails()
}

func getNames() -> NSArray {
    var names = nil
    ....
    var task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in 
        names = ...
    }) 
    task.resume()
    return names
}

func getEmails() -> NSArray {
    var emails = nil
    ....
    var task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in 
        emails = ...
    }) 
    task.resume()
    return emails
}

但是在上面的设计中,getNames() 和 getEmails() 很可能 return 为零,因为任务到 return 时还没有更新 emails/name s.

另一种设计(我目前正在实施)是有效地删除 'prepareEmails' 函数并在任务函数中按顺序执行所有操作

func prepareEmails() {
    getNames()
}

func getNames() {
    ...
    var task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in 
        getEmails(names)
    }) 
    task.resume()
}

func getEmails(names: NSArray) {
    ...
    var task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in 
        sendEmails(emails, names)
    }) 
    task.resume()
}

还有比后者更有效的设计吗?这是我对并发的第一次体验,所以任何建议将不胜感激。

调用具有 completionHandler 参数的异步方法时的典型模式是您自己使用 completionHandler 闭包模式。所以这些方法没有 return 任何东西,而是用 returned 信息作为参数调用闭包:

func getNames(completionHandler:(NSArray!)->()) {
    ....
    let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {data, response, error -> Void in 
        let names = ...
        completionHandler(names)
    }
    task.resume()
}

func getEmails(completionHandler:(NSArray!)->()) {
    ....
    let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {data, response, error -> Void in 
        let emails = ...
        completionHandler(emails)
    }
    task.resume()
}

然后,如果您需要按照代码示例的建议顺序执行这些操作(即,如果电子邮件的检索取决于 return 由 getNames 编辑的姓名),您可以这样做类似于:

func prepareEmails() {
    getNames() { names in 
        getEmails() {emails in
            sendEmails(names, emails) // I'm assuming the names and emails are in the input to this method
        }
    }
}

或者,如果他们可以 运行 同时进行,那么您应该这样做,因为这样会更快。诀窍是如何使第三个任务依赖于另外两个异步任务。两个传统的替代方案包括

  1. 将这些异步任务中的每一个包装在其自己的异步 NSOperation 中,然后创建依赖于其他两个操作的第三个任务。这可能超出了问题的范围,但你可以参考Operation Queue section of the Concurrency Programming Guide or see the Asynchronous vs Synchronous Operations and Subclassing Notes sections of the NSOperation Class Reference.

  2. 使用dispatch groups,在每个请求前进入group,在每个request的completion handler内离开group,然后添加一个dispatch group notification block(当所有group "enter" 调用与其相应的 "leave" 调用匹配):

    func prepareEmails() {
        let group = dispatch_group_create()
    
        var emails: NSArray!
        var names: NSArray!
    
        dispatch_group_enter(group)
        getNames() { results in
            names = results
            dispatch_group_leave(group)
        }
    
        dispatch_group_enter(group)
        getEmails() {results in
            emails = results
            dispatch_group_leave(group)
        }
    
        dispatch_group_notify(group, dispatch_get_main_queue()) {
            if names != nil && emails != nil {
                self.sendEmails(names, emails)
            } else {
                // one or both of those requests failed; tell the user
            }
        }
    }
    

坦率地说,如果有任何方法可以在单个网络请求中同时检索电子邮件和姓名,那将会更加高效。但是如果你遇到两个单独的请求,你可以像上面那样做。

注意,我通常不会在 Swift 代码中使用 NSArray,而是使用 String 对象数组(例如 [String])。此外,如果其中任何一个失败,我会在 return 错误的性质中加入错误处理。但希望这能说明 (a) 使用 completionHandler 块编写您自己的方法所涉及的概念; (b) 根据其他两个异步任务的完成调用第三位代码。

上面的答案(特别是 Rob 的基于 DispatchQueue 的答案)描述了 运行 两个任务并行并响应结果所必需的并发概念。为清楚起见,答案缺乏错误处理,因为传统上,并发问题的正确解决方案非常冗长。

HoneyBee不是这样。

HoneyBee.start()
        .setErrorHandler(handleErrorFunc)
        .branch {
             [=10=].chain(getNames)
             +
             [=10=].chain(getEmails)
        }
        .chain(sendEmails)

此代码片段管理所有并发,将所有错误路由到 handleErrorFunc 并且 看起来 像所需的并发模式。