Skip to content

Commit

Permalink
Throw ScheduleAfterSyncException on racy sync
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf committed Aug 21, 2021
1 parent 362e05c commit 19a946f
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 7 deletions.
43 changes: 43 additions & 0 deletions base/task.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct CompositeException <: Exception
end
length(c::CompositeException) = length(c.exceptions)
push!(c::CompositeException, ex) = push!(c.exceptions, ex)
pushfirst!(c::CompositeException, ex) = pushfirst!(c.exceptions, ex)
isempty(c::CompositeException) = isempty(c.exceptions)
iterate(c::CompositeException, state...) = iterate(c.exceptions, state...)
eltype(::Type{CompositeException}) = Any
Expand Down Expand Up @@ -341,6 +342,29 @@ end

## lexically-scoped waiting for multiple items

struct ScheduledAfterSyncException <: Exception
values::Vector{Any}
end

function showerror(io::IO, ex::ScheduledAfterSyncException)
print(io, "ScheduledAfterSyncException: ")
if isempty(ex.values)
print(io, "(no values)")
return
end
show(io, ex.values[1])
if length(ex.values) == 1
print(io, " is")
elseif length(ex.values) == 2
print(io, " and one more ")
print(io, nameof(typeof(ex.values[2])))
print(io, " are")
else
print(io, " and ", length(ex.values) - 1, " more objects are")
end
print(io, " registered after the end of a `@sync` block")
end

function sync_end(c::Channel{Any})
local c_ex
while isready(c)
Expand All @@ -365,6 +389,25 @@ function sync_end(c::Channel{Any})
end
end
close(c)

# Capture all waitable objects scheduled after the end of `@sync` and
# include them in the exception. This way, the user can check what was
# scheduled by examining at the exception object.
local racy
for r in c
if !@isdefined(racy)
racy = []
end
push!(racy, r)
end
if @isdefined(racy)
if !@isdefined(c_ex)
c_ex = CompositeException()
end
# Since this is a clear programming error, show this exception first:
pushfirst!(c_ex, ScheduledAfterSyncException(racy))
end

if @isdefined(c_ex)
throw(c_ex)
end
Expand Down
16 changes: 16 additions & 0 deletions test/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -789,3 +789,19 @@ if Sys.isapple() || (Sys.islinux() && Sys.ARCH === :x86_64)
end
end
end # Sys.isapple()

@testset "ScheduledAfterSyncException" begin
t = :DummyTask
msg = sprint(showerror, Base.ScheduledAfterSyncException(Any[t]))
@test occursin(":DummyTask is registered after the end of a `@sync` block", msg)
msg = sprint(showerror, Base.ScheduledAfterSyncException(Any[t, t]))
@test occursin(
":DummyTask and one more Symbol are registered after the end of a `@sync` block",
msg,
)
msg = sprint(showerror, Base.ScheduledAfterSyncException(Any[t, t, t]))
@test occursin(
":DummyTask and 2 more objects are registered after the end of a `@sync` block",
msg,
)
end
32 changes: 25 additions & 7 deletions test/threads_exec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -913,12 +913,17 @@ end
end
end

# @spawn racying with sync_end

hidden_spawn(f) = Threads.@spawn f()

function sync_end_race()
y = Ref(:notset)
local t
@sync begin
for _ in 1:6 # tweaked to maximize `nerror` below
Threads.@spawn nothing
end
t = hidden_spawn() do
Threads.@spawn y[] = :completed
end
Expand All @@ -935,16 +940,29 @@ function check_sync_end_race()
@sync begin
done = Threads.Atomic{Bool}(false)
try
# Additional task for randomizing the scheduling:
Threads.@spawn begin
while !done[]
yield()
end
end
# `Threads.@spawn` must fail to be scheduled or complete its execution:
ncompleted = 0
nnotscheduled = 0
nerror = 0
for i in 1:1000
sync_end_race() in (:completed, :notscheduled) || return i
y = try
yield()
sync_end_race()
catch err
if err isa CompositeException
if err.exceptions[1] isa Base.ScheduledAfterSyncException
nerror += 1
continue
end
end
rethrow()
end
y in (:completed, :notscheduled) || return (; i, y)
ncompleted += y === :completed
nnotscheduled += y === :notscheduled
end
# Useful for tuning the test:
@debug "`check_sync_end_race` done" nthreads() ncompleted nnotscheduled nerror
finally
done[] = true
end
Expand Down

0 comments on commit 19a946f

Please sign in to comment.