It sepends on the nature of fun. Some code does not really have points at which it is able to exit. But if it occurs in a loop, you can check the elapsed time in the start of each loop.
But in the general case, without changes inside fun, I think you best best is along the lines of
my_task = @async fun(a, b)
sleep(timeout_duration)
my_result = if istaskfinnished(my_task)
#code to retrieve result from task here
else
# some `yieldto` code if you do not want the computation to finnish in the background
(false, false, false)
end
x, y, z = my_result
This is very rough pseudo-code, but should give an idea. But beweare: what you are asking is hard, and asynchronous programming if significantly harder to do than sequential programming.
julia> sleep(5)
0.335358 seconds (4.62 k allocations: 325.352 KiB, 15.94% compilation time)
0.125368 seconds (38.31 k allocations: 2.736 MiB, 99.26% compilation time)
2.054542 seconds (433.44 k allocations: 29.480 MiB, 0.71% gc time, 97.21% compilation time)
julia> my_result = if istaskfinnished(my_task)
#code to retrieve result from task here
else
# some `yieldto` code if you do not want the computation to finnish in the background
(false, false, false)
end
ERROR: UndefVarError: `istaskfinnished` not defined
Stacktrace:
[1] top-level scope
@ REPL[75]:1
julia> x, y, z = my_result
ERROR: UndefVarError: `my_result` not defined
Stacktrace:
[1] top-level scope
@ REPL[76]:1
I changed the function to eliminate any “Nothing”. Unfortunately, something clashes. Can this be done without parallelism?
julia> function limitowany_czas(url, tagg, limit)
my_task = @sync get_links_by_tagg(url, tagg)
sleep(limit)
my_result = if istaskdone(my_task)
#code to retrieve result from task here
else
# some `yieldto` code if you do not want the computation to finnish in the background
(false, false, false)
end
x, y, z = my_result
end
limitowany_czas (generic function with 1 method)
julia> limit(url, tagg, 200)
0.002283 seconds (111 allocations: 7.078 KiB)
0.000924 seconds (12 allocations: 608 bytes)
0.054315 seconds (40.23 k allocations: 2.606 MiB)
42.964573 seconds (174.80 k allocations: 86.045 MiB, 0.12% gc time)
ERROR: MethodError: no method matching iterate(::Nothing)
Closest candidates are:
iterate(::BitSet)
@ Base bitset.jl:326
iterate(::BitSet, ::Any)
@ Base bitset.jl:326
iterate(::Base.AsyncGenerator, ::Base.AsyncGeneratorState)
@ Base asyncmap.jl:362
...
Stacktrace:
[1] indexed_iterate(I::Nothing, i::Int64)
@ Base .\tuple.jl:95
[2] limit(url::String, tagg::String, limit::Int64)
@ Main .\REPL[88]:11
[3] top-level scope
@ REPL[94]:1
The async approach isn’t using parallelism, it’s using concurrency. I’m being picky because generally when we say parallelism we mean we’re actively doing things in multiple threads of execution, whereas here we’re not - one thread of execution is waiting for a specific amount of time for the other to complete.
In general, something is going to have to wait an amount of time to generate an interrupt of some kind whilst something else is doing work, so this is a kind of naturally concurrent task.
In any case, it’s clear that the if istaskdone() branch just needs to retrieve a value correctly (which it obviously doesn’t do in the version of the code you tested - that branch is empty!).
I’m not completely familiar with Julia’s preferred approach for inter-task communication, but it seems like:
function limitowany_czas(url, tagg, limit)
my_task = @async get_links_by_tagg(url, tagg)
sleep(limit)
my_result = if istaskdone(my_task)
fetch(my_task)
else
# some `yieldto` code if you do not want the computation to finnish in the background
(false, false, false)
end
x, y, z = my_result
end
Yes, thanks, but it has a significant drawback. It always waits for 200 seconds, which significantly extends the working time because many iterations last 1 - 2 seconds. 200 seconds is an absolute limit for difficult cases. You need a mechanism: done, move on… close t ?
Another appreach is to start two async processed that put results into a channel. One puts the default return into the channel after X seconds, the other puts the actual result in once computed. Then, you simply take! your result from the channel, and then close it. That should return after X seconds in any case.
Probably dmart to ensure that Threads.nthreads() is greater than one to make sure that Julia actually has multiple threads at its disposal. If not, the single available thread might not have a point at which it naturally yirlds to other tasks (see yield, or yieldto, or something like it)
Okay, so again, if you read the documentation I linked you to, you can see that there’s a timedwait function that waits for a particular test to be true, or a max time, and returns a value to let you know which happened.
Using that:
function limitowany_czas(url, tagg, limit)
my_task = @async get_links_by_tagg(url, tagg)
my_result = if timedwait(istaskdone(my_task), limit) == :ok
fetch(my_task)
else
# some `yieldto` code if you do not want the computation to finnish in the background
(false, false, false)
end
x, y, z = my_result
end
Again, I’ve not tested this, but this is fairly easy to construct just by reading the documentation.
❯ julia --startup-file=no --threads=auto
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.10.2 (2024-03-01)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> function run_f_with_timeout(f, timeout=10, default_value=missing)
Threads.nthreads() > 1 || @warn "Julia only has access to $(Threads.nthreads()) threads. This is likely to work better with more than one. To change this, start julia with e.g. `julia --threads=auto`"
result_channel = Channel(1)
@async begin
sleep(timeout)
put!(result_channel, default_value)
end
@async begin
f_result = f()
put!(result_channel, f_result)
end
# This call will block until the channel has content
return_value = take!(result_channel)
close(result_channel) # Might be able to make the long-running task error before finnishing. Not sure though.
return return_value
end
run_f_with_timeout (generic function with 3 methods)
julia> run_f_with_timeout() do
sleep(1)
return pi
end
π = 3.1415926535897...
julia> run_f_with_timeout() do
sleep(11)
return pi
end
missing
Note that this does only may interrupt the loop - I am not sure. Again, it is often hard to reason about parallel/concurrent programs.
No, I think your code is always correct, yes? (I was thinking of using a channel - but for this particular case, I think it’s easier for the person reading it to conceptualise the “wait” as a non-thread).
I recently started using GPT myself for programming. My experience is that it is extremely helpful to have an idea of where you are going, and provide outlines of the functionality you want. You cannot really offload the creative aspects of the problem-solving and expect good results, unless it is a very common problem.
The quality of the output usually matches the quality of the input.
See also the chatbot on JuliaHub, which provided a decent answer given the following prompt:
Can you write a function that takes another 0-argument function as its input, and returns the return value from that function? Also, I want the outer function to return a default value after 10 seconds, so that the function always returns some value after 10 seconds
Output:
using Base.Threads
import Base.Threads.@spawn
function run_with_timeout(f::Function, default_value, timeout::Int)
result = Ref{Any}()
t = @spawn begin
result[] = f()
end
sleep(timeout)
if isready(t)
return fetch(result)
else
return default_value
end
end
# Example usage
function my_function()
sleep(5) # Simulating some computation
return "Result after 5 seconds"
end
result = run_with_timeout(my_function, "Default Value", 10)
println(result)
This code errors because you have to use istaskdone instead of isready`. It also constrains the timeout to Int for no reason, and does not stop the long-running task.