# This file is a part of Julia. License is MIT: https://julialang.org/license ## basic task functions and TLS # Container for a captured exception and its backtrace. Can be serialized. mutable struct CapturedException <: Exception ex::Any processed_bt::Vector{Any} function CapturedException(ex, bt_raw::Vector{Ptr{Void}}) # bt_raw MUST be an Array of code pointers than can be processed by jl_lookup_code_address # Typically the result of a catch_backtrace() # Process bt_raw so that it can be safely serialized bt_lines = Any[] process_func(args...) = push!(bt_lines, args) process_backtrace(process_func, bt_raw, 100) # Limiting this to 100 lines. CapturedException(ex, bt_lines) end CapturedException(ex, processed_bt::Vector{Any}) = new(ex, processed_bt) end function showerror(io::IO, ce::CapturedException) showerror(io, ce.ex, ce.processed_bt, backtrace=true) end mutable struct CompositeException <: Exception exceptions::Vector{Any} CompositeException() = new(Any[]) CompositeException(exceptions) = new(exceptions) end length(c::CompositeException) = length(c.exceptions) push!(c::CompositeException, ex) = push!(c.exceptions, ex) isempty(c::CompositeException) = isempty(c.exceptions) start(c::CompositeException) = start(c.exceptions) next(c::CompositeException, state) = next(c.exceptions, state) done(c::CompositeException, state) = done(c.exceptions, state) function showerror(io::IO, ex::CompositeException) if !isempty(ex) showerror(io, ex.exceptions[1]) remaining = length(ex) - 1 if remaining > 0 print(io, string("\n\n...and ", remaining, " more exception(s).\n")) end else print(io, "CompositeException()\n") end end function show(io::IO, t::Task) print(io, "Task ($(t.state)) @0x$(hex(convert(UInt, pointer_from_objref(t)), Sys.WORD_SIZE>>2))") end """ @task Wrap an expression in a [`Task`](@ref) without executing it, and return the [`Task`](@ref). This only creates a task, and does not run it. ```jldoctest julia> a1() = det(rand(1000, 1000)); julia> b = @task a1(); julia> istaskstarted(b) false julia> schedule(b); julia> yield(); julia> istaskdone(b) true ``` """ macro task(ex) :(Task(()->$(esc(ex)))) end """ current_task() Get the currently running [`Task`](@ref). """ current_task() = ccall(:jl_get_current_task, Ref{Task}, ()) """ istaskdone(t::Task) -> Bool Determine whether a task has exited. ```jldoctest julia> a2() = det(rand(1000, 1000)); julia> b = Task(a2); julia> istaskdone(b) false julia> schedule(b); julia> yield(); julia> istaskdone(b) true ``` """ istaskdone(t::Task) = ((t.state == :done) | istaskfailed(t)) """ istaskstarted(t::Task) -> Bool Determine whether a task has started executing. ```jldoctest julia> a3() = det(rand(1000, 1000)); julia> b = Task(a3); julia> istaskstarted(b) false ``` """ istaskstarted(t::Task) = ccall(:jl_is_task_started, Cint, (Any,), t) != 0 istaskfailed(t::Task) = (t.state == :failed) task_result(t::Task) = t.result task_local_storage() = get_task_tls(current_task()) function get_task_tls(t::Task) if t.storage === nothing t.storage = ObjectIdDict() end (t.storage)::ObjectIdDict end """ task_local_storage(key) Look up the value of a key in the current task's task-local storage. """ task_local_storage(key) = task_local_storage()[key] """ task_local_storage(key, value) Assign a value to a key in the current task's task-local storage. """ task_local_storage(key, val) = (task_local_storage()[key] = val) """ task_local_storage(body, key, value) Call the function `body` with a modified task-local storage, in which `value` is assigned to `key`; the previous value of `key`, or lack thereof, is restored afterwards. Useful for emulating dynamic scoping. """ function task_local_storage(body::Function, key, val) tls = task_local_storage() hadkey = haskey(tls,key) old = get(tls,key,nothing) tls[key] = val try body() finally hadkey ? (tls[key] = old) : delete!(tls,key) end end # NOTE: you can only wait for scheduled tasks function wait(t::Task) if !istaskdone(t) if t.donenotify === nothing t.donenotify = Condition() end end while !istaskdone(t) wait(t.donenotify) end if istaskfailed(t) throw(t.exception) end return task_result(t) end suppress_excp_printing(t::Task) = isa(t.storage, ObjectIdDict) ? get(get_task_tls(t), :SUPPRESS_EXCEPTION_PRINTING, false) : false function register_taskdone_hook(t::Task, hook) tls = get_task_tls(t) push!(get!(tls, :TASKDONE_HOOKS, []), hook) t end # runtime system hook called when a task finishes function task_done_hook(t::Task) # `finish_task` sets `sigatomic` before entering this function err = istaskfailed(t) result = task_result(t) handled = false if err t.backtrace = catch_backtrace() end q = t.consumers t.consumers = nothing if isa(t.donenotify, Condition) && !isempty(t.donenotify.waitq) handled = true notify(t.donenotify, result, true, err) end # Execute any other hooks registered in the TLS if isa(t.storage, ObjectIdDict) && haskey(t.storage, :TASKDONE_HOOKS) foreach(hook -> hook(t), t.storage[:TASKDONE_HOOKS]) delete!(t.storage, :TASKDONE_HOOKS) handled = true end #### un-optimized version #isa(q,Condition) && notify(q, result, error=err) if isa(q,Task) handled = true nexttask = q nexttask.state = :runnable if err nexttask.exception = result end yieldto(nexttask, result) # this terminates the task elseif isa(q,Condition) && !isempty(q.waitq) handled = true notify(q, result, error=err) end if err && !handled if isa(result,InterruptException) && isdefined(Base,:active_repl_backend) && active_repl_backend.backend_task.state == :runnable && isempty(Workqueue) && active_repl_backend.in_eval throwto(active_repl_backend.backend_task, result) # this terminates the task end if !suppress_excp_printing(t) let bt = t.backtrace # run a new task to print the error for us @schedule with_output_color(Base.error_color(), STDERR) do io print(io, "ERROR (unhandled task failure): ") showerror(io, result, bt) println(io) end end end end # Clear sigatomic before waiting sigatomic_end() wait() # this will not return end ## dynamically-scoped waiting for multiple items sync_begin() = task_local_storage(:SPAWNS, ([], get(task_local_storage(), :SPAWNS, ()))) function sync_end() spawns = get(task_local_storage(), :SPAWNS, ()) if spawns === () error("sync_end() without sync_begin()") end refs = spawns[1] task_local_storage(:SPAWNS, spawns[2]) c_ex = CompositeException() for r in refs try wait(r) catch ex if !isa(r, Task) || (isa(r, Task) && !istaskfailed(r)) rethrow(ex) end finally if isa(r, Task) && istaskfailed(r) push!(c_ex, CapturedException(task_result(r), r.backtrace)) end end end if !isempty(c_ex) throw(c_ex) end nothing end """ @sync Wait until all dynamically-enclosed uses of `@async`, `@spawn`, `@spawnat` and `@parallel` are complete. All exceptions thrown by enclosed async operations are collected and thrown as a `CompositeException`. """ macro sync(block) quote sync_begin() v = $(esc(block)) sync_end() v end end function sync_add(r) spawns = get(task_local_storage(), :SPAWNS, ()) if spawns !== () push!(spawns[1], r) if isa(r, Task) tls_r = get_task_tls(r) tls_r[:SUPPRESS_EXCEPTION_PRINTING] = true end end r end function async_run_thunk(thunk) t = Task(thunk) sync_add(t) enq_work(t) t end """ @async Like `@schedule`, `@async` wraps an expression in a `Task` and adds it to the local machine's scheduler queue. Additionally it adds the task to the set of items that the nearest enclosing `@sync` waits for. """ macro async(expr) thunk = esc(:(()->($expr))) :(async_run_thunk($thunk)) end """ timedwait(testcb::Function, secs::Float64; pollint::Float64=0.1) Waits until `testcb` returns `true` or for `secs` seconds, whichever is earlier. `testcb` is polled every `pollint` seconds. """ function timedwait(testcb::Function, secs::Float64; pollint::Float64=0.1) pollint > 0 || throw(ArgumentError("cannot set pollint to $pollint seconds")) start = time() done = Channel(1) timercb(aw) = begin try if testcb() put!(done, :ok) elseif (time() - start) > secs put!(done, :timed_out) end catch e put!(done, :error) finally isready(done) && close(aw) end end if !testcb() t = Timer(timercb, pollint, pollint) ret = fetch(done) close(t) else ret = :ok end ret end