duemunk / Async

Syntactic sugar in Swift for asynchronous dispatches in Grand Central Dispatch
MIT License
4.59k stars 316 forks source link

Async for non-GCD async operations #31

Closed 0angelic0 closed 5 years ago

0angelic0 commented 9 years ago

It can be very useful if Async can use with async operations.

Right now, I think Async can use only with sync operations.

Async.background {
  // do some "sync" stuff 1
}.background {
  // do some "sync" stuff 2 after sync stuff 1
}

But if I use some async operations such as Alamofire to upload files. Now the chain is just not wait for an upload to be finished.

Async.background {
  // async upload
  Alamofire.upload(.POST, "http://httpbin.org/post", file: fileURL)
    .reponseJSON { (_, _, JSON, _) in
        // upload done!!
    }
}.background {
  // do some stuff S after upload done!! ------>> Sorry S() is doing right away and not wait
  S()
}

An idea for accomplish this is something like passing a done block and waiting for done() to be called to continue to the next Async chain. I saw Quick/Nimble use this for waitUntil operation here.

So, here is a use case proposal.

Async.background { done in
  // async upload
  Alamofire.upload(.POST, "http://httpbin.org/post", file: fileURL)
    .reponseJSON { (_, _, JSON, _) in
        // upload done!!
        done()
    }
}.background {
  // do some "sync" stuff S1 after upload done!!
  S1()
}.background { done in
  // do some "async" stuff A1 after S1
  A1 { done() }
}.main {
  // All uploading, S1 and A1 are done, updates UI here
}

The Async is now very smart and every operations (.background, .main, .utility, etc..) can take a parameterless block for sync operations and a "one parameter block" (that parameter is another block expose as parameter name done) for async operations.

Any thought or possibility?

adad184 commented 9 years ago

i'm wondering this situation too :). In some situations, we do async network request in background.

duemunk commented 9 years ago

Hi guys! I think this could be very nice if it could be incorporated nicely in to the Async framework. I've been doing some work on letting Async block take input parameters + return values. So far I haven't succeeded. Async really is just a DSL for GCD + the chaining syntax.

I suggest you take a look at forbind by my friend @ulrikdamm. It's a future/promises implementation for Swift and looks really "promising" :laughing:

It's no that I don't want to add it to Async, but so far it does quite well doing what is already does + I haven't found a workable solution in Swift yet.

0angelic0 commented 9 years ago

I have some implementation done for the callback feature and can work with Alamofire. But I cannot think how to implement this to Async.

I use classes for self reference (self.next) but Async use structs. Please take a look at DGTAsync.

angelcasado commented 9 years ago

You can do this right now without adding a pull request at least as a temporary solution. So from what it looks like this project was meant to fire off tasks on background queues but won't wait for an async task (like a network request) to finish before firing off the next block in the chain. This is a common problem when using blocks and long running tasks in general.

WARNING: This may be sloppy and I haven't scoped out what the overhead for doing something like this is.

Also normally you could also use a dispatch_group_t but that doesn't work out the way we think it would. I won't go into details here just to keep this brief.

This problem can be solved dispatch_semaphore_t instead.

First without semaphores.

Our network function makes a fake HTTP request and puts it on its own queue and runs asynchronously.

func longLastingAsyncNetworkTask(url: String, _ completion: () -> Void) {

        var queue = dispatch_queue_create("com.networking.queue", DISPATCH_QUEUE_SERIAL)

        dispatch_async(queue, { () -> Void in
            var waitTime: UInt32 = 0

            switch url {
            case "http://apple.com":
                waitTime = 8
            case "http://google.com":
                waitTime = 3
            case "http://microsoft.com":
                waitTime = 5

            default:
                break
            }

            sleep(waitTime)
            println("[Network] \(url) in \(waitTime) sec(s)")
            completion()
        })
    }

Then of course the meat and potatoes:

Async.userInitiated {

            self.longLastingAsyncNetworkTask("http://apple.com") {
                // running 1
            }

            println("Fired Off 1")

            }.background {

                self.longLastingAsyncNetworkTask("http://google.com") {
                    // running 2
                }

                println("Fired Off 2")

            }.background {

                self.longLastingAsyncNetworkTask("http://microsoft.com") {
                    // running 3
                }

                println("Fired Off 3")

            }.main {

                println("All done.")
        }

