Skip to content

Commit d0f028e

Browse files
committed
initial implementation
0 parents  commit d0f028e

File tree

7 files changed

+226
-0
lines changed

7 files changed

+226
-0
lines changed

.github/workflow/CI.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
on:
3+
- push
4+
- pull_request
5+
jobs:
6+
test:
7+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
8+
runs-on: ${{ matrix.os }}
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
version:
13+
- '1.6'
14+
- '1.9'
15+
- '1.10.0'
16+
- 'nightly'
17+
os:
18+
- ubuntu-latest
19+
arch:
20+
- x64
21+
steps:
22+
- uses: actions/checkout@v2
23+
- uses: julia-actions/setup-julia@v1
24+
with:
25+
version: ${{ matrix.version }}
26+
arch: ${{ matrix.arch }}
27+
- uses: actions/cache@v1
28+
env:
29+
cache-name: cache-artifacts
30+
with:
31+
path: ~/.julia/artifacts
32+
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
33+
restore-keys: |
34+
${{ runner.os }}-test-${{ env.cache-name }}-
35+
${{ runner.os }}-test-
36+
${{ runner.os }}-
37+
- uses: julia-actions/julia-buildpkg@v1
38+
- uses: julia-actions/julia-runtest@v1
39+
- uses: julia-actions/julia-processcoverage@v1
40+
- uses: codecov/codecov-action@v1
41+
with:
42+
file: lcov.info

.github/workflow/TagBot.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: TagBot
2+
on:
3+
issue_comment:
4+
types:
5+
- created
6+
workflow_dispatch:
7+
inputs:
8+
lookback:
9+
default: 3
10+
permissions:
11+
actions: read
12+
checks: read
13+
contents: write
14+
deployments: read
15+
issues: read
16+
discussions: read
17+
packages: read
18+
pages: read
19+
pull-requests: read
20+
repository-projects: read
21+
security-events: read
22+
statuses: read
23+
jobs:
24+
TagBot:
25+
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: JuliaRegistries/TagBot@v1
29+
with:
30+
token: ${{ secrets.GITHUB_TOKEN }}
31+
# Edit the following line to reflect the actual name of the GitHub Secret containing your private key
32+
ssh: ${{ secrets.DOCUMENTER_KEY }}
33+
# ssh: ${{ secrets.NAME_OF_MY_SSH_PRIVATE_KEY_SECRET }}

Project.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name = "StableTasks"
2+
uuid = "91464d47-22a1-43fe-8b7f-2d57ee82463f"
3+
authors = ["Mason Protter <[email protected]>"]
4+
version = "0.1.0"
5+
6+
[compat]
7+
julia = "1.6"
8+
9+
[extras]
10+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
11+
12+
[targets]
13+
test = ["Test"]

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# StableTasks.jl
2+
3+
StableTasks is a simple package with one main API `SimpleTasks.@spawn` (not exported by default).
4+
5+
It works like `Threads.@spawn`, except it is *type stable* to `fetch` from (and it does not yet support threadpools
6+
other than the default threadpool).
7+
8+
``` julia
9+
julia> Core.Compiler.return_type(() -> fetch(StableTasks.@spawn 1 + 1), Tuple{})
10+
Int64
11+
```
12+
versus
13+
14+
``` julia
15+
julia> Core.Compiler.return_type(() -> fetch(Threads.@spawn 1 + 1), Tuple{})
16+
Any
17+
```

src/StableTasks.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module StableTasks
2+
3+
macro spawn end
4+
5+
using Base: RefValue
6+
struct StableTask{T}
7+
t::Task
8+
ret::RefValue{T}
9+
end
10+
11+
include("internals.jl")
12+
13+
end # module StableTasks

