Is There a Specific Way to Append Dispatchworkitems to a Dispatchqueue Instead of Re Declaring Them in Code

Is there a specific way to append DispatchWorkItems to a DispatchQueue instead of re declaring them in code?

Yes, you can have an array of DispatchWorkItem objects, but to dispatch them all, you’d just have to iterate through them, e.g., with either for-in or forEach:

let queue = DispatchQueue(label: "com.domain.app.requests")
let group = DispatchGroup()

let itemsToExecute: [DispatchWorkItem] = [item1, item2]

itemsToExecute.forEach { queue.async(group: group, execute: $0) }

group.notify(queue: .main) {
print("all done") // this is called when the requests are done
}

Note, I used async vs sync, because the whole point of using GCD is to avoid blocking the main queue, and while sync blocks, async doesn’t.

This begs the question of why you’d bother using an array of DispatchWorkItem at all, though. Just add the tasks to the queue directly, and the queue takes care of keeping track of all of them for you.


Frankly, we’d probably just want to use URLSession. For example:

@discardableResult
func request(from urlString: String, completion: @escaping (Result<String,Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: URL(string: urlString)!) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}

guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(NetworkError.invalidResponse(data, response)))
return
}

guard let string = String(data: data, encoding: .utf8) else {
completion(.failure(NetworkError.nonStringBody))
return
}

completion(.success(string))
}
task.resume()
return task
}

Where perhaps:

enum NetworkError: Error {
case invalidResponse(Data, URLResponse?)
case nonStringBody
}

Then, you can do something like:

for urlString in urlStrings {
group.enter()
request(from: urlString) { result in
defer { group.leave() }

switch result {
case .failure(let error):
print(urlString, error)

case .success(let string):
print(urlString, string.count)
}
}
}

group.notify(queue: .main) {
print("all done")
}

How can i wait to receive a response from a DispatchWorkItem before moving on to the next request or next DispatchWorkItem in a Dispatch Queue

I might advise against using semaphores or the like to block threads so that you can make asynchronous tasks behave synchronously, solely for the sake of DispatchWorkItem.

When I want to establish dependencies between asynchronous tasks, I have historically used Operation rather than DispatchWorkItem. (Admittedly, in iOS 13 and later, we might contemplate Combine’s Future/Promise, but for now operations are the way to go.) Operations have been designed to support wrapping of asynchronous processes much more elegantly than DispatchWorkItem. So you can use a queue whose maxConcurrentOperationCount is 1, like so:

let networkQueue = OperationQueue()
networkQueue.maxConcurrentOperationCount = 1

let completionOperation = BlockOperation {
print("all done")
}

for url in urls {
let operation = NetworkOperation(url: url) { result in
switch result {
case .failure(let error):
...

case .success(let data):
...
}
}
completionOperation.addDependency(operation)
networkQueue.addOperation(operation)
}

OperationQueue.main.addOperation(completionOperation)

Or you can use a more reasonable maxConcurrentOperationCount and use dependencies only between those operations where you need this sequential behavior:

let networkQueue = OperationQueue()
networkQueue.maxConcurrentOperationCount = 4

let completionOperation = BlockOperation {
print("all done")
}

var previousOperation: Operation?

for url in urls {
let operation = NetworkOperation(url: url) { result in
switch result {
case .failure(let error):
...

case .success(let data):
...
}
}
if let previousOperation = previousOperation {
operation.addDependency(previousOperation)
}
completionOperation.addDependency(operation)
networkQueue.addOperation(operation)
previousOperation = operation
}

OperationQueue.main.addOperation(completionOperation)

This is what that NetworkOperation might look like:

class NetworkOperation: AsynchronousOperation {
typealias NetworkCompletion = (Result<Data, Error>) -> Void

enum NetworkError: Error {
case invalidResponse(Data, URLResponse?)
}

private var networkCompletion: NetworkCompletion?
private var task: URLSessionTask!

init(request: URLRequest, completion: @escaping NetworkCompletion) {
super.init()

task = URLSession.shared.dataTask(with: request) { data, response, error in
defer {
self.networkCompletion = nil
self.finish()
}

guard let data = data, error == nil else {
self.networkCompletion?(.failure(error!))
return
}

guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
self.networkCompletion?(.failure(NetworkError.invalidResponse(data, response)))
return
}

self.networkCompletion?(.success(data))
}
networkCompletion = completion
}

