From de09bb2c9c4e5615b30c35adb12bbdeac7245560 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 11 Jun 2021 22:29:13 -0400 Subject: [PATCH] Setup documentation --- .github/workflows/docs.yml | 40 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 2 +- docs/.gitignore | 2 ++ docs/Project.toml | 2 ++ docs/make.jl | 28 ++++++++++++++++++++++++ docs/src/index.md | 29 +++++++++++++++++++++++++ src/ConcurrentCollections.jl | 2 ++ src/docs/ConcurrentDict.md | 26 +++++++++++++++++++++++ src/docs/ConcurrentQueue.md | 28 ++++++++++++++++++++++++ src/docs/ConcurrentStack.md | 28 ++++++++++++++++++++++++ src/docs/Delete.md | 24 +++++++++++++++++++++ src/docs/Keep.md | 25 ++++++++++++++++++++++ src/docs/WorkStealingDeque.md | 38 +++++++++++++++++++++++++++++++++ src/docs/modify!.md | 35 ++++++++++++++++++++++++++++++ src/docs/tryget.md | 2 ++ src/docs/trypop!.md | 19 +++++++++++++++++ src/docs/trypopfirst!.md | 20 ++++++++++++++++++ src/utils.jl | 19 +++++++++++++++++ test/Project.toml | 2 ++ test/test_doctest.jl | 11 ++++++++++ 20 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/.gitignore create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/index.md create mode 100644 src/docs/ConcurrentDict.md create mode 100644 src/docs/ConcurrentQueue.md create mode 100644 src/docs/ConcurrentStack.md create mode 100644 src/docs/Delete.md create mode 100644 src/docs/Keep.md create mode 100644 src/docs/WorkStealingDeque.md create mode 100644 src/docs/modify!.md create mode 100644 src/docs/tryget.md create mode 100644 src/docs/trypop!.md create mode 100644 src/docs/trypopfirst!.md create mode 100644 test/test_doctest.jl diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..d4d0b10 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,40 @@ +name: Documentation + +on: + push: + branches: + - master + - actions/trigger/docs + tags: '*' + pull_request: + +jobs: + Documenter: + runs-on: ubuntu-latest + env: + GKSwstype: "nul" + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: nightly + # Use `JULIA_PKG_SERVER` mitigation implemented in julia-buildpkg: + - uses: julia-actions/julia-buildpkg@v1 + - name: Install Run.jl + run: julia -e 'using Pkg; pkg"add Run@0.1"' + - name: Install dependencies + run: julia -e 'using Run; Run.prepare_docs()' + - name: Build and deploy + id: build-and-deploy + if: | + github.event_name == 'push' || ( + github.event_name == 'pull_request' && + !contains(github.head_ref, 'create-pull-request/') + ) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.SSH_KEY }} + run: julia -e 'using Run; Run.docs()' + - name: Just build + if: steps.build-and-deploy.outcome == 'skipped' + run: julia -e 'using Run; Run.docs()' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1229aff..eb4d9ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - julia-version: ['1.6', 'nightly'] + julia-version: ['nightly'] fail-fast: false name: Test Julia ${{ matrix.julia-version }} steps: diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a303fff --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +build/ +site/ diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..dfa65cd --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,2 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..fb1fcd3 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,28 @@ +using Documenter +using ConcurrentCollections + +makedocs( + sitename = "ConcurrentCollections", + format = Documenter.HTML(), + modules = [ConcurrentCollections], +) + +for (root, dirs, files) in walkdir(joinpath(@__DIR__, "build")) + for name in files + path = joinpath(root, name) + if endswith(name, ".html") || name == "search_index.js" + html = replace( + read(path, String), + "ConcurrentCollections.Implementations" => "ConcurrentCollections", + ) + write(path, html) + end + end +end + +# Documenter can also automatically deploy documentation to gh-pages. +# See "Hosting Documentation" and deploydocs() in the Documenter manual +# for more information. +#=deploydocs( + repo = "" +)=# diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..8ba545d --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,29 @@ +# ConcurrentCollections.jl + +```@index +``` + +## Collections + +```@docs +ConcurrentQueue +ConcurrentStack +WorkStealingDeque +``` + +## Functions + +```@docs +trypop! +trypopfirst! +``` + +## Experimental + +```@docs +ConcurrentDict +modify! +tryget +Keep +Delete +``` diff --git a/src/ConcurrentCollections.jl b/src/ConcurrentCollections.jl index 4ed5c16..77a3b4c 100644 --- a/src/ConcurrentCollections.jl +++ b/src/ConcurrentCollections.jl @@ -69,4 +69,6 @@ end # module Implementations using .Implementations: ConcurrentQueue, ConcurrentStack, WorkStealingDeque +Implementations.define_docstrings() + end # baremodule ConcurrentCollections diff --git a/src/docs/ConcurrentDict.md b/src/docs/ConcurrentDict.md new file mode 100644 index 0000000..fd75ad4 --- /dev/null +++ b/src/docs/ConcurrentDict.md @@ -0,0 +1,26 @@ + ConcurrentDict{K,V}() + +Concurrent dictionary. All operations are lock-free except when the dictionary +is resized. + +!!! warning + `ConcurrentDict` is experimental because it cannot be implemented with the + builtin atomics. + +!!! note + Although tasks `wait` on concurrent modifications (e.g., `setindex!`) during + resize, the worker threads participate in the resize to avoid wasting CPU + resources. + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> dict = ConcurrentDict{String,Int}(); + +julia> dict["hello"] = 1; + +julia> dict["hello"] +1 +``` diff --git a/src/docs/ConcurrentQueue.md b/src/docs/ConcurrentQueue.md new file mode 100644 index 0000000..bf15a37 --- /dev/null +++ b/src/docs/ConcurrentQueue.md @@ -0,0 +1,28 @@ + ConcurrentQueue{T}() + +Concurrent queue of objects of type `T`. + +Use `push!` to insert an element at the tail and [`trypopfirst!`](@ref) to +retrieve and remove an element at the head. + +Implementation detail: It implements the Michael and Scott queue. + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> queue = ConcurrentQueue{Int}(); + +julia> push!(queue, 1); + +julia> push!(queue, 2); + +julia> popfirst!(queue) +1 + +julia> trypopfirst!(queue) +Some(2) + +julia> trypopfirst!(queue) # returns nothing +``` diff --git a/src/docs/ConcurrentStack.md b/src/docs/ConcurrentStack.md new file mode 100644 index 0000000..1d8fd0a --- /dev/null +++ b/src/docs/ConcurrentStack.md @@ -0,0 +1,28 @@ + ConcurrentStack{T}() + +Concurrent stack of objects of type `T`. + +Use `push!` to insert an element and [`trypop!`](@ref) to retrieve and remove an +element. + +It implements the Treiber stack. + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> stack = ConcurrentStack{Int}(); + +julia> push!(stack, 1); + +julia> push!(stack, 2); + +julia> pop!(stack) +2 + +julia> trypop!(stack) +Some(1) + +julia> trypop!(stack) # returns nothing +``` diff --git a/src/docs/Delete.md b/src/docs/Delete.md new file mode 100644 index 0000000..1f97b1f --- /dev/null +++ b/src/docs/Delete.md @@ -0,0 +1,24 @@ + Delete(ans) + +A special type used in [`modify!`](@ref) to indicate that a slot should be +removed. + +That is to say + +```Julia +y = modify!(dict, key) do value + Delete(f(something(value))) +end +y[] +``` + +is an optimization of + +```Julia +r = Ref{Any}() +modify!(dict, key) do value + r[] = f(something(value)) + nothing +end +r[] +``` diff --git a/src/docs/Keep.md b/src/docs/Keep.md new file mode 100644 index 0000000..fe73796 --- /dev/null +++ b/src/docs/Keep.md @@ -0,0 +1,25 @@ + Keep(ans) + +A special type used in [`modify!`](@ref) to indicate that a slot should be +remain unchanged while propagating the result `ans` of some computation to +the caller. + +That is to say, + +```Julia +y = modify!(dict, key) do value + Keep(f(something(value))) +end +y[] +``` + +is an optimization of + +```Julia +r = Ref{Any}() +modify!(dict, key) do value + r[] = f(something(value)) + Some(value) +end +r[] +``` diff --git a/src/docs/WorkStealingDeque.md b/src/docs/WorkStealingDeque.md new file mode 100644 index 0000000..b9022b1 --- /dev/null +++ b/src/docs/WorkStealingDeque.md @@ -0,0 +1,38 @@ + WorkStealingDeque{T}() + +Concurrent work-stealing "deque" of objects of type `T`. + +This is not a full deque in the sense that: + +* `push!` and [`trypop!`](@ref) operating at the tail of the collection can + only be executed by a single task. +* [`trypopfirst!`](@ref) (aka steal) for retrieving and removing an element at + the head can be invoked from any tasks. However, there is no `pushfirst!`. + +Implementation detail: It implements the dynamic circular work-stealing deque by +Chase and Lev (2005). + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> deque = WorkStealingDeque{Int}(); + +julia> push!(deque, 1); + +julia> push!(deque, 2); + +julia> push!(deque, 3); + +julia> trypop!(deque) +Some(3) + +julia> fetch(Threads.@spawn trypopfirst!(deque)) +Some(1) + +julia> fetch(Threads.@spawn popfirst!(deque)) +2 + +julia> trypopfirst!(deque) # returns nothing +``` diff --git a/src/docs/modify!.md b/src/docs/modify!.md new file mode 100644 index 0000000..60771d3 --- /dev/null +++ b/src/docs/modify!.md @@ -0,0 +1,35 @@ + modify!(f, dict::ConcurrentDict{K,V}, key::K) -> y + +Atomically update `key` slot of `dict` using a function `f`. + +If `key` does not exist, `f` is called with `nothing`. The call `f(nothing)` +must return either (1) `nothing` to keep the slot unoccupied or (2) +`Some(value::V)` to insert `value`. + +If `key` exist, `f` is called with a `ref` such that `ref[]` retrieves the +`value` corresponding to the `key`. The call `f(ref)` must return either (1) +`nothing` to delete the slot, (2) `Some(value′::V)` to insert `value`, (3) +[`Keep(ans)`](@ref ConcurrentCollection.Keep) to return `y = Keep(ans)` from +`modify!`, or (4) [`Delete(ans)`](@ref ConcurrentCollection.Delete) to delete +slot and return a value `y = Delete(ans)` from `modify!`. + +The function `f` may be called more than once if multiple tasks try to modify +the dictionary. + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> dict = ConcurrentDict{String,Int}(); + +julia> modify!(dict, "hello") do _ + Some(1) + end +Some(1) + +julia> modify!(dict, "hello") do ref + Some(something(ref[]) + 1) + end +Some(2) +``` diff --git a/src/docs/tryget.md b/src/docs/tryget.md new file mode 100644 index 0000000..c2cb890 --- /dev/null +++ b/src/docs/tryget.md @@ -0,0 +1,2 @@ + tryget(dict::ConcurrentDict{K,V}, key) -> Some(value::T) or nothing + diff --git a/src/docs/trypop!.md b/src/docs/trypop!.md new file mode 100644 index 0000000..ad7d3c8 --- /dev/null +++ b/src/docs/trypop!.md @@ -0,0 +1,19 @@ + trypop!(collection) -> Some(value::T) or nothing + +Try to pop a `value` from the tail of `collection`. Return `Some(value)` if it +is non-empty. Return `nothing` if empty. + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> stack = ConcurrentStack{Int}(); + +julia> push!(stack, 1); + +julia> trypop!(stack) +Some(1) + +julia> trypop!(stack) # returns nothing +``` diff --git a/src/docs/trypopfirst!.md b/src/docs/trypopfirst!.md new file mode 100644 index 0000000..78f237d --- /dev/null +++ b/src/docs/trypopfirst!.md @@ -0,0 +1,20 @@ + trypopfirst!(collection) -> Some(value::T) or nothing + + +Try to pop a `value` from the head of `collection`. Return `Some(value)` if it +is non-empty. Return `nothing` if empty. + +# Examples + +```julia +julia> using ConcurrentCollections + +julia> queue = ConcurrentQueue{Int}(); + +julia> push!(queue, 1); + +julia> trypopfirst!(queue) +Some(1) + +julia> trypopfirst!(queue) # returns nothing +``` diff --git a/src/utils.jl b/src/utils.jl index 81720ef..de7e483 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -224,3 +224,22 @@ function threaded_typed_mapreduce(f, ::Type{T}, op, xs; kw...) where {T} end return mapreduce(getindex, op, refs; kw...) end + +function define_docstrings() + docstrings = [:ConcurrentCollections => joinpath(dirname(@__DIR__), "README.md")] + docsdir = joinpath(@__DIR__, "docs") + for filename in readdir(docsdir) + stem, ext = splitext(filename) + ext == ".md" || continue + name = Symbol(stem) + name in names(ConcurrentCollections, all=true) || continue + push!(docstrings, name => joinpath(docsdir, filename)) + end + for (name, path) in docstrings + include_dependency(path) + doc = read(path, String) + doc = replace(doc, r"^```julia"m => "```jldoctest $name") + doc = replace(doc, "TAB" => "_TAB_") + @eval ConcurrentCollections $Base.@doc $doc $name + end +end diff --git a/test/Project.toml b/test/Project.toml index 85c929d..535a09b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,4 +1,6 @@ [deps] BangBang = "198e06fe-97b7-11e9-32a5-e1d131e6ad66" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +ConcurrentCollections = "5060bff5-0b44-40c5-b522-fcd3ca5cecdd" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/test_doctest.jl b/test/test_doctest.jl new file mode 100644 index 0000000..58a5bb5 --- /dev/null +++ b/test/test_doctest.jl @@ -0,0 +1,11 @@ +module TestDoctest + +import ConcurrentCollections +using Documenter: doctest +using Test + +@testset "doctest" begin + doctest(ConcurrentCollections) +end + +end # module