Skip to content

Pause a job #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
fikr4n opened this issue Aug 20, 2017 · 15 comments
Closed

Pause a job #104

fikr4n opened this issue Aug 20, 2017 · 15 comments

Comments

@fikr4n
Copy link

fikr4n commented Aug 20, 2017

In case of Android, we can cancel a job and its children in onDestroy. Is there a way to pause it (e.g. in onStop) and resume it (e.g. in onStart)? Or any plan to implement it?

@fikr4n
Copy link
Author

fikr4n commented Aug 21, 2017

Not sure, but I have idea like this:

class MyFragment : Fragment() {
    ...
    private val parentJob = Job() // cancel on destroy
    private var stoppedJob: Job? = null // active on stop

    ...

    fun onStart() {
        super.onStart()
        stoppedJob?.cancel()
    }

    fun onStop() {
        super.onStop()
        stoppedJob = Job()
    }

    fun onDestroy() {
        super.onDestroy()
        parentJob.cancel()
    }

    private fun showSomething(id: Long) = launch(UI + parentJob) {
        val result = asyncSomething(id).await()
        stoppedJob?.join()
        updateUiBasedOnSomething(result)
    }

    ...
}

@elizarov
Copy link
Contributor

What you exactly want to happen while it is paused? What kind of asynchronous activities you are concerned about?

If you've sent some network network request to a server, then it does not make much sense "to pause" it, because there is nothing you are can really do about it -- it is already is flight and is being processed by the server.

However, if you doing some UI animations with coroutines or something like that, then pausing them indeed makes sense. In the latter case, I'd suggest to implement a dedicated CoroutineDispatcher that is tied to your activity life cycle. It can queue all the work while activity is paused to resume it when it is restarted.

@fikr4n
Copy link
Author

fikr4n commented Aug 21, 2017

However, if you doing some UI animations with coroutines or something like that, then pausing them indeed makes sense.

Yes, it's what I meant, there's no problem with the HTTP response, but I want to delay displaying/toasting/animating something based on that response until the fragment/activity started (in case it is stopped).

@Dem0n13
Copy link

Dem0n13 commented Nov 15, 2017

Thank you for this thread, I am looking for something like that.
In java I use my implementation of Promise/Deferred, so in Activity.onStop I create Deferred<Void> whenActivityRunning and onStart I resolve this deferred. Usage:

asyncOperation.run(args)
    .thenPromise(result -> whenActivityRunning.then(() -> result))
    .thenVoid(result -> updateUiOrSmthElse(result));

As I undestand, current suggestion is implement a dedicated CoroutineDispatcher , does it?

@Dem0n13
Copy link

Dem0n13 commented Nov 16, 2017

I tried to implement simple pausable dispatcher:

class PausableDispatcher(private val handler: Handler): CoroutineDispatcher() {
    private val queue: Queue<Runnable> = LinkedList()
    private var isPaused: Boolean = false

    @Synchronized override fun dispatch(context: CoroutineContext, block: Runnable) {
        println("dispatch")
        if (isPaused) {
            queue.add(block)
        } else {
            handler.post(block)
        }
    }

    @Synchronized fun pause() {
        println("pause")
        isPaused = true
    }

    @Synchronized fun resume() {
        println("resume")
        isPaused = false
        runQueue()
    }

    private fun runQueue() {
        queue.iterator().let {
            while (it.hasNext()) {
                val block = it.next()
                it.remove()
                handler.post(block)
            }
        }
    }
}

usage:

class MainActivity : Activity() {
    private val dispatcher = PausableDispatcher(Handler(Looper.getMainLooper()))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val button = findViewById<Button>(R.id.button1)
        button.setOnClickListener {
            launch(dispatcher) {
               suspendFunc()
               delay(1000)
               suspendFunc()
               delay(1000)
               suspendFunc()
            }
        }
    }

    override fun onPause() {
        super.onPause()
        dispatcher.pause()
    }

    override fun onResume() {
        super.onResume()
        dispatcher.resume()
    }

    override fun onDestroy() {
        super.onDestroy()
        dispatcher.cancel()
    }
}

If I "minimize" my application after first suspendFunc call, next call pushes to queue.
When I "restore" my application, it continues execution.

I am not sure about "dispatcher.cancel", and about implementation details, but it seems to work. Any suggestions?

@elizarov
Copy link
Contributor

I'm closing this issue in favor of #258. Having thought about the issue I don't think that solving "pausing" problem on a dispatcher level is a right way to do it. It is better to explicitly write code that suspends coroutine until the desired state is reached.

@neworld
Copy link

neworld commented Mar 29, 2019

I found one use case where is pausable dispatcher very handy. I will try to explain as a simplified model. Let's say we have some global notification display. We have to display only one at the time. There could be many requests at the time. Also, the user is able to pause notifications at any time. The code could be as simple as that:

private val notifications = Channel<Notification>(UNLIMITED)

suspend fun enqueueNotification(notification: Notification) {
  notifications.send(notification)
}

suspend fun pause() { pauseable.pause() }
suspend fun resume() { pauseable.resume()  }

private fun launchDispacher() {
  withContext(pauseable) {
    for (notification in notifications) {
      val view = prepareView(notification)
      showInAnimation(view) //suspension point. 
    
      delay(5000) //suspension point

      showOutAnimation(view) //suspension point. 
    }
  }
}

A user has to be able to pause here. Dont ask why :) I am ok to pause dispacher at any suspension point.

@fvasco
Copy link
Contributor

fvasco commented Mar 29, 2019

@neworld
Why not cancel Job on pause and start a new Job on resume?

@neworld
Copy link

neworld commented Mar 29, 2019

Because of this:

      showInAnimation(view) //suspension point. 
      delay(5000) //suspension point
      showOutAnimation(view) //suspension point. 

Sometimes new job should start from begining, sometimes new job need to close old notification first. And there is very simplified version. Real one has more suspension points.

@fvasco
Copy link
Contributor

fvasco commented Mar 29, 2019

I'm trying to understand your real use case, your last code can be rewritten:

try {
    showInAnimation(view) //suspension point. 
    delay(5000) //suspension point
} finally {
    withContext(NonCancellable) {
      showOutAnimation(view) //suspension point.
    }
}

alternatively you can use a Mutex to pause/resume the job.

@neworld
Copy link

neworld commented Mar 29, 2019

Actually, it can't be rewrite in this way, because if the user pauses notification while one is shown, it should be visible as long as the user doesn't resume. Mutex could do the job as well:

    for (notification in notifications) {
      val view = prepareView(notification)
      pauseable.withLock { showInAnimation(view) }
    
      delay(5000) //suspension point

      pauseable.withLock { showOutAnimation(view) }
    }

@morder
Copy link

morder commented Apr 9, 2020

now we can use
viewLifecycleOwner.lifecycleScope.launchWhenResumed { ... }
it's working like pausing

@fikr4n
Copy link
Author

fikr4n commented Apr 13, 2020

Launches and runs the given block when the Lifecycle controlling this LifecycleCoroutineScope is at least in Lifecycle.State.RESUMED state.

The returned Job will be cancelled when the Lifecycle is destroyed.
LifecycleCoroutineScope.launchwhenresumed

Really? Is it paused everytime the lifecycle pauses, continued when the lifecycle resumes, paused again when the lifecycle pauses again?

@morder
Copy link

morder commented Apr 13, 2020

I have checked and it's working as I expected. You can check it by yourself.

@muhammad-farhan-bakht
Copy link

lifecycleScope.launchWhenResumed

A little bit late :) I have tested and used it., it is working for my case. Thanks a lot @morder this what I am searching for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants