Can I rely on sleep waking up from a call to yield(task)

The documentation for sleep() doesn’t mention that yield wakes it up, which I guess makes it an undocumented feature…But I’d really like to utilize that if possible…

Example:

using Dates

task = schedule(Task() do
    sleepAt = now()
    sleep(10)
    println("Slept for $(now() - sleepAt)")
end)

sleep(2)
yield(task)

If not I can use the wait() call on a condition that’s never triggered. (Triggering it on shutdown is possible, but would require that these tasks be treated special from the other tasks during shutdown…and I’d prefer to avoid that.)

It’s not necessarily that yield() wakes up sleep() calls specifically, but instead that once the timer backing a sleep() call has elapsed, the task that contains that sleep() call is now eligible to be woken up by the scheduler (which a yield() call may cause to happen). This is also true for tasks waiting on I/O or other syscalls, with the obvious difference being that their condition for waking their waiting task is different than for sleep().

If your goal is to wait on a task to wake up, then you’ll need to place a Condition() within the task and notify() it once your task has woken up (which a wait() call can then wait on). If you’re just waiting for the sleeping task to complete, then just wait() on it.

EDIT: To answer the question in your title, I would say that no, you cannot assume that a yield() call will wake up your task, because you can arbitrarily many tasks waiting in the scheduler queue ahead of your now-woken task that may be scheduled first. However, if we assume that the scheduler runs over the queue round-robin style, then yes you can assume that your sleeping task has woken once the yield() call returns, because the yield()-ing task will end up executing chronologically after the previously sleeping task executes.

Okay so taking your answer it’s an undocumented reaction, because with the above code, the task “wakes up” after 2 seconds not 10.

Reading the wait documentation closer it only mentions the “no argument” version waiting for an undefined period and only being restarted in response to a schedule or yieldto call. The documentation for yieldto specifically mentions Its use is discouraged. But the documentation for yield says it calls schedule for the task, so I guess that works.

If I don’t have a documented way to break a sleep then I guess I’ll have to use 2 task, one that does the actual work, that I can ensure is stopped at shutdown (by setting a shutdown flag and notifying it’s condition) and a second that periodically wakes up and trigger’s the first’s condition.

Granted the documentation for wait say it’s often in a while loop to ensure a waited-for condition is met before proceeding…and the condition object is edge triggered so I can’t check that…ugh it’s either boolean channels or more flags.

Sorry, I didn’t realize that this was unexpected; yield() will immediately return if there are no other tasks to be scheduled, effectively making it a no-op. If that 10 second timer hasn’t elapsed, it is not possible or safe to wake the task.

So this is a new requirement that wasn’t mentioned before (or I didn’t notice it). I think it would help if you could give a more real-world example of what you’re trying to accomplish, otherwise I feel like I won’t be as useful in giving you advice.

To be fair, your example code does produce an error that crashes julia 1.1 a couple seconds later:

julia> using Dates

julia> task = schedule(Task() do
           sleepAt = now()
           sleep(10)
           println("Slept for $(now() - sleepAt)")
       end)
Task (runnable) @0x00007fcbc60fc760

julia> sleep(2); yield(task)
Slept for 3265 milliseconds

julia> ERROR: schedule: Task not runnable
Stacktrace:
 [1] enq_work at ./event.jl:88 [inlined]
 [2] #schedule#443(::Bool, ::Function, ::Task, ::EOFError) at ./event.jl:134

Ahhh, that’s interesting I had not seen that crash on version 1.1.1. However if I add a sleep() after the yield(task) so now I see the crash…Well that options is out.

Real world examples get long, but here you go, given everything I’ve read/seen it sounds like I’m going to have to resort to this basic flow:

using Dates
using Base.Threads

shutdown = Atomic{Bool}(false)
wakeup   = Channel{Bool}(3)

wakeTask = schedule(Task() do
    local wakeAt = now() + Second(10)

    while shutdown[] == false
        if now() >= wakeAt
            put!(wakeup, true)
            wakeAt = now() + Second(10)
        else
            sleep(Dates.value(wakeAt-now()) / 1000)
        end
    end
end)

workTask = schedule(Task() do
    while take!(wakeup) == true
        println("Heart beat")
    end
    println("Cleanup")
end)

function stop()
    atomic_xchg!(shutdown, true)
    put!(wakeup, false)
    wait(workTask)
end

I guess I was just hoping for less code. Which an early way to wake up sleep would give…or a wait with a timeout. But I don’t have either of those, so this is probably what I’ll use.

  • The wakeTask feels gratuitous, but I don’t see a way around it.
  • The channel is dangerous in the aspect that if the workTask stops unexpectedly/crashes then stop() could hang forever. So I’ll want to add protection for that.
  • I can’t really use a Condition because the wait documentation says wait could return before the condition is set and I don’t want my heart beating too fast.
  • I might be able to remove the checks around sleep, it’s documentation doesn’t mention waking up early…

In my real world the delays are not in seconds but hours…so I really would like a way to shutdown workTask early so it can save it’s state.