-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
with
for deterministic destruction
#7721
Comments
The |
You can do the same things with julias do notation anonymous function syntax and with macros. Try finally also lends itself to deterministic destruction. |
I brought this up briefly in #4664. I think |
I know it can be done in a custom way by hacking some macros, but it would be much more beneficial if we had this feature in combination with support from the standard library - as it is done in python. Having a standard way of doing it would make new libraries conform to this standard way. Then the users don't have to worry about the method name of the finalizer (Is it close(), release(), free()?). |
@klaufir, it's not hacking some macros. In Julia your example is
and the file is automatically closed for you. See? No macros 😄. But there are places where one might want to use finalizers in most circumstances yet be able to force them to run in specific situations. So there might be room for some new features, but it's not like there isn't already a way to do this kind of thing. |
Oh, I see there is a standard way already. Sorry for the noise. |
That is the standard idiom – writing code to ensure that some code is always called upon exit is still manual though. I've often wanted something like Go's |
I think |
I agree that implementing that pattern usually requires a trip to the manual, and occasionally interferes with some other desirable call syntax. Ran into the latter with CUDArt. |
with
for deterministic destruction
Just checking: the general feeling is that "do" doesn't do it, and "with" is still desirable? |
I'm convinced at this point that with open("input") as r
with open("output", "w") as w
# do actual work
end
end Another problem with both syntaxes is that they cannot be used inline, making them unhelpful, e.g. for the common problem of wanting to open a file only to pass it to a function and then close it when that call returns (see here for example). I noticed that the syntax r = open("input")!
w = open("output", "w")!
# do actual work Calls to write(open("file","w")!, data) Since the result of the try f = open("file","w")
write(f, data)
finally
finalize(f)
end So that addresses #14546 in a systematic way without adding any new methods and can eliminate many of the functions littering #14608. |
Wow, I kind of like that idea. It would be excellent if resource-backed objects only needed to implement |
Just for a second, can we imagine inverting the syntax: so v = ...! covers the unusual case where I do want to wait for GC to finalize v? How much existing code depends on that? Which bugs are worse: referencing prematurely-finalized objects, or leaking resources? The former are pretty easy to detect, at least at the cost of the finalizer explicitly putting the object into an invalid state. |
See also #11207 |
@awf you don't want to translate every function call into a try/catch with a corresponding finalize call. |
I think we can have
vs
|
Nice. +1 |
I like the idea, but not the syntax. Sigils for resource management starts getting into rust levels of "what is going on" for newcomers unfamiliar with what a trailing |
@JeffBezanson: shouldn't we be having this call |
Next step is an Although the implementation will be quite different, I hope that the syntax to use it can be made similar to the one discussed here. A macro might work: dothework(@fin open("file"))
dothework(@rc open("file")) |
@tkelman: Given the potential ubiquity of this, a very lightweight syntax is essential. Since you don't like the syntax, please propose alternatives. This cannot be done with a function since it interacts with the surrounding syntax; if we had |
I kind of like the idea of combining
-100. I would be amazed if there is any reasonable way to make that work. The right way to handle this case is to have the compiler insert speculative early-free calls. |
I don't mind the indentation of the do block form. I think readable and intuitive syntax should trump saving keystrokes especially for subtle sources of bugs like resource management. Defer with a macro would be easier to explain the rules for than a sigil handled at the parser level. |
I've watched a lot of people write code like this over and over: f = open("file")
# do work
close(f) I cannot get them to use the do-block form, even though I've explained many times that it's the right way to express this since it prevents unclosed file handles in case of exceptions. Of course, the file handles do eventually get closed on gc, so it's not dire, but in general, if people want to do something one way, it's better to make the thing they want to do work right rather than lecturing them about how some other way to do it is better. I doubt we'd have more luck with getting people to use the Longer term, this syntax would entirely eliminate the need for having do-block versions of all functions to do cleanup. That's a big win. But the real question is whether it's important enough to have its own syntax. I would argue that the pattern of doing setup, then doing work, then doing some cleanup (regardless of how the stack unwinds) is ubiquitous. It's also annoying to get right without syntactic support and as above, people usually just don't bother. So in my view, having syntactic support for doing this pattern correctly is a no-brainer. Whether the
Makes sense to me. I suspect it will make sense to other people too and not be terribly hard to explain. |
Right, having something like But I think this is orthogonal to the point I'm trying to make here. It's the following dichotomy which is bothering me:
Hah, with the transformation |
Sorry, but i disagree with the proposal of merging the management of
macro lock(l, expr)
quote
temp = $(esc(l))
lock(temp)
try
$(esc(expr))
finally
unlock(temp)
end
end
end while macro sync(block) # ~ 10 sloc @ https://github.com/JuliaLang/julia/blob/v1.5.2/base/task.jl
var = esc(sync_varname)
quote
let $var = Channel(Inf)
v = $(esc(block))
sync_end($var)
v
end
end
end
# and
function sync_end(c::Channel{Any}) # ~ 30 sloc
local c_ex
while isready(c)
r = take!(c)
if isa(r, Task)
_wait(r)
if istaskfailed(r)
if !@isdefined(c_ex)
c_ex = CompositeException()
end
push!(c_ex, TaskFailedException(r))
end
else
try
wait(r)
catch e
if !@isdefined(c_ex)
c_ex = CompositeException()
end
push!(c_ex, e)
end
end
end
close(c)
if @isdefined(c_ex)
throw(c_ex)
end
nothing
end
then # https://github.com/JuliaLang/julia/blob/v1.5.2/base/channels.jl#L381
take!(c::Channel)) # ~ 30 sloc
take_buffered(c::Channel)
take_unbuffered(c::Channel{T}) where {T} nearly 100 sloc. to use
EDIT to let the common forms speak by themselves : # open @ https://github.com/JuliaLang/julia/blob/v1.5.2/base/io.jl
function open(f::Function, args...; kwargs...)
io = open(args...; kwargs...)
try
f(io)
finally
close(io)
end
end
# Base.@lock @ https://github.com/JuliaLang/julia/blob/v1.5.2/base/lock.jl
macro lock(l, expr)
quote
temp = $(esc(l))
lock(temp)
try
$(esc(expr))
finally
unlock(temp)
end
end
end |
Rather than annotating individual objects, would it be possible to annotate a type for "aggressive" memory management, so that the compiler is encouraged to call In all the examples given so far, it seems like we want to perform deterministic destruction of essentially every object of certain types, e.g. file handles. In that case, it would be better not to rely on the programmer remembering to include call-site annotations. And ordinary garbage collection will still operate as a fallback when static analysis fails. See also #34836. I realize that we don't want reference-counting-like semantics for every type because of the performance implications, but it seems like it would be helpful to have it for some types. And since this is essentially just a performance hint it will be backwards compatible (and will automatically improve old code using these types). |
If we had escape analysis, the compiler would just do that for every object. There's no need to opt into it. |
So, when interfacing with C, finalizers are a very common source of bugs, and were incredibly painful to deal with when trying to use Vulkan.jl . It'd definitely be great if there were an abstraction that let you deal with values that should never be GCed, but should be disposed of by scope at a specified point with a destructor. @vtjnash I strongly support escape analysis for exactly that reason (inserted frees whenever something does not escape). It'd be great if there were a complementary syntax that also guarentees an object never escapes though. |
I use https://github.com/adambrewster/Defer.jl for that. Works nice. Here is an example: https://github.com/JuliaGPU/AMGX.jl/blob/e7ac5aaf3d141425dcb1f5ebd2b203901f6e5856/test/test_solver.jl#L9-L15. |
Okay, that looks pretty close to what I was looking for, with the |
@saolof, I've found the opposite to be true: finalizers are a godsend when interfacing with C code as they provide a simple means of ensuring that C-allocated objects are finalized and freed when they are no longer reachable. What kinds of issues did you encounter? |
FWIW, I tried for a long time to use finalizers in AMGX.jl but it just want possible to uphold e.g the ordering of destruction needed. So in the end I just recommend the Defer.jl approach for the package. |
Note that Defer.jl uses mutable global state to manage the scopes so it's more a useful behavioral prototype than a production-ready package. Related to Defer.jl I've been thinking about this problem again in the last few days, and have just started a new package https://github.com/c42f/Contexts.jl. To quote some of the readme:
Some examples: function f()
@context readlines(@!(open("tmp/hi.txt", "r")))
end Create a temporary file and ensure it's cleaned up afterward @context function f()
path, io = @! mktemp()
write(io, "content")
flush(io)
@info "mktemp output" path ispath(path) isopen(io) read(path, String)
end Defer shredding of a secretbuffer until scope exit @context function f()
buf = SecretBuffer()
@defer shred!(buf)
secret_computation(buf)
end Superficially this is similar to Defer.jl, but the internal context-passing design resolves the problems with composability I was trying to describe in #7721 (comment). It's also compatible with existing verbs like |
Finalizers can cause double frees if you try to call the disposal function directly, so if you use them you implicitly give up the ability to manually destroy something before the GC removes it, and sometime you run into the opposite issue of it being run while C code is supposed to hold onto the data. When finalization order matters and you want it to happen at the end of some scope, that is incredibly annoying. If the compiler had good escape analysis and there were some statement that ensured that an object doesn't escape a scope or live past a certain point but keeps it alive until then, it'd be a different issue (arguably this is an overlap of several possible features in the language design space from pinning to linear types) |
I might be missing the point... would this not work? Recording the fact that the disposal function has been called in the Julia wrapper object? mutable struct Wrapper
ptr::CPtr
Wrapper() = (...; register finalizer)
end
free(wrapper::Wrapper) = if wrapper.ptr c_free(wapper.ptr); wrapper.ptr=null end
finalize(wrapper::Wrapper) = if wrapper.ptr c_free(wrapper.ptr) end |
Yes, you can quite easily make finalizers idempotent but that's just a small part of the crux. |
Exactly 👍 To get the ordering correct when using finalizers, you'd have to ensure that each wrapper also held a Julia reference to any parent resources it wants to keep alive. This is possible but it bulks up the wrappers on the Julia side and is generally just annoying. I think Contexts.jl solves these problems in a pleasant way, and I'm excited because it also solves a bunch of problems I didn't expect. For example, have you ever wanted to return a raw using Contexts
@! function raw_buffer(len)
buf = Vector{UInt8}(undef, len)
@defer GC.@preserve buf nothing
pointer(buf)
end
@context begin
len = 1_000_000_000
ptr = @! raw_buffer(len)
GC.gc() # `buf` is preserved!
unsafe_store!(ptr, 0xff)
end |
Starred the repo. This is exactly what I need |
Excellent. I've got a registration PR for this at JuliaRegistries/General#36658 so you should be able to use it conveniently soon. However, I'm considering using a less generic name for the package so I've put the merge on hold for now. In the meantime if you try the package out I'd appreciate any feedback.
Good question. There's not a lot of overlap:
The use cases for these two different types of contexts are fairly different. |
Ok, I've renamed Contexts.jl as ResourceContexts.jl and it's now registered in the General registry. A long while back (years ago now!) @JeffBezanson wrote
I think it's interesting to compare the implicit There's some benefits to passing a context:
Clearly context passing introduces a form of colored functions (colored by their calling convention). It's possible there's some drawbacks for this, as it makes context passing an official part of the API of a function — in a sense, it bifurcates method tables into two based on the syntax used at the callsite. There's a big potential benefit for this, however — users will get a method error for calling |
FWIW here is the with function of LibGit2 @ julia v1.6 function with(f::Function, obj)
try
f(obj)
finally
close(obj)
end
end May clearly be promoted to a more generic "module host". It may be enhanced too by handling a return for f function with(f::Function, obj)
local r
try
r = f(obj)
finally
close(obj)
end
r
end tests : mutable struct A
v
step # consumption
A(v) = new(v, 10) # big step
end
f(a::A) = a.v * a.step
close(a::A) = a.step = 1 # small step after close
using Test
a = A(1)
@test with(f, a) == 10
@test f(a) == 1 # no it's closed |
How about defining a generic function with(fn, args...)
try
fn(args...)
finally
for arg in args
finalize(arg)
end
end
end |
solved by #7721 (comment) |
There's no catch, so it should propagate the error, and not return |
With eager finalization (#45272), couldn't destruction be guaranteed for something as simple as a |
There are no guarantees, but I did have a sketch of a design that would add those guarantees based on the same mechanism that I had discussed with @JeffBezanson and @StefanKarpinski at some point. |
Can someone help me understand how the eager finalization works? Is it just based on whether the compiler can figure out the lifetime of the object and infer the finalizer? Are there other ways/requirements to "opt in" to eager finalization? |
Pretty much
The compiler needs to be able to prove :nothrow and :notaskstate on the actual finalizer itself. |
Deterministic destruction
Deterministic destruction is a guarantee that some specified resources will be released when exiting from the enclosing block even if we exit from the block by the means of exceptions.
Example
In python deterministic destruction is done using the
with
statement.Considering the code above we don't have to worry about closing a file explicitly. After the
with
block, the file will be closed. Even when something throws an exception inside thewith
block, the resources handled by thewith
statement will be released (in this case closed).Other languages
using
statement for this same purpose.Julia?
It is my firm belief that Julia also needs to have a feature supporting deterministic destruction. We have countless cases when a resource needs to be closed at soon as possible: serial ports, database connections, some external api handles, files, etc. In cases like these deterministic destruction would mean cleaner code, no explicit
close()
/release()
calls and no sandwiching the code intry
..catch
..finally
blocks.The text was updated successfully, but these errors were encountered: