From 5085920d2f811e32f6f72704f30f76743f36f3e6 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 6 Jan 2025 16:33:16 -0500 Subject: [PATCH] Make `Timer(f, ...)` tasks match the stickiness of the parent task. Add `spawn` kwarg. (#56745) (cherry picked from commit 0787a62091f886b39537386c3d1eaba72cb673ed) --- NEWS.md | 2 ++ base/asyncevent.jl | 15 ++++++++++++--- test/channels.jl | 7 +++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7077aa8e3a8dc..51c410a1ea9c8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -71,6 +71,8 @@ New library features * `replace(string, pattern...)` now supports an optional `IO` argument to write the output to a stream rather than returning a string ([#48625]). * `startswith` now supports seekable `IO` streams ([#43055]). +* `Timer(f, ...)` will now match the stickiness of the parent task when creating timer tasks, which can be overridden + by the new `spawn` kwarg. This avoids the issue where sticky tasks i.e. `@async` make their parent sticky ([#56745]) Standard library changes ------------------------ diff --git a/base/asyncevent.jl b/base/asyncevent.jl index 11551df5d0e52..bd2d409f41041 100644 --- a/base/asyncevent.jl +++ b/base/asyncevent.jl @@ -268,7 +268,7 @@ end # timer with repeated callback """ - Timer(callback::Function, delay; interval = 0) + Timer(callback::Function, delay; interval = 0, spawn::Union{Nothing,Bool}=nothing) Create a timer that runs the function `callback` at each timer expiration. @@ -278,6 +278,13 @@ callback is only run once. The function `callback` is called with a single argum itself. Stop a timer by calling `close`. The `callback` may still be run one final time, if the timer has already expired. +If `spawn` is `true`, the created task will be spawned, meaning that it will be allowed +to move thread, which avoids the side-effect of forcing the parent task to get stuck to the thread +it is on. If `spawn` is `nothing` (default), the task will be spawned if the parent task isn't sticky. + +!!! compat "Julia 1.12" + The `spawn` argument was introduced in Julia 1.12. + # Examples Here the first number is printed after a delay of two seconds, then the following numbers are @@ -297,8 +304,9 @@ julia> begin 3 ``` """ -function Timer(cb::Function, timeout::Real; interval::Real=0.0) - timer = Timer(timeout, interval=interval) +function Timer(cb::Function, timeout; spawn::Union{Nothing,Bool}=nothing, kwargs...) + sticky = spawn === nothing ? current_task().sticky : !spawn + timer = Timer(timeout; kwargs...) t = @task begin unpreserve_handle(timer) while _trywait(timer) @@ -312,6 +320,7 @@ function Timer(cb::Function, timeout::Real; interval::Real=0.0) isopen(timer) || return end end + t.sticky = sticky # here we are mimicking parts of _trywait, in coordination with task `t` preserve_handle(timer) @lock timer.cond begin diff --git a/test/channels.jl b/test/channels.jl index cb23b457b8da5..397ff0aaa2a81 100644 --- a/test/channels.jl +++ b/test/channels.jl @@ -555,8 +555,11 @@ end # make sure 1-shot timers work let a = [] Timer(t -> push!(a, 1), 0.01, interval = 0) - sleep(0.2) - @test a == [1] + @test timedwait(() -> a == [1], 10) === :ok +end +let a = [] + Timer(t -> push!(a, 1), 0.01, interval = 0, spawn = true) + @test timedwait(() -> a == [1], 10) === :ok end # make sure that we don't accidentally create a one-shot timer