Создание подкласса OperationQueue для добавления периода сна

      import Foundation

class MyOperationQueue {
    
    static let shared = MyOperationQueue()
    
    private var queue: OperationQueue
    
    init() {
        self.queue = OperationQueue()
        queue.name = "com.myqueue.name"
        queue.maxConcurrentOperationCount = 1
        queue.qualityOfService = .background
    }
    
    func requestDataOperation() {
        queue.addOperation {
            print("START NETWORK \(Date())")
            NetworkService.shared.getData()
            print("END   NETWORK \(Date())")
        }
    }
    
    func scheduleSleep() {
        queue.cancelAllOperations()
        queue.addOperation {
            print("SLEEP START \(Date())")
            Thread.sleep(forTimeInterval: 5)
            print("SLEEP END   \(Date())")
        }
    }
    
    func cancelAll() {
        queue.cancelAllOperations()
    }
}

я кладу requestDataOperationфункция внутри таймера для каждого 10-секундного интервала. И у меня есть кнопка для звонка scheduleSleepвручную. Ожидалось, что я буду отклонять запрос каждые 5 секунд, когда нажимаю кнопку.

Но я получаю что-то вроде этого:

      START NETWORK
END   NETWORK
SLEEP START   2021-03-11 11:13:40 +0000
SLEEP END     2021-03-11 11:13:45 +0000
SLEEP START   2021-03-11 11:13:45 +0000
SLEEP END     2021-03-11 11:13:50 +0000
START NETWORK
END   NETWORK

Как добавить еще 5 секунд с момента последнего нажатия и объединить их вместе, а не разбивать на две операции? Я звоню queue.cancelAllOperations и начать новую операцию сна, но, похоже, не работает.

Ожидайте результата:

      START NETWORK
END   NETWORK
SLEEP START   2021-03-11 11:13:40 +0000
// <- the second tap when 2 seconds passed away
SLEEP END     2021-03-11 11:13:47 +0000  // 2+5
START NETWORK
END   NETWORK

1 ответ

Если вы хотите, чтобы какая-то операция была отложена на определенное время, я бы не создавал класс «очередь», а просто определял бы класс, которого просто не будет, пока не пройдет это время (например, пять секунд спустя). Это не только устраняет необходимость в двух отдельных «операциях сна», но и полностью исключает их.

Например,

      class DelayedOperation: Operation {
    @Atomic private var enoughTimePassed = false
    private var timer: DispatchSourceTimer?
    private var block: (() -> Void)?

    override var isReady: Bool { enoughTimePassed && super.isReady }    // this operation won't run until (a) enough time has passed; and (b) any dependencies or the like are satisfied

    init(timeInterval: TimeInterval = 5, block: @escaping () -> Void) {
        self.block = block
        super.init()
        resetTimer(for: timeInterval)
    }

    override func main() {
        block?()
        block = nil
    }

    func resetTimer(for timeInterval: TimeInterval = 5) {
        timer = DispatchSource.makeTimerSource()                        // create GCD timer (eliminating reference to any prior timer will cancel that one)
        timer?.setEventHandler { [weak self] in
            guard let self = self else { return }
            self.willChangeValue(forKey: #keyPath(isReady))             // make sure to do necessary `isReady` KVO notification
            self.enoughTimePassed = true
            self.didChangeValue(forKey: #keyPath(isReady))
        }
        timer?.schedule(deadline: .now() + timeInterval)
        timer?.resume()
    }
}

Я синхронизирую свое взаимодействие с enoughTimePassed со следующей оболочкой свойств, но вы можете использовать любой механизм синхронизации, который захотите:

      @propertyWrapper
struct Atomic<Value> {
    private var value: Value
    private var lock = NSLock()

    init(wrappedValue: Value) {
        value = wrappedValue
    }

    var wrappedValue: Value {
        get { synchronized { value } }
        set { synchronized { value = newValue } }
    }

    private func synchronized<T>(block: () throws -> T) rethrows -> T {
        lock.lock()
        defer { lock.unlock() }
        return try block()
    }
}

Просто убедитесь, что это потокобезопасный.

Во всяком случае, определив, что DelayedOperation, тогда вы можете сделать что-то вроде

      logger.debug("creating operation")

let operation = DelayedOperation {
    logger.debug("some task")
}

queue.addOperation(operation)

И это задержит выполнение этой задачи (в данном случае просто войдите в систему с сообщением «какая-то задача») на пять секунд. Если вы хотите сбросить таймер, просто вызовите этот метод в подклассе операции:

      operation.resetTimer()

Например, здесь я создал задачу, добавил ее в очередь, сбросил ее три раза с интервалом в две секунды, и блок фактически запускается через пять секунд после последнего сброса:

      2021-09-30 01:13:12.727038-0700 MyApp[7882:228747] [ViewController] creating operation
2021-09-30 01:13:14.728953-0700 MyApp[7882:228747] [ViewController] delaying operation
2021-09-30 01:13:16.728942-0700 MyApp[7882:228747] [ViewController] delaying operation
2021-09-30 01:13:18.729079-0700 MyApp[7882:228747] [ViewController] delaying operation
2021-09-30 01:13:23.731010-0700 MyApp[7882:228829] [ViewController] some task

Теперь, если вы используете операции для сетевых запросов, то вы, вероятно, уже реализовали свой собственный асинхронный подкласс, который выполняет необходимые KVO для isFinished, isExecutingи т. д., так что вы можете жениться на вышеупомянутом isReady логика с существующим Operation подкласс.

Но идея состоит в том, чтобы полностью потерять операцию «сна» (которая блокирует один из очень ограниченного числа рабочих потоков) с помощью асинхронного шаблона.


С учетом всего вышесказанного, если бы я лично хотел отклонить сетевой запрос, я бы не стал интегрировать это в операцию или очередь операций. Я бы просто сделал это отбой в то время, когда я начал запрос:

      weak var timer: Timer?

func debouncedRequest(in timeInterval: TimeInterval = 5) {
    timer?.invalidate()
    timer = .scheduledTimer(withTimeInterval: timeInterval, repeats: false) { _ in
        // initiate request here
    }
}
Другие вопросы по тегам