convenience init(url: URL, completion: @escaping NetworkCompletion) {
self.init(request: URLRequest(url: url), completion: completion)
}

override func main() {
task.resume()
}

override func cancel() {
task.cancel()
}
}

This is passing back Data, but you can write permutations/subclasses that further parse that into whatever your web service is returning using JSONDecoder or whatever. But hopefully this illustrates the basic idea.

The above uses this AsynchronousOperation class:

/// Asynchronous operation base class
///
/// This is abstract to class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and
/// implement asynchronous operations. All you must do is:
///
/// - override `main()` with the tasks that initiate the asynchronous task;
///
/// - call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `finish()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `finish()` is called.

public class AsynchronousOperation: Operation {

/// State for this operation.

@objc private enum OperationState: Int {
case ready
case executing
case finished
}

/// Concurrent queue for synchronizing access to `state`.

private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)

/// Private backing stored property for `state`.

private var _state: OperationState = .ready

/// The state of the operation

@objc private dynamic var state: OperationState {
get { stateQueue.sync { _state } }
set { stateQueue.sync(flags: .barrier) { _state = newValue } }
}

// MARK: - Various `Operation` properties

open override var isReady: Bool { return state == .ready && super.isReady }
public final override var isAsynchronous: Bool { return true }
public final override var isExecuting: Bool { return state == .executing }
public final override var isFinished: Bool { return state == .finished }

// KVN for dependent properties

open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if ["isReady", "isFinished", "isExecuting"].contains(key) {
return [#keyPath(state)]
}

return super.keyPathsForValuesAffectingValue(forKey: key)
}

// Start

public final override func start() {
if isCancelled {
state = .finished
return
}

state = .executing

main()
}

/// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.

open override func main() {
fatalError("Subclasses must implement `main`.")
}

/// Call this function to finish an operation that is currently executing

public final func finish() {
if isExecuting { state = .finished }
}
}

There are lots of ways to write a base AsynchronousOperation, and I don’t want to get lost in the details, but the idea is that we now have an Operation that we can use for any asynchronous process.

Swift iOS -DispatchWorkItem is still running even though it's getting Cancelled and Set to Nil

This cancel routine is not doing what I suspect you think it is. When you cancel a DispatchWorkItem, it performs no preemptive cancellation. It certainly has no bearing on the updateChildValues call. All it does is perform a thread-safe setting of the isCancelled property, which if you were manually iterating through a loop, you could periodically check and exit prematurely if you see that the task was canceled.

As a result, the checking of isCancelled at the start of the task isn't terribly useful pattern, because if the task has not yet been created, there is nothing to cancel. Or if the task has been created and added to a queue, and canceled before the queue had a chance to start, it will obviously just be canceled but never started, you'll never get to your isCancelled test. And if the task has started, it's likely gotten past the isCancelled test before cancel was called.

Bottom line, attempts to time the cancel request so that they are received precisely after the task has started but before it has gotten to the isCancelled test is going to be an exercise in futility. You have a race that will be almost impossible to time perfectly. Besides, even if you did happen to time this perfectly, this merely demonstrates how ineffective this whole process is (only 1 in a million cancel requests will do what you intended).

Generally, if you had asynchronous task that you wanted to cancel, you'd wrap it in an asynchronous custom Operation subclass, and implement a cancel method that stops the underlying task. Operation queues simply offer more graceful patterns for canceling asynchronous tasks than dispatch queues do. But all of this presumes that the underlying asynchronous task offers a mechanism for canceling it and I don't know if Firebase even offers a meaningful mechanism to do that. I certainly haven't seen it contemplated in any of their examples. So all of this may be moot.

I'd suggest you step away from the specific code pattern in your question and describe what you are trying to accomplish. Let's not dwell on your particular attempted solution to your broader problem, but rather let's understand what the broader goal is, and then we can talk about how to tackle that.


As an aside, there are other technical issues in your example.

Specifically, I'm assuming you're running this on the main queue. So task.perform() runs it on the current queue immediately. But your DispatchQueue.main.asyncAfter(...) can only be run when whatever is running on the main queue is done. So, even though you specified a delay of 0.0000000001 seconds, it actually won't run until the main queue is available (namely, after your perform is done running on the main queue and you're well past the isCancelled test).

