URLSession.shared.datatask 和 dispatchgroup 的问题

issue with URLSession.shared.datatask and dispatchgroup

我对 swift 还是很陌生,所以请多多包涵。我有一个问题,这个应用程序在存档和网守签名后在我的开发机器上运行良好。但在其他用户机器上,它无法 return 变量。我想让它工作,所以捕获的东西可能需要一些工作,就像我在 swift

上说的我非常 new/green

    enum apiCalls: Error {
        case badRequest
    }
    
    func CheckPortSpeeds() {
        if NetMon.shared.isConnected == true {
            guard let url = URL(string: MdnUrl) else {
                return
            }
            var request = URLRequest(url: url )
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            let data = ["jsonrpc": "2.0", "method": "runCmds", "params": ["version": 1, "cmds": ["enable", "show interfaces ethernet1/1-30/1 status"], "format": "json"], "id": 1] as [String : Any]
            request.httpBody = try! JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted)
            var response : JSON
            var status : Int
            do {
                (response, status) = try APICaller().getResults(request: request)
            }
            catch apiCalls.badRequest {
                status = 400
                response = ["nil": "nil"]
                Alerts.shared.AlertUser(Message: "Host Not Reachable", Info: "Api Module")
            }
            catch SwiftyJSONError.notExist {
                status = 400
                response = ["nil": "nil"]
                Alerts.shared.AlertUser(Message: "Bad JSON response from host", Info: "Api Module")
            }
            catch {
                status = 400
                response = ["nil": "nil"]
                Alerts.shared.AlertUser(Message: "Unknown Error", Info: "Api Module")
            }
            if status == 200 {
                for n in 1...30 {
                    InterfaceDict["eth\(n)"] = Dictionary<String, String>()
                    InterfaceDict["eth\(n)"]?["portSpeed"] = response["result"][1]["interfaceStatuses"]["Ethernet\(n)/1"]["bandwidth"].int64
                    InterfaceDict["eth\(n)"]?["optic"] = response["result"][1]["interfaceStatuses"]["Ethernet\(n)/1"]["interfaceType"].string
                    guard let optic = InterfaceDict["eth\(n)"]?["optic"] as? String else {
                        return
                    }
                    InstalledOptic[n] = optic
                    guard let prtspd = InterfaceDict["eth\(n)"]?["portSpeed"] as? Int64 else {
                        return
                    }
                    PortSpeed[n] = prtspd
                }
                PortHandler().PortFlopper()
                ContentView().replies.responses += 1
            
                for r in 1...30 {
                    if PortMatch[r] == false {
                        FlipPorts(port: r, optic: InstalledOptic[r]!)
                    }
                }
            }
        }
        else{
            Alerts.shared.AlertUser(Message: "Network connection is down, please check netwok conection and try again", Info: "Network Monitor")
        }
    }

以上基本上是设置 api 调用等。

下面是我对 Arista 交换机的 eapi

进行 api 调用的地方

import Cocoa
import SwiftyJSON

class APICaller: NSObject {
    

    enum apiCalls: Error {
        case badRequest
    }
    
    func getResults(request: URLRequest) throws -> (JSON, Int) {
        var result: Result<(JSON, Int), Error> = .failure(SwiftyJSONError.notExist)
        let group = DispatchGroup()
        group.enter()
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            result = Result(catching: {
                if let e = error { throw e }
                guard let status = (response as? HTTPURLResponse)?.statusCode else {
                    throw apiCalls.badRequest
                }
                let json = try JSON(data: data ?? Data())
                return (json, status)
            })
            group.leave()
        }
        task.resume()
        group.wait()
        return try result.get()
    }
}

我已经尝试为此创建一个调度队列 运行 异步调度队列中的整个块,我尝试过同步,我尝试了一些疯狂的事情,但似乎没有就 return 变量而言,在用户机器上工作,它似乎并不等待。等待我的并且工作完美......我在这里有点困惑,api调用部分在帮助下被重写了几次,因为它最初是锁定用户机器上的线程并触发线程安全,现在它只是returns 一个 nil 变量并弹出我的自定义错误消息...

请...帮助....

您应该放弃这种尝试使用分派组来使固有异步 API 行为同步的方法。通常,建议您完全避免使用 wait(无论是调度组还是信号量或其他)。 wait 方法本质上是低效的(它阻塞了一个非常有限的工作线程)并且应该只由后台队列使用,如果你真的需要它,这里不是这种情况。

但是如果你在移动平台上阻塞主线程,不立即响应并且是同步的方法会导致非常糟糕的用户体验(应用程序将在此同步方法期间冻结),更糟糕的是,OS 如果您在错误的时间执行此操作,看门狗进程甚至可能会终止您的应用程序。

您正在调用异步方法,因此您的方法也应该采用异步模式。例如。与其尝试 return 值,不如为您的方法提供一个 @escaping 闭包参数,并在网络请求完成时调用该闭包。

考虑 getResults:您已经在那里雇用了 Result<(JSON, Int), Error>。所以让我们用它作为闭包的参数类型:

enum ApiError: Error {      // type names should start with upper case letter
    case badRequest
}

func getResults(request: URLRequest, completion: @escaping (Result<(JSON, Int), Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }

        guard
            let status = (response as? HTTPURLResponse)?.statusCode,
            let data = data
        else {
            completion(.failure(ApiError.badRequest))
            return
        }

        do {
            let json = try JSON(data: data)
            completion(.success((json, status)))
        } catch let parseError {
            completion(.failure(parseError))
        }
    }
    task.resume()
}

完成后,您将像这样使用它:

getResults(request: request) { result in
    switch result {
    case let .failure(error):
        // do something with error
    case let .success((json, statusCode)):
        // do something with json and statusCode
    }
}

// but do not use error, json, or statusCode here, because the above runs asynchronous (i.e. later)

请注意,如果您的目标是 iOS 15 或 macOS 12,Swift 现在有一个更直观的异步模式(称为 async/await),但我假设您正在尝试针对范围更广的 OS 版本,因此如果您需要支持较旧的 OS 版本,以上是传统技术。


顺便说一句,你提到它抛出关于“线程安全”的错误。这是一个非常不同的问题,可能是因为您正在尝试从后台线程执行 UI 更新,或者在您从多个线程更新某些共享 属性 时存在一些线程安全违规。

一个非常常见的模式是确保我们将完成处理程序分派回主线程。如果我们将所有内容分派回主队列,就很难引入线程安全问题或从错误的线程更新 UI。

例如,在上述方法的以下再现中,我将成功和失败条件分派回主队列:

func getResults(request: URLRequest, completion: @escaping (Result<(JSON, Int), Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        do {
            if let error = error { throw error }

            guard
                let status = (response as? HTTPURLResponse)?.statusCode,
                let data = data
            else {
                throw ApiError.badRequest
            }

            let json = try JSON(data: data)
            DispatchQueue.main.async {
                completion(.success((json, status)))
            }
        } catch let error {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
    task.resume()
}

如果你在所有这些之后仍然有“线程安全”问题,我建议你单独研究,如果你还有问题,post small reproducible example单独的问题。但是线程安全和异步模式是非常不同的问题。