What you get is:

Fired Off 1
Fired Off 2
Fired Off 3
All done.
[Network] http://google.com in 3 sec(s)
[Network] http://microsoft.com in 5 sec(s)
[Network] http://apple.com in 8 sec(s)

Which is the problem as I understand it. The code is continuing even though the async task inside hasn't finished yet.

With semaphores

Now using the same async method as above with our new meat and potatoes:

var semaphore = dispatch_semaphore_create(0)

        Async.userInitiated {

            self.longLastingAsyncNetworkTask("http://apple.com") {
                dispatch_semaphore_signal(semaphore)
            }

            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
            println("Fired Off 1")

            }.background {

                self.longLastingAsyncNetworkTask("http://google.com") {
                    dispatch_semaphore_signal(semaphore)
                }

                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
                println("Fired Off 2")

            }.background {

                self.longLastingAsyncNetworkTask("http://microsoft.com") {
                    dispatch_semaphore_signal(semaphore)
                }

                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
                println("Fired Off 3")

            }.main {

                println("All done.")
        }

We get:

[Network] http://apple.com in 8 sec(s)
Fired Off 1
[Network] http://google.com in 3 sec(s)
Fired Off 2
[Network] http://microsoft.com in 5 sec(s)
Fired Off 3
All done.

So what happened? I'll let the Apple Docs explain:

When you create the semaphore, you specify the number of available resources. This value becomes the initial count variable for the semaphore. Each time you wait on the semaphore, the dispatch_semaphore_wait function decrements that count variable by 1. If the resulting value is negative, the function tells the kernel to block your thread. On the other end, the dispatch_semaphore_signal function increments the count variable by 1 to indicate that a resource has been freed up. If there are tasks blocked and waiting for a resource, one of them is subsequently unblocked and allowed to do its work.

After we fired off longLastingAsyncNetworkTask(completion: ()->Void) we called dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) to decrement our var semaphore which already had 0 resources to -1.

This stops code execution right before our println("Fired Off X") and waits for the completion for longLastingAsyncNetworkTask(completion: ()->Void) to execute dispatch_semaphore_signal(semaphore) which increments and frees up our semaphore back to 0.

Then our code continues on it's way in order. Also since this is all on a background thread the main thread isn't being blocked until of course you run .main.

Hope that makes sense.

eneko commented 8 years ago

I have submitted a pull request for adding AsyncGroup to help manage multiple asynchronous operations, including custom ones based or not on GCD.

The PR was not intended to solve the issue on this thread, but this code should work by using a GCD group, even if it only has one operation that we want to wait on:

let group = AsyncGroup()
group.enter()
// async upload
Alamofire.upload(.POST, "http://httpbin.org/post", file: fileURL)
    .reponseJSON { (_, _, JSON, _) in
        // upload done!!
        group.leave()
    }
}
group.wait()
Async.background {
  // do some stuff S in **background** after upload done :)
  S()
}

Hope that helps!

JoshHrach commented 8 years ago

I love the idea of an AsyncGroup. Unfortunately, I'm not able to get it working for my needs. I suspect is has to do with the way I'm calling it.

let group = AsyncGroup()
group.enter()
self.externalInterface.getDataFromNetwork(withCode: code) {
    data in
    self.retreivedData = data
    group.leave()
}
group.wait()

It simply hangs at this point. I suspect it is because my network call is happening in my other class and not in the ExternalInterface class that I'm directly referencing here. By the time the data gets to the code above, it has been returned through 2 different handlers.

duemunk commented 8 years ago

@JoshHrach I haven't actually used AsyncGroup myself :)

Please open a separate issues with as much code and explanation as possible. Hopefully someone will be able to help you out!

JoshHrach commented 8 years ago

@duemunk I'll do that tomorrow. Thanks.

JoshHrach commented 8 years ago

@duemunk I never did open another issue. But I did get it resolved. I ended up adding support for dispatch_group_notify() and made a pull request with it. It was the only thing missing from AsyncGroup (mentioned earlier in this issue) that I needed for our project.

duemunk commented 5 years ago

Project is in maintenance mode, so I'm closing this issue. Thanks for everyone's work!