If you want to test this race between running the task and canceling the task, you need to perform the cancel on a different thread. For example, you could try:

weak var task: DispatchWorkItem?

let item = DispatchWorkItem {
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}

DispatchQueue.global().asyncAfter(deadline: .now() + 0.00001) {
task?.cancel()
}

task = item
DispatchQueue.main.async {
item.perform()
}

Now you can play with various delays and see the different behavior between a delay of 0.1 seconds and one of 0.0000000001 seconds. And you'll want to make sure the app has reached quiescence before you try this test (e.g. do it on a button press event, not in viewDidLoad).

But again, this will merely illustrate the futility of the whole exercise. You're going to have a really hard time catching the task between the time it started and before it checked the isCancelled property. If you really want to manifest the cancel logic in some repeatable manner, we're going to have to artificially make this happen:

weak var task: DispatchWorkItem?

let queue = DispatchQueue(label: "com.domain.app.queue") // create a queue for our test, as we never want to block the main thread

let semaphore = DispatchSemaphore(value: 0)

let item = DispatchWorkItem {
// You'd never do this in a real app, but let's introduce a delay
// long enough to catch the `cancel` between the time the task started.
//
// You could sleep for some interval, or we can introduce a semphore
// to have it not proceed until we send a signal.

print("starting")
semaphore.wait() // wait for a signal before proceeding

// now let's test if it is cancelled or not

if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}

DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
task?.cancel()
semaphore.signal()
}

task = item
queue.async {
item.perform()
}

Now, you'd never do this, but it just illustrates that isCancelled does work.

Frankly, you'd never use isCancelled like this. You would generally use the isCancelled process if doing some long process where you can periodically check the isCancelled status and exit if it is true. But that's not the case in your situation.

The conclusion of all of this is that checking isCancelled at the start of a task is unlikely to ever achieve what you had hoped for.

Swift GCD Work Item Dispatch queue cancelling

You need to declare DispatchWorkItem variable at the top of class. In your code work1 variable is becoming inaccessible as soon as compiler is out of function. Every time you change your switch a new DispatchWorkItem variable initialize. Please go through below example for correct use of DispatchWorkItem with stop/resume functionality

    @IBOutlet weak var button: UIButton!
@IBOutlet weak var label: UILabel!
var isStart = false
var work: DispatchWorkItem!

private func getCurrentTime() -> DispatchWorkItem {

work = DispatchWorkItem { [weak self] in

while true {
if (self?.work.isCancelled)!{break}
let date = Date()
let component = Calendar.current.dateComponents([.second], from: date)
DispatchQueue.main.async {
self?.label.text = "\(component.second!)"
}

}

}

return work
}

@IBAction func btnPressed() {

isStart = !isStart
button.setTitle(isStart ? "Stop" : "Start", for: .normal)

let workItem = getCurrentTime()
let globalQueue = DispatchQueue.global(qos: .background)
if isStart {
globalQueue.async(execute: workItem)
} else {
workItem.cancel()
}

}

This example will display current time seconds value in label. I have used while true, just to show an example.

Stop a DispatchQueue that is running on the main thread

You can use DispatchWorkItems. They can be scheduled on DispatchQueues and cancelled before their execution.

