3

I've got a function which is called by observing the NotificationCenter:

NotificationCenter.default.addObserver(self, selector: #selector(observedPosition(_: ), name: "calculatePosition", object: nil)

and then the function:

@objc func observedPosition(_ notification: NSNotification) {
   if let data = notification.object as? Int {
      self.sendPosition(from: data)
   }

As this function can be called multiple times in very short time periods I would like to add it to the queue and call sendPosition() only once the previous sendPosition() has finished.

I tried something like this but dunno if it's a correct approach:

@objc func observedPosition(_ notification: NSNotification) {
    let queue = DispatchQueue(label: queueLabel, attributes: [], targer: nil)
    queue.sync {
        if let data = notification.object as? Int {
            self.sendPosition(from: data)
        }
    }
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
mikro098
  • 2,173
  • 2
  • 32
  • 48

2 Answers2

3

Details

  • Xcode Version 10.3 (10G8), Swift 5

Key features

  • Implemented own queue which will execute functions one by one
  • All operations (closures) stored in array
  • Thread safety

Solution

// MARK: - StackableOperationsQueue performs functions from the stack one by one (serial performing)

class StackableOperationsQueue {
    private let semaphore = DispatchSemaphore(value: 1)
    private lazy var operations = [QueueOperation]()
    private lazy var isExecuting = false

    fileprivate func _append(operation: QueueOperation) {
        semaphore.wait()
        operations.append(operation)
        semaphore.signal()
        execute()
    }

    func append(operation: QueueOperation) { _append(operation: operation) }

    private func execute() {
        semaphore.wait()
        guard !operations.isEmpty, !isExecuting else { semaphore.signal(); return }
        let operation = operations.removeFirst()
        isExecuting = true
        semaphore.signal()
        operation.perform()
        semaphore.wait()
        isExecuting = false
        semaphore.signal()
        execute()
    }
}

// MARK: - StackableOperationsCuncurentQueue performs functions from the stack one by one (serial performing) but in cuncurent queue

class StackableOperationsCuncurentQueue: StackableOperationsQueue {
    private var queue: DispatchQueue
    init(queue: DispatchQueue) { self.queue = queue }
    override func append(operation: QueueOperation) {
        queue.async { [weak self] in self?._append(operation: operation) }
    }
}

// MARK: QueueOperation interface

protocol QueueOperation: class {
    var сlosure: (() -> Void)? { get }
    var actualityCheckingClosure: (() -> Bool)? { get }
    init (actualityCheckingClosure: (() -> Bool)?, serialClosure: (() -> Void)?)
    func perform()
}

extension QueueOperation {
    // MARK: - Can queue perform the operation `сlosure: (() -> Void)?` or not
    var isActual: Bool {
        guard   let actualityCheckingClosure = self.actualityCheckingClosure,
                self.сlosure != nil else { return false }
        return actualityCheckingClosure()
    }
    func perform() { if isActual { сlosure?() } }

    init (actualIifNotNill object: AnyObject?, serialClosure: (() -> Void)?) {
        self.init(actualityCheckingClosure: { return object != nil }, serialClosure: serialClosure)
    }
}

class SerialQueueOperation: QueueOperation {
    let сlosure: (() -> Void)?
    let actualityCheckingClosure: (() -> Bool)?
    required init (actualityCheckingClosure: (() -> Bool)?, serialClosure: (() -> Void)?) {
        self.actualityCheckingClosure = actualityCheckingClosure
        self.сlosure = serialClosure
    }
}

Usage example

class TEST {

    private lazy var stackableOperationsQueue: StackableOperationsCuncurentQueue = {
        let queue = DispatchQueue(label: "custom_queue", qos: .background,
                                  attributes: [.concurrent], autoreleaseFrequency: .workItem, target: nil)
        return StackableOperationsCuncurentQueue(queue: queue)
    }()

    private func addOperationToQueue(closure: (() -> Void)?) {
        let operation = SerialQueueOperation(actualIifNotNill: self) { closure?() }
        stackableOperationsQueue.append(operation: operation)
        print("!!!! Function added ")
    }

    private func simpleFunc(index: Int) {
        print("Func \(index) started")
        sleep(UInt32(index+1));
        print("Func \(index) ended")
    }

    func run() {
        (0...3).forEach { index in
            addOperationToQueue { [weak self] in self?.simpleFunc(index: index) }
        }
    }
}

let test = TEST()
test.run()

Usage example results

//  qos: .background
!!!! Function added 
!!!! Function added 
!!!! Function added 
!!!! Function added 
Func 0 started
Func 0 ended
Func 1 started
Func 1 ended
Func 2 started
Func 2 ended
Func 3 started
Func 3 ended


//  qos: .userInitiated
!!!! Function added 
Func 0 started
!!!! Function added 
!!!! Function added 
!!!! Function added 
Func 0 ended
Func 1 started
Func 1 ended
Func 2 started
Func 2 ended
Func 3 started
Func 3 ended
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
  • hi buddy, i got error "Cannot find type 'StackableOperationsCuncurentQueue' in scope", did u define it yet? – famfamfam Feb 28 '21 at 12:44
  • 1
    @famfamfam I just checked to code - it works. Check please that you copied all the code – Vasily Bodnarchuk Mar 02 '21 at 16:10
  • thanks, can i ask this question? I have a chat screen + add new message to chat when have new message(work perfect), but sometimes when another VC presented above this ChatScreen, i got some layout issues, i think may be ChatScreen is not fully shown -> got this error, so i want to add new message to your defined queue, and call synchronized to add to ChatScreen, may it work with your solution? Thanks so much – famfamfam Mar 02 '21 at 16:34
  • @famfamfam it is difficult to help you without any code. I suggest you to create new question and share some code there. – Vasily Bodnarchuk Mar 09 '21 at 14:36
1

That is correct, so long as you ensure the same queue is being used to schedule all sendPosition method calls. For example, if this queue were a local variable, it would be of no use at all.

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • I've updated the code, it's a variable inside the function called by `NotificationCenter`. Actually my question was supposed to be about this `queue` as a variable. – mikro098 Jan 04 '18 at 21:26
  • @codddeer123 As it is in your post, that queue won't help at all. Every `observedPosition(_:)` call pins up a new, separate queue, which will immediately execute that task, without any regard for the other queues before it are doing. You'll need one queue that's shared among all invocations of `observedPosition(_:)` – Alexander Jan 04 '18 at 21:28
  • ok, so should I just put `var queue` outside this function and then call `queue.sync { }` as I did in the post? – mikro098 Jan 04 '18 at 21:49
  • @codddeer123 Well preferably `let queue`, since there's only one queue and you won't be reassigning to it. Now, where exactly you put this queue depends on your requirements. Is it necessary for `observedPosition` to ensure exclusivity between `sendPosition` calls, or is it necessary for all `sendPosition` calls (regardless of caller) to always be exclusive? – Alexander Jan 04 '18 at 21:55
  • sendPosition regardless of caller is preferable – mikro098 Jan 04 '18 at 23:49
  • 1
    So then it's better to enforce that within the implementation of `sendPosition(from:)`. You can take the existing entire body of `sendPosition` and wrap it in the `queue.sync` call. You could also employ this queue in a similar fashion for all other calls you want to be mutually exclusive with the `sendPosition(from:)` – Alexander Jan 05 '18 at 00:24
  • @Alexander You want to say if `sendPosition(from:)` will be wrap in `queue.sync` then it will call only one at a time ? . for multiple calls. How to schedule others call in queue ? Sorry I am understanding you here – Prashant Tukadiya Jan 05 '18 at 04:59
  • "How to schedule others call in queue?" I don't understand what you're asking. If the contents of the `sendPosition(from:)` were wrapped in a `queue.sync { ... }`, then every call to `sendPosition(from:)` will line up in the queue, 1 by 1, and wait until other invocations of the function finish before they start. – Alexander Jan 05 '18 at 05:32
  • hm, I thought that it will work but the problem seems to be closure inside `sendPosition()` function. When it enters the closure it gets quite random and doesn't execute serially. – mikro098 Jan 05 '18 at 08:54
  • Are you storing your queue as an instance property of your class? I.e. are you sure you have one queue shared among all calls? – Alexander Jan 05 '18 at 17:23