src/internals.jl

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module Internals
2+
3+
import StableTasks: @spawn, StableTask
4+
5+
function Base.fetch(t::StableTask{T}) where {T}
6+
fetch(t.t)
7+
t.ret[]
8+
end
9+
10+
for func [:wait, :istaskdone, :istaskfailed, :istaskstarted, :yield, :yieldto]
11+
if isdefined(Base, func)
12+
@eval Base.$func(t::StableTask) = $func(t.t)
13+
end
14+
end
15+
16+
Base.yield(t::StableTask, x) = yield(t.t, x)
17+
Base.yieldto(t::StableTask, x) = yieldto(t.t, x)
18+
if isdefined(Base, :current_exceptions)
19+
Base.current_exceptions(t::StableTask; backtrace::Bool=true) = current_exceptions(t.t; backtrace)
20+
end
21+
if isdefined(Base, :errormonitor)
22+
Base.errormonitor(t::StableTask) = errormonitor(t.t)
23+
end
24+
Base.schedule(t::StableTask) = (schedule(t.t); t)
25+
Base.schedule(t, val; error=false) = (schedule(t.t, val; error); t)
26+
27+
28+
macro spawn(ex)
29+
tp = QuoteNode(:default)
30+
31+
letargs = Base._lift_one_interp!(ex)
32+
33+
thunk = Base.replace_linenums!(:(()->($(esc(ex)))), __source__)
34+
var = esc(Base.sync_varname) # This is for the @sync macro which sets a local variable whose name is
35+
# the symbol bound to Base.sync_varname
36+
# I asked on slack and this is apparently safe to consider a public API
37+
quote
38+
let $(letargs...)
39+
f = $thunk
40+
T = Core.Compiler.return_type(f, Tuple{})
41+
ref = Ref{T}()
42+
f_wrap = () -> (ref[] = f(); nothing)
43+
task = Task(f_wrap)
44+
task.sticky = false
45+
if $(Expr(:islocal, var))
46+
put!($var, task) # Sync will set up a Channel, and we want our task to be in there.
47+
end
48+
schedule(task)
49+
StableTask(task, ref)
50+
end
51+
end
52+
end
53+
54+
55+
# Copied from base rather than calling it directly because who knows if it'll change in the future
56+
function _lift_one_interp!(e)
57+
letargs = Any[] # store the new gensymed arguments
58+
_lift_one_interp_helper(e, false, letargs) # Start out _not_ in a quote context (false)
59+
letargs
60+
end
61+
_lift_one_interp_helper(v, _, _) = v
62+
function _lift_one_interp_helper(expr::Expr, in_quote_context, letargs)
63+
if expr.head === :$
64+
if in_quote_context # This $ is simply interpolating out of the quote
65+
# Now, we're out of the quote, so any _further_ $ is ours.
66+
in_quote_context = false
67+
else
68+
newarg = gensym()
69+
push!(letargs, :($(esc(newarg)) = $(esc(expr.args[1]))))
70+
return newarg # Don't recurse into the lifted $() exprs
71+
end
72+
elseif expr.head === :quote
73+
in_quote_context = true # Don't try to lift $ directly out of quotes
74+
elseif expr.head === :macrocall
75+
return expr # Don't recur into macro calls, since some other macros use $
76+
end
77+
for (i,e) in enumerate(expr.args)
78+
expr.args[i] = _lift_one_interp_helper(e, in_quote_context, letargs)
79+
end
80+
expr
81+
end
82+
83+
end # module Internals

test/runtests.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Test, StableTasks
2+
using StableTasks: @spawn
3+
4+
@testset "Type stability" begin
5+
@test 2 ==@inferred fetch(@spawn 1 + 1)
6+
t = @eval @spawn inv([1 2 ; 3 4])
7+
@test inv([1 2 ; 3 4]) == @inferred fetch(t)
8+
end
9+
10+
@testset "API funcs" begin
11+
T = @spawn rand(Bool)
12+
@test isnothing(wait(T))
13+
@test istaskdone(T)
14+
@test istaskfailed(T) == false
15+
@test istaskstarted(T)
16+
r = Ref(0)
17+
@sync begin
18+
@spawn begin
19+
sleep(5)
20+
r[] = 1
21+
end
22+
@test r[] == 0
23+
end
24+
@test r[] == 1
25+
end

0 commit comments

Comments
 (0)