let work = DispatchWorkItem(block: {
self.isShootingOnHold = false
self.shoot()
self.shootingEngine = Timer.scheduledTimer(timeInterval: (Double(60)/Double(self.ratePerMinute)), target: self, selector: #selector(ShootingEnemy.shoot), userInfo: nil, repeats: true)
})
DispatchQueue.main.asyncAfter(deadline: .now() + (delay * Double(isDelayAccounted.hashValue)) + extraDelay, execute: work)
work.cancel()

Swift Cancel DispatchQueue Process

There is no way to stop or "kill" a DispatchWorkItem or NSOperation from outside. There is a cancel() method, but that merely sets the isCancelled property of the item or operation to true. This does not stop the execution of the item itself. Ans since recv is blocking, there is no way to check the isCancelled flag during execution. This means the answer posted by Vadian unfortunately wouldn't do anything.

According to the Apple docs on NSOperation.cancel:

This method does not force your operation code to stop.

The same goes for NSOperationQueue.cancelAllOperations:

Canceling the operations does not automatically remove them from the queue or stop those that are currently executing.

You might think it is possible to drop down to using a raw NSThread. However, the same principle applies hier. You cannot deterministically kill a thread from the outside.

Possible solution: timeout

The best solution I can think of is to use the timeout feature of the socket. I don't know where UDPServer comes from, but perhaps it has a built in timeout.

Possible solution: Poor man's timeout (send packet to localhost)

Another option you can try is to send some UDP packets to yourself after a certain time has elapsed. This way, recv will receive some data, and execution will continue. This could possibly be used as a "poor man's timeout".

How to handle group wait result in Swift 3

This code snippet raises a variety of different questions:

  1. I notice that the behavior differs a bit between the playground and when you run it in an app. I suspect it's some idiosyncrasy of needsIndefiniteExecution of PlaygroundPage and GCD. I'd suggest testing this in an app. With the caveat of the points I raise below, it works as expected when I ran this from an app.

  2. I notice that you've employed this pattern:

    __dispatch_group_async(group, queue) {
    ...
    }

    I would suggest:

    queue.async(group: group) {
    ...
    }
  3. You are doing group.suspend(). A couple of caveats:

    • One suspends queues, not groups.

    • And if you ever call suspend(), make sure you have a corresponding call to resume() somewhere.

    • Also, remember that suspend() stops future blocks from starting, but it doesn't do anything with the blocks that may already be running. If you want to stop blocks that are already running, you may want to cancel them.

    • Finally, note that you can only suspend queues and sources that you create. You can't (and shouldn't) suspend a global queue.

  4. I also notice that you're using wait on the same queue that you dispatched the test() call. In this case, you're getting away with that because it is a concurrent queue, but this sort of pattern invites deadlocks. I'd suggest avoiding wait altogether if you can, and certainly don't do it on the same queue that you called it from. Again, it's not a problem here, but it's a pattern that might get you in trouble in the future.

    Personally, I might be inclined to use notify rather than wait to trigger the block of code to run when the two dispatched blocks are done. This eliminates any deadlock risk. And if I wanted to have a block of code to run after a certain amount of time (i.e. a timeout process), I might use a timer to trigger some cleanup process in case those two blocks were still running (perhaps canceling them; see How to stop a DispatchWorkItem in GCD?).

Is it possible that UI view item no longer valid, after we finish executing DispatchWorkItem and attempt to update UI with DispatchQueue.main.async

I was wondering, during UI updating, is there ever be a chance, that the UI is destroy, or no longer valid?

If the view controller in question has been dismissed, the view will be removed from the view hierarchy. But, the way you’ve written this, if the view controller has been dismissed, your code will update a view that isn’t visible any more. Worse, the memory associated with your view controller and its views be not deallocated until this dispatched block finishes. There’s no point in doing that.

So, if we were to use your code pattern, you might instead do:

@IBAction func buttonClick(_ sender: Any) {
recordLabel.text = "Perform some time consuming networking..."

let workItem = DispatchWorkItem { // use `[weak self] in` pattern here, too, if you reference `self` anywhere
// Perform some time consuming networking...

DispatchQueue.main.async { [weak self] in
// Update the view if it’s still around, but don’t if not
self?.recordLabel.text = "Done"
}
}
DispatchQueue.global().async(execute: workItem)
}

Or, more naturally,

@IBAction func buttonClick(_ sender: Any) {
recordLabel.text = "Perform some time consuming networking..."

DispatchQueue.global().async { // use `[weak self] in` pattern here, too, if you reference `self` anywhere
// Perform some time consuming networking...

DispatchQueue.main.async { [weak self] in
// Update the view if it’s still around, but don’t if not
self?.recordLabel.text = "Done"
}
}
}

It is worth noting that one generally doesn’t dispatch network requests to a global queue because networking libraries like URLSession, Alamofire, etc., already perform their request asynchronously. So you wouldn’t do that async dispatch to the global queue.

Likewise, if this network request was merely to update something for this view controller’s view, you might even cancel the network request when the view was dismissed. (Why bother continuing to do a network request merely to update a view that might no longer exist?) It depends upon the nature of the request.

Finally, when you get this immediate issue behind you, you might reconsider whether the view controller should be issuing network requests at all, as opposed to some other type of object. That’s well beyond the scope of this question, but something to reconsider in the long term.

But we can’t comment further on any of these observations without seeing what you’re doing inside this dispatch to the global queue.



Related Topics



Leave a reply



Submit