Skip to content

Commit 58bafe4

Browse files
vtjnashJeffBezanson
authored andcommitted
threads: expand thread-safe region for I/O (#32309)
This should hopefully cover most I/O operations which go through libuv to make them thread-safe. There is no scaling here, just one big global lock, so expect worse-than-single-threaded performance if doing I/O on multiple threads (as compared to doing the same work on one thread). The intention is to handle performance improvement incrementally later. It also necessarily redesigns parts of the UDPSocket implementation to properly handle concurrent (single-threaded) usage, as a necessary part of making it handle parallel (thread-safe) usage.
1 parent dd56dbf commit 58bafe4

25 files changed

+1017
-863
lines changed

base/asyncevent.jl

+93-65
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ mutable struct AsyncCondition
1515
handle::Ptr{Cvoid}
1616
cond::ThreadSynchronizer
1717
isopen::Bool
18+
set::Bool
1819

1920
function AsyncCondition()
20-
this = new(Libc.malloc(_sizeof_uv_async), ThreadSynchronizer(), true)
21+
this = new(Libc.malloc(_sizeof_uv_async), ThreadSynchronizer(), true, false)
22+
iolock_begin()
2123
associate_julia_struct(this.handle, this)
22-
finalizer(uvfinalize, this)
2324
err = ccall(:uv_async_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
2425
eventloop(), this, uv_jl_asynccb::Ptr{Cvoid})
2526
if err != 0
@@ -28,6 +29,8 @@ mutable struct AsyncCondition
2829
this.handle = C_NULL
2930
throw(_UVError("uv_async_init", err))
3031
end
32+
finalizer(uvfinalize, this)
33+
iolock_end()
3134
return this
3235
end
3336
end
@@ -40,28 +43,10 @@ the async condition object itself.
4043
"""
4144
function AsyncCondition(cb::Function)
4245
async = AsyncCondition()
43-
waiter = Task(function()
44-
lock(async.cond)
45-
try
46-
while isopen(async)
47-
success = try
48-
stream_wait(async, async.cond)
49-
true
50-
catch exc # ignore possible exception on close()
51-
isa(exc, EOFError) || rethrow()
52-
finally
53-
unlock(async.cond)
54-
end
55-
success && cb(async)
56-
lock(async.cond)
57-
end
58-
finally
59-
unlock(async.cond)
46+
@async while _trywait(async)
47+
cb(async)
48+
isopen(async) || return
6049
end
61-
end)
62-
# must start the task right away so that it can wait for the AsyncCondition before
63-
# we re-enter the event loop. this avoids a race condition. see issue #12719
64-
yield(waiter)
6550
return async
6651
end
6752

@@ -81,59 +66,108 @@ mutable struct Timer
8166
handle::Ptr{Cvoid}
8267
cond::ThreadSynchronizer
8368
isopen::Bool
69+
set::Bool
8470

8571
function Timer(timeout::Real; interval::Real = 0.0)
8672
timeout 0 || throw(ArgumentError("timer cannot have negative timeout of $timeout seconds"))
8773
interval 0 || throw(ArgumentError("timer cannot have negative repeat interval of $interval seconds"))
74+
timeout = UInt64(round(timeout * 1000)) + 1
75+
interval = UInt64(round(interval * 1000))
76+
loop = eventloop()
8877

89-
this = new(Libc.malloc(_sizeof_uv_timer), ThreadSynchronizer(), true)
90-
ccall(:jl_uv_update_timer_start, Cvoid,
91-
(Ptr{Cvoid}, Any, Ptr{Cvoid}, Ptr{Cvoid}, UInt64, UInt64),
92-
eventloop(), this, this.handle, uv_jl_timercb::Ptr{Cvoid},
93-
UInt64(round(timeout * 1000)) + 1, UInt64(round(interval * 1000)))
78+
this = new(Libc.malloc(_sizeof_uv_timer), ThreadSynchronizer(), true, false)
79+
associate_julia_struct(this.handle, this)
80+
iolock_begin()
81+
err = ccall(:uv_timer_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), loop, this)
82+
@assert err == 0
9483
finalizer(uvfinalize, this)
84+
ccall(:uv_update_time, Cvoid, (Ptr{Cvoid},), loop)
85+
err = ccall(:uv_timer_start, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, UInt64, UInt64),
86+
this, uv_jl_timercb::Ptr{Cvoid}, timeout, interval)
87+
@assert err == 0
88+
iolock_end()
9589
return this
9690
end
9791
end
9892

9993
unsafe_convert(::Type{Ptr{Cvoid}}, t::Timer) = t.handle
10094
unsafe_convert(::Type{Ptr{Cvoid}}, async::AsyncCondition) = async.handle
10195

102-
function wait(t::Union{Timer, AsyncCondition})
103-
lock(t.cond)
104-
try
105-
isopen(t) || throw(EOFError())
106-
stream_wait(t, t.cond)
107-
finally
108-
unlock(t.cond)
96+
function _trywait(t::Union{Timer, AsyncCondition})
97+
set = t.set
98+
if !set
99+
t.handle == C_NULL && return false
100+
iolock_begin()
101+
set = t.set
102+
if !set
103+
preserve_handle(t)
104+
lock(t.cond)
105+
try
106+
set = t.set
107+
if !set
108+
if t.handle != C_NULL
109+
iolock_end()
110+
set = wait(t.cond)
111+
unlock(t.cond)
112+
iolock_begin()
113+
lock(t.cond)
114+
end
115+
end
116+
finally
117+
unlock(t.cond)
118+
unpreserve_handle(t)
119+
end
120+
end
121+
iolock_end()
109122
end
123+
t.set = false
124+
return set
125+
end
126+
127+
function wait(t::Union{Timer, AsyncCondition})
128+
_trywait(t) || throw(EOFError())
129+
nothing
110130
end
111131

132+
112133
isopen(t::Union{Timer, AsyncCondition}) = t.isopen
113134

114135
function close(t::Union{Timer, AsyncCondition})
136+
iolock_begin()
115137
if t.handle != C_NULL && isopen(t)
116138
t.isopen = false
117139
ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t)
118140
end
141+
iolock_end()
119142
nothing
120143
end
121144

122145
function uvfinalize(t::Union{Timer, AsyncCondition})
123-
if t.handle != C_NULL
124-
disassociate_julia_struct(t.handle) # not going to call the usual close hooks
125-
close(t)
126-
t.handle = C_NULL
146+
iolock_begin()
147+
lock(t.cond)
148+
try
149+
if t.handle != C_NULL
150+
disassociate_julia_struct(t.handle) # not going to call the usual close hooks
151+
if t.isopen
152+
t.isopen = false
153+
ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t)
154+
end
155+
t.handle = C_NULL
156+
notify(t.cond, false)
157+
end
158+
finally
159+
unlock(t.cond)
127160
end
128-
t.isopen = false
161+
iolock_end()
129162
nothing
130163
end
131164

132165
function _uv_hook_close(t::Union{Timer, AsyncCondition})
133166
lock(t.cond)
134167
try
135-
uvfinalize(t)
136-
notify_error(t.cond, EOFError())
168+
t.isopen = false
169+
t.handle = C_NULL
170+
notify(t.cond, t.set)
137171
finally
138172
unlock(t.cond)
139173
end
@@ -144,7 +178,8 @@ function uv_asynccb(handle::Ptr{Cvoid})
144178
async = @handle_as handle AsyncCondition
145179
lock(async.cond)
146180
try
147-
notify(async.cond)
181+
async.set = true
182+
notify(async.cond, true)
148183
finally
149184
unlock(async.cond)
150185
end
@@ -155,11 +190,12 @@ function uv_timercb(handle::Ptr{Cvoid})
155190
t = @handle_as handle Timer
156191
lock(t.cond)
157192
try
193+
t.set = true
158194
if ccall(:uv_timer_get_repeat, UInt64, (Ptr{Cvoid},), t) == 0
159195
# timer is stopped now
160196
close(t)
161197
end
162-
notify(t.cond)
198+
notify(t.cond, true)
163199
finally
164200
unlock(t.cond)
165201
end
@@ -199,7 +235,7 @@ Here the first number is printed after a delay of two seconds, then the followin
199235
julia> begin
200236
i = 0
201237
cb(timer) = (global i += 1; println(i))
202-
t = Timer(cb, 2, interval = 0.2)
238+
t = Timer(cb, 2, interval=0.2)
203239
wait(t)
204240
sleep(0.5)
205241
close(t)
@@ -209,37 +245,28 @@ julia> begin
209245
3
210246
```
211247
"""
212-
function Timer(cb::Function, timeout::Real; interval::Real = 0.0)
213-
t = Timer(timeout, interval = interval)
214-
waiter = Task(function()
215-
while isopen(t)
216-
success = try
217-
wait(t)
218-
true
219-
catch exc # ignore possible exception on close()
220-
isa(exc, EOFError) || rethrow()
221-
false
222-
end
223-
success && cb(t)
248+
function Timer(cb::Function, timeout::Real; interval::Real=0.0)
249+
timer = Timer(timeout, interval=interval)
250+
@async while _trywait(timer)
251+
cb(timer)
252+
isopen(timer) || return
224253
end
225-
end)
226-
# must start the task right away so that it can wait for the Timer before
227-
# we re-enter the event loop. this avoids a race condition. see issue #12719
228-
yield(waiter)
229-
return t
254+
return timer
230255
end
231256

232257
"""
233258
timedwait(testcb::Function, secs::Float64; pollint::Float64=0.1)
234259
235260
Waits until `testcb` returns `true` or for `secs` seconds, whichever is earlier.
236261
`testcb` is polled every `pollint` seconds.
262+
263+
Returns :ok, :timed_out, or :error
237264
"""
238265
function timedwait(testcb::Function, secs::Float64; pollint::Float64=0.1)
239266
pollint > 0 || throw(ArgumentError("cannot set pollint to $pollint seconds"))
240267
start = time()
241268
done = Channel(1)
242-
timercb(aw) = begin
269+
function timercb(aw)
243270
try
244271
if testcb()
245272
put!(done, :ok)
@@ -251,14 +278,15 @@ function timedwait(testcb::Function, secs::Float64; pollint::Float64=0.1)
251278
finally
252279
isready(done) && close(aw)
253280
end
281+
nothing
254282
end
255283

256284
if !testcb()
257285
t = Timer(timercb, pollint, interval = pollint)
258-
ret = fetch(done)
286+
ret = fetch(done)::Symbol
259287
close(t)
260288
else
261289
ret = :ok
262290
end
263-
ret
291+
return ret
264292
end

base/condition.jl

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ unlock(c::GenericCondition) = unlock(c.lock)
7676
trylock(c::GenericCondition) = trylock(c.lock)
7777
islocked(c::GenericCondition) = islocked(c.lock)
7878

79+
lock(f, c::GenericCondition) = lock(f, c.lock)
80+
unlock(f, c::GenericCondition) = unlock(f, c.lock)
81+
7982
"""
8083
wait([x])
8184

base/coreio.jl

-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ write(::DevNull, ::UInt8) = 1
1717
unsafe_write(::DevNull, ::Ptr{UInt8}, n::UInt)::Int = n
1818
close(::DevNull) = nothing
1919
flush(::DevNull) = nothing
20-
wait_connected(::DevNull) = nothing
2120
wait_readnb(::DevNull) = wait()
22-
wait_readbyte(::DevNull) = wait()
2321
wait_close(::DevNull) = wait()
2422
eof(::DevNull) = true
2523

base/file.jl

+6-6
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ function mktempdir(parent=tempdir(); prefix=temp_prefix)
545545
try
546546
ret = ccall(:uv_fs_mkdtemp, Int32,
547547
(Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}),
548-
eventloop(), req, tpath, C_NULL)
548+
C_NULL, req, tpath, C_NULL)
549549
if ret < 0
550550
ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req)
551551
uv_error("mktempdir", ret)
@@ -631,8 +631,8 @@ function readdir(path::AbstractString)
631631
uv_readdir_req = zeros(UInt8, ccall(:jl_sizeof_uv_fs_t, Int32, ()))
632632

633633
# defined in sys.c, to call uv_fs_readdir, which sets errno on error.
634-
err = ccall(:jl_uv_fs_scandir, Int32, (Ptr{Cvoid}, Ptr{UInt8}, Cstring, Cint, Ptr{Cvoid}),
635-
eventloop(), uv_readdir_req, path, 0, C_NULL)
634+
err = ccall(:uv_fs_scandir, Int32, (Ptr{Cvoid}, Ptr{UInt8}, Cstring, Cint, Ptr{Cvoid}),
635+
C_NULL, uv_readdir_req, path, 0, C_NULL)
636636
err < 0 && throw(SystemError("unable to read directory $path", -err))
637637
#uv_error("unable to read directory $path", err)
638638

@@ -644,7 +644,7 @@ function readdir(path::AbstractString)
644644
end
645645

646646
# Clean up the request string
647-
ccall(:jl_uv_fs_req_cleanup, Cvoid, (Ptr{UInt8},), uv_readdir_req)
647+
ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{UInt8},), uv_readdir_req)
648648

649649
return entries
650650
end
@@ -816,9 +816,9 @@ Return the target location a symbolic link `path` points to.
816816
function readlink(path::AbstractString)
817817
req = Libc.malloc(_sizeof_uv_fs)
818818
try
819-
ret = ccall(:jl_uv_fs_readlink, Int32,
819+
ret = ccall(:uv_fs_readlink, Int32,
820820
(Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}),
821-
eventloop(), req, path, C_NULL)
821+
C_NULL, req, path, C_NULL)
822822
if ret < 0
823823
ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req)
824824
uv_error("readlink", ret)

base/filesystem.jl

+7-7
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ function open(path::AbstractString, flags::Integer, mode::Integer=0)
7979
req = Libc.malloc(_sizeof_uv_fs)
8080
local handle
8181
try
82-
ret = ccall(:jl_uv_fs_open, Int32,
82+
ret = ccall(:uv_fs_open, Int32,
8383
(Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Int32, Int32, Ptr{Cvoid}),
84-
eventloop(), req, path, flags, mode, C_NULL)
85-
handle = ccall(:jl_uv_fs_result, Cssize_t, (Ptr{Cvoid},), req)
84+
C_NULL, req, path, flags, mode, C_NULL)
85+
handle = ccall(:uv_fs_get_result, Cssize_t, (Ptr{Cvoid},), req)
8686
ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req)
8787
uv_error("open", ret)
8888
finally # conversion to Cstring could cause an exception
@@ -138,9 +138,9 @@ write(f::File, c::UInt8) = write(f, Ref{UInt8}(c))
138138
function truncate(f::File, n::Integer)
139139
check_open(f)
140140
req = Libc.malloc(_sizeof_uv_fs)
141-
err = ccall(:jl_uv_fs_ftruncate, Int32,
141+
err = ccall(:uv_fs_ftruncate, Int32,
142142
(Ptr{Cvoid}, Ptr{Cvoid}, OS_HANDLE, Int64, Ptr{Cvoid}),
143-
eventloop(), req, f.handle, n, C_NULL)
143+
C_NULL, req, f.handle, n, C_NULL)
144144
Libc.free(req)
145145
uv_error("ftruncate", err)
146146
return f
@@ -149,9 +149,9 @@ end
149149
function futime(f::File, atime::Float64, mtime::Float64)
150150
check_open(f)
151151
req = Libc.malloc(_sizeof_uv_fs)
152-
err = ccall(:jl_uv_fs_futime, Int32,
152+
err = ccall(:uv_fs_futime, Int32,
153153
(Ptr{Cvoid}, Ptr{Cvoid}, OS_HANDLE, Float64, Float64, Ptr{Cvoid}),
154-
eventloop(), req, f.handle, atime, mtime, C_NULL)
154+
C_NULL, req, f.handle, atime, mtime, C_NULL)
155155
Libc.free(req)
156156
uv_error("futime", err)
157157
return f

base/io.jl

-3
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ Close an I/O stream. Performs a [`flush`](@ref) first.
6161
"""
6262
function close end
6363
function flush end
64-
function wait_connected end
6564
function wait_readnb end
66-
function wait_readbyte end
6765
function wait_close end
6866
function bytesavailable end
6967

@@ -260,7 +258,6 @@ iswritable(io::AbstractPipe) = iswritable(pipe_writer(io))
260258
isopen(io::AbstractPipe) = isopen(pipe_writer(io)) || isopen(pipe_reader(io))
261259
close(io::AbstractPipe) = (close(pipe_writer(io)); close(pipe_reader(io)))
262260
wait_readnb(io::AbstractPipe, nb::Int) = wait_readnb(pipe_reader(io), nb)
263-
wait_readbyte(io::AbstractPipe, byte::UInt8) = wait_readbyte(pipe_reader(io), byte)
264261
wait_close(io::AbstractPipe) = (wait_close(pipe_writer(io)); wait_close(pipe_reader(io)))
265262

266263
"""

0 commit comments

Comments
 (0)