7

I have a UDP method that waits for a reply using the DispatchQueue using the following code:

DispatchQueue.global(qos: .userInitiated).async {
    let server:UDPServer=UDPServer(address:"0.0.0.0", port:5005)
    let (data,_,_) = server.recv(1024)
    DispatchQueue.main.async {
       ...
    }
}

This works perfectly and sets off a process to wait for my data to come in. What's keeping me up at night is what happens if we never get a reply? server.recv never returns so I cannot see how the process will ever end? Is there a way of giving it a predetermined amount of time to run for?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
iphaaw
  • 6,764
  • 11
  • 58
  • 83
  • see the discussion in this post: https://stackoverflow.com/questions/38105105/difference-between-dispatching-to-a-queue-with-sync-and-using-a-work-item-with/38108546#38108546 – E.Coms Jan 17 '19 at 23:07
  • It doesn't have a built in timeout feature that I know of. The only thing I can think of is using `cancel()` and checking the `isCancelled` property from inside the callback. However this doesn't work if `recv` is blocking. – Rengers Jan 22 '19 at 14:54
  • 1
    How is `UDPServer` implemented? How does `recv` look like? Without seeing the actual blocking code is hard to give you suggestions. – Cristik Jan 27 '19 at 06:06
  • I'm using SwiftSockets. – iphaaw Jan 28 '19 at 20:48
  • Took a look at the `SwiftSockets`, looks like what you want is not possible, because `SwiftSockets` don't provide the functionality specified in https://stackoverflow.com/questions/13547721/udp-socket-set-timeout – Cristik Jan 28 '19 at 21:29
  • If SwiftSockets does not support timeouts, you might be able to simulate a fake timeout by sending yourself some UDP data. I've updated my answer. – Rengers Jan 29 '19 at 10:27

3 Answers3

8

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".

Rengers
  • 14,911
  • 1
  • 36
  • 54
5

Add a timer which fires after a specific timeout interval

Declare a constant for the timeout interval and a property for the timer

private let timeoutSeconds = 30
private var timer : DispatchSourceTimer?

and write two functions to start and stop the timer.

fileprivate func startDispatchTimer()
{
    let interval : DispatchTime = .now() + .seconds(timeoutSeconds)
    if timer == nil {
        timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
        timer!.schedule(deadline:interval)
        timer!.setEventHandler {
            // do something when the timer fires
            self.timer = nil
        }
        timer!.resume()
    }
}

fileprivate func stopDispatchTimer()
{
    timer?.cancel()
    timer = nil
}

Start the timer after initializing the server instance and stop it on success. On failure add code in the setEventHandler closure to handle the timeout for example deallocating the server instance.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • 1
    But how would you actually cancel the DispatchWorkItem that is currently running in the background? You cannot directly stop a DispatchWorkItem, and since `server.recv` never returns, you cannot check whether it was canceled either. – Rengers Jan 22 '19 at 14:48
  • @wvteijlingen Then use `Operation` and `OperationQueue` – vadian Jan 22 '19 at 14:55
  • 1
    Still, you cannot stop an Operation from the outside. You can cancel it, but that will only set the `isCancelled` flag to true. It is still the job of the operation itself to check for that flag periodically, and stop executing if it it set. Since `recv` is blocking, there is no way to check the `isCancelled` flag during execution. – Rengers Jan 22 '19 at 15:03
  • @wvteijlingen I'm not sure that I'm understanding you right. If you start the timer before calling `recv` the event handler will call `cancelAllOperations()` on the operation queue if it times out. The global queue is just an example. You can create your own queue for the timer. – vadian Jan 22 '19 at 15:10
  • 1
    You can call `cancelAllOperations()`, but that will not stop any running operation. From the docs: "Canceling the operations does not automatically remove them from the queue or stop those that are currently executing. For operations that are already executing, the operation object itself must check for cancellation and stop what it is doing so that it can move to the finished state." (https://developer.apple.com/documentation/foundation/operationqueue/1417849-cancelalloperations). So if the timer fires when the `recv` method is blocking, there is no way to stop the execution. – Rengers Jan 22 '19 at 15:12
  • @wvteijlingen Re: "You cannot directly stop a DispatchWorkItem", well, maybe not "directly", but somewhat indirectly. [Have you seen this answer](https://stackoverflow.com/a/45403021/499581)? The code does cancel the `DispatchWorkItem` without issue. – l'L'l Jan 26 '19 at 06:46
  • 1
    @l'L'l: Yes, I know that you can call `cancel()` on a `DispatchWorkItem`. However, this just sets the `isCancelled` property to true. It is still the job of the work item itself to stop executing. In that sense you cannot stop or "kill" a work item. You can only ask it to stop by itself. This does not work in OP's case. – Rengers Jan 26 '19 at 08:43
  • @wvteijlingen: Putting a while loop in the work item that checks the bool status is possible from what I observed; if `isCancelled` is true then return and the thread is killed — that’s how I got it to work, unless that isn’t what the actual issue is. – l'L'l Jan 26 '19 at 13:30
  • 1
    @l'L'l: Normally that would work. In this case however, `recv` never returns, making a while loop useless. – Rengers Jan 26 '19 at 17:23
0

Where should not be any case where UDPServer will not return. There should be timeout limit for how long UDPServer should wait. For example consider below case:

 DispatchQueue.global(qos: .background).async {
 let server = UDPServer(address:"0.0.0.0", port:5005)
    switch server.recv(1024) {
        case .success:
            print("Server received message from client.")
        case .failure(let error):
            print("Server failed to received message from client: \(error)")
        }

        server.close()
        DispatchQueue.main.async {
            ...
        }
    }
Rokon
  • 355
  • 3
  • 11