【问题标题】:Multiple workers in Swift Command Line ToolSwift 命令行工具中的多个工作人员
【发布时间】:2021-12-27 19:01:31
【问题描述】:

在 Swift 中编写命令行工具 (CLT) 时,我想处理大量数据。我已经确定我的代码受 CPU 限制,并且性能可以从使用多个内核中受益。因此,我想并行化部分代码。假设我要实现以下伪代码:

Fetch items from database
Divide items in X chunks
Process chunks in parallel
Wait for chunks to finish
Do some other processing (single-thread)

现在我一直在使用 GCD,一个幼稚的方法看起来像这样:

let group = dispatch_group_create()
let queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT)
for chunk in chunks {
    dispatch_group_async(group, queue) {
        worker(chunk)
    }
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)

但是 GCD 需要一个运行循环,因此代码将挂起,因为该组永远不会执行。 runloop 可以用dispatch_main() 启动,但它永远不会退出。也可以在几秒钟内运行NSRunLoop,但这并不是一个可靠的解决方案。不考虑 GCD,用 Swift 怎么实现呢?

【问题讨论】:

  • GCD 不需要运行循环 - 但您的代码可能会向主线程提交块,在这种情况下您需要调用 dispatch_main 或使用运行循环。
  • @CouchDeveloper 没有运行循环提交给主线程的块不会运行对吗?因此需要一个运行循环来运行它们,即使dispatch_main 也会在后台创建一个运行循环。
  • dispatch_main 不一定需要创建运行循环。其实我相信不会。这是执行提交到主队列的块的一种方法。是的,它永远不会返回,这在许多应用程序中可能不是很有意义。但是,我相信,如果您不向主线程分派块,则应用程序应该可以在没有 dispatch_main 和运行循环的情况下正常运行(使用 dispatch_groups 等待完成)。

标签: swift


【解决方案1】:

我错误地将锁定线程解释为挂起程序。没有运行循环,这项工作将执行得很好。问题中的代码将运行良好,并阻塞主线程,直到整个组完成。

假设chunks 包含 4 项工作负载,以下代码启动 4 个并发工作人员,然后等待所有工作人员完成:

let group = DispatchGroup()
let queue = DispatchQueue(label: "", attributes: .concurrent)

for chunk in chunk {
    queue.async(group: group, execute: DispatchWorkItem() {
        do_work(chunk)
    })
}

_ = group.wait(timeout: .distantFuture)

【讨论】:

  • 为了帮助未来的读者,您能否发布最终解决方案的源代码?
  • 感谢您更新您的答案!
【解决方案2】:

就像使用 Objective-C CLI 一样,您可以使用 NSRunLoop 创建自己的运行循环。

这是一种可能的实现,以 this gist 为模型:

class MainProcess {
    var shouldExit = false

    func start () {
        // do your stuff here
        // set shouldExit to true when you're done
    }
}

println("Hello, World!")

var runLoop : NSRunLoop
var process : MainProcess

autoreleasepool {
    runLoop = NSRunLoop.currentRunLoop()
    process = MainProcess()

    process.start()

    while (!process.shouldExit && (runLoop.runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 2)))) {
        // do nothing
    }
}

正如 Martin 指出的那样,您可以使用 NSDate.distantFuture() as NSDate 而不是 NSDate(timeIntervalSinceNow: 2)。 (强制转换是必要的,因为distantFuture() 方法签名表明它返回AnyObject。)

如果您需要访问 CLI 参数 see this answer。你也可以return exit codes using exit()

【讨论】:

  • 我不禁认为这是非常丑陋/不可读的代码。为什么不手动处理几个线程呢?
  • 您实际上可以使用“遥远的未来”而不是两秒,比较stackoverflow.com/a/25126900/1187415。如果处理了任何调度源,runMode() 将始终返回。
  • @bouke 你可以使用线程来代替,但我不想在这里重复threads vs. GCD
  • @AaronBrager 我并不是要将此作为线程与 GCD 的问题。但是,我正在寻找一种很好的方法来实现这个用例。用一个简单易懂的代码示例为这个用例建立一个最佳实践。
  • @AaronBrager 感谢您的回复。您的回答帮助我发现最终不需要运行循环。如果锁定主线程是目标,dispatch_group_wait 是你的朋友。
【解决方案3】:

Aaron Brager 解决方案的 Swift 3 最小实现,它简单地结合了 autoreleasepoolRunLoop.current.run(...) 直到你打破循环:

var shouldExit = false
doSomethingAsync() { _ in
    defer {
        shouldExit = true
    }
}
autoreleasepool {
    var runLoop = RunLoop.current
    while (!shouldExit && (runLoop.run(mode: .defaultRunLoopMode, before: Date.distantFuture))) {}
}

【讨论】:

    【解决方案4】:

    在这种情况下,我认为CFRunLoopNSRunLoop 容易得多

    func main() {
        /**** YOUR CODE START **/
        let group = dispatch_group_create()
        let queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT)
        for chunk in chunks {
            dispatch_group_async(group, queue) {
                worker(chunk)
            }
        }
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
        /**** END **/
    }
    
    
    let runloop = CFRunLoopGetCurrent()
    CFRunLoopPerformBlock(runloop, kCFRunLoopDefaultMode) { () -> Void in
        dispatch_async(dispatch_queue_create("main", nil)) {
            main()
            CFRunLoopStop(runloop)
        }
    }
    CFRunLoopRun()
    

    【讨论】:

    • 感谢您的回答,此代码看起来已经比使用 NSRunLoop 更好。但是,如果阻塞主线程没问题(在我的用例中),dispatch_group_wait 工作得很好,我刚刚发现。
    猜你喜欢
    • 2015-10-11
    • 2014-11-16
    • 1970-01-01
    • 2022-01-05
    • 2013-11-05
    • 2016-01-10
    • 2017-08-16
    • 1970-01-01
    相关资源
    最近更新 更多