Skip to content

Commit c8595c6

Browse files
authored
Document the blocking and yield API for various resources (#97)
1 parent b8de238 commit c8595c6

File tree

13 files changed

+168
-82
lines changed

13 files changed

+168
-82
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# News
22

3+
## v1.2.0 - 2023-08-06
4+
5+
- Priorities can now be non-integer.
6+
- Relax some of the previous deprecations, implement `Base.lock` and `Base.trylock`, and document the differences in blocking and yield-ness of Base and ConcurrentSim methods.
7+
38
## v1.1.0 - 2023-08-02
49

510
- Start using `Base`'s API: `Base.unlock`, `Base.islocked`, `Base.isready`, `Base.put!`, `Base.take!`. Deprecate `put`, `release`. Moreover, consider using `Base.take!` instead of `Base.get` (which was not deprecated yet, as we decide which semantics to follow). Lastly, `Base.lock` and `Base.trylock` are **not** implement -- they are superficially similar to `request` and `tryrequest`, but have to be explicitly `@yield`-ed.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ license = "MIT"
55
desc = "A discrete event process oriented simulation framework."
66
authors = ["Ben Lauwens and SimJulia and ConcurrentSim contributors"]
77
repo = "https://github.com/JuliaDynamics/ConcurrentSim.jl.git"
8-
version = "1.1.0"
8+
version = "1.2.0"
99

1010
[deps]
1111
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,7 @@ A [detailed change log is kept](https://github.com/JuliaDynamics/ConcurrentSim.j
5959

6060
## Alternatives
6161

62-
`ConcurrentSim.jl` and `DiscreteEvents.jl` both provide for typical event-based simulations. `ConcurrentSim.jl` is built around coroutines (implemented in `ResumableFunctions.jl`), while `DiscreteEvents.jl` uses Julia's async primitives via `Channels`. If you are evaluating which library to you for your goals, `ConcurrentSim.jl` might be a good choice if you are used to python's SimPy, but otherwise you are advised to try a small demo project in each and do your own benchmarks. Do not hesitate to submit issues on Github with questions or suggestions or feature requests. We value hearing what your experience with this library (compared to other libraries) has been.
62+
`ConcurrentSim.jl` and `DiscreteEvents.jl` both provide for typical event-based simulations.
63+
`ConcurrentSim.jl` is built around coroutines (implemented in `ResumableFunctions.jl`), while `DiscreteEvents.jl` uses Julia's async primitives via `Channels`.
64+
`DiscreteEvents.jl` has an explicit clock that "ticks" at a fixed finite resolution, while `ConcurrentSim.jl` uses coroutines to make arbitrary jumps in time.
65+
If you are evaluating which library to you for your goals, `ConcurrentSim.jl` might be a good choice if you are used to python's SimPy, but otherwise you are advised to try a small demo project in each and do your own benchmarks. Do not hesitate to submit issues on Github with questions or suggestions or feature requests. We value hearing what your experience with this library (compared to other libraries) has been.

docs/make.jl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ makedocs(
1313
pages = [
1414
"Home" => "index.md",
1515
"Tutorial" => "tutorial.md",
16-
"Topical Guides" => ["Basics" => "guides/basics.md",
17-
"Environments" => "guides/environments.md",
18-
"Events" => "guides/events.md",],
19-
"Examples" => ["Ross" => "examples/ross.md", "Latency" =>
20-
"examples/Latency.md"],
16+
"Topical Guides" => [
17+
"Basics" => "guides/basics.md",
18+
"Environments" => "guides/environments.md",
19+
"Events" => "guides/events.md",
20+
"Resource API" => "guides/blockingandyielding.md",
21+
],
22+
"Examples" => [
23+
"Ross" => "examples/ross.md",
24+
"Latency" => "examples/Latency.md",
25+
"Multi-server Queue" => "examples/mmc.md",
26+
],
2127
"API" => "api.md"
2228
]
2329
)

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Private = false
66
```
77

88
```@docs
9+
lock(res::Resource; priority=0)
910
unlock(res::Resource; priority=0)
1011
take!(sto::Store, filter::Function=get_any_item; priority=0)
1112
```

docs/src/examples/mmc.md

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ An [M/M/c queue](https://en.wikipedia.org/wiki/M/M/c_queue) is a basic queue wit
66

77
## Code
88

9-
```julia
9+
```jldoctest
10+
using StableRNGs
11+
using Distributions
12+
using ConcurrentSim
13+
using ResumableFunctions
14+
1015
#set simulation parameters
11-
Random.seed!(8710) # set random number seed for reproducibility
16+
rng = StableRNG(123)
1217
num_customers = 10 # total number of customers generated
1318
1419
# set queue parameters
@@ -24,51 +29,53 @@ service_dist = Exponential(1 / mu) # service time distribution
2429
println("Customer $id arrived: ", now(env))
2530
@yield request(server) # customer starts service
2631
println("Customer $id entered service: ", now(env))
27-
@yield timeout(env, rand(d_s)) # server is busy
32+
@yield timeout(env, rand(rng,d_s)) # server is busy
2833
@yield unlock(server) # customer exits service
2934
println("Customer $id exited service: ", now(env))
3035
end
3136
3237
# setup and run simulation
33-
sim = Simulation() # initialize simulation environment
34-
server = Resource(sim, num_servers) # initialize servers
35-
arrival_time = 0.0
36-
for i = 1:num_customers # initialize customers
37-
arrival_time += rand(arrival_dist)
38-
@process customer(sim, server, i, arrival_time, service_dist)
38+
function setup_and_run()
39+
sim = Simulation() # initialize simulation environment
40+
server = Resource(sim, num_servers) # initialize servers
41+
arrival_time = 0.0
42+
for i = 1:num_customers # initialize customers
43+
arrival_time += rand(rng,arrival_dist)
44+
@process customer(sim, server, i, arrival_time, service_dist)
45+
end
46+
run(sim) # run simulation
3947
end
40-
run(sim) # run simulation
48+
setup_and_run()
4149
42-
## output
43-
#
44-
# Customer 1 arrived: 0.1229193244813443
45-
# Customer 1 entered service: 0.1229193244813443
46-
# Customer 2 arrived: 0.22607641035584877
47-
# Customer 2 entered service: 0.22607641035584877
48-
# Customer 3 arrived: 0.4570009029409502
49-
# Customer 2 exited service: 1.7657345101378559
50-
# Customer 3 entered service: 1.7657345101378559
51-
# Customer 1 exited service: 2.154824561031012
52-
# Customer 3 exited service: 2.2765287086137764
53-
# Customer 4 arrived: 2.3661687470062995
54-
# Customer 4 entered service: 2.3661687470062995
55-
# Customer 5 arrived: 2.6110816119637885
56-
# Customer 5 entered service: 2.6110816119637885
57-
# Customer 5 exited service: 2.8017888690417583
58-
# Customer 6 arrived: 3.019540357955037
59-
# Customer 6 entered service: 3.019540357955037
60-
# Customer 6 exited service: 3.351151832298383
61-
# Customer 7 arrived: 3.5254699872847612
62-
# Customer 7 entered service: 3.5254699872847612
63-
# Customer 7 exited service: 4.261422043181396
64-
# Customer 4 exited service: 4.602071952938201
65-
# Customer 8 arrived: 7.27536704811686
66-
# Customer 8 entered service: 7.27536704811686
67-
# Customer 9 arrived: 7.491176033637809
68-
# Customer 9 entered service: 7.491176033637809
69-
# Customer 10 arrived: 8.39098457094977
70-
# Customer 8 exited service: 8.683396356977969
71-
# Customer 10 entered service: 8.683396356977969
72-
# Customer 9 exited service: 8.7501656586875
73-
# Customer 10 exited service: 9.049670951561666
50+
# output
51+
Customer 1 arrived: 0.14518451436852475
52+
Customer 1 entered service: 0.14518451436852475
53+
Customer 2 arrived: 0.5941831542903504
54+
Customer 2 entered service: 0.5941831542903504
55+
Customer 3 arrived: 1.5490648267819074
56+
Customer 4 arrived: 1.6242796925312217
57+
Customer 5 arrived: 1.6911000709069648
58+
Customer 1 exited service: 2.200985520126681
59+
Customer 3 entered service: 2.200985520126681
60+
Customer 6 arrived: 2.2989039524296317
61+
Customer 3 exited service: 3.5822120399442174
62+
Customer 4 entered service: 3.5822120399442174
63+
Customer 7 arrived: 4.377930221620456
64+
Customer 8 arrived: 5.16494279700802
65+
Customer 2 exited service: 5.900722829377648
66+
Customer 5 entered service: 5.900722829377648
67+
Customer 9 arrived: 7.0099944106308705
68+
Customer 10 arrived: 7.828990220943469
69+
Customer 5 exited service: 9.634196437885254
70+
Customer 6 entered service: 9.634196437885254
71+
Customer 4 exited service: 9.670688398447817
72+
Customer 7 entered service: 9.670688398447817
73+
Customer 7 exited service: 15.066978111608014
74+
Customer 8 entered service: 15.066978111608014
75+
Customer 8 exited service: 16.655548432659554
76+
Customer 9 entered service: 16.655548432659554
77+
Customer 6 exited service: 17.401833154870328
78+
Customer 10 entered service: 17.401833154870328
79+
Customer 9 exited service: 17.586065352135993
80+
Customer 10 exited service: 18.690264775280085
7481
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Blocking and Yielding Resource API
2+
3+
The goal of this page is to list the most common synchronization and resource management patterns used in `ConcurrentSim.jl` simulations and to briefly compare them to Julia's base capabilities for asynchronous and parallel programming.
4+
5+
There are many different approaches to discrete event simulation in particular and to asynchronous and parallel programming in general. This page assumes some rudimentary understanding of concurrency in programming. While not necessary, you are encouraged to explore the following resources for a more holistic understanding:
6+
7+
- "concurrency" vs "parallelism" - see [stackoverflow.com](https://stackoverflow.com/questions/1050222/what-is-the-difference-between-concurrency-and-parallelism) on the topic;
8+
- "threads" vs "tasks": A task is the actual piece of work, a thread is the "runway" on which a task runs. You can have more tasks than threads and you can even have tasks that jump between threads - see Julia's [parallel programming documentation](https://docs.julialang.org/en/v1/manual/parallel-computing/) (in particular the [async](https://docs.julialang.org/en/v1/manual/asynchronous-programming/) and [multithreading](https://docs.julialang.org/en/v1/manual/multi-threading/) docs), and multiple Julia blog post on [multithreading](https://julialang.org/blog/2019/07/multithreading/) and [its misuses](https://julialang.org/blog/2023/07/PSA-dont-use-threadid/);
9+
- "locks" used to guard (or synchronize) the access to a given resource: i.e. one threads locks an array while modifying it in order to ensure that another thread will not be modifying it at the same time. Julia's `Base` multithreading capabilities provide a `ReentrantLock`, together with a `lock`, `trylock`, `unlock`, and `islocked` API;
10+
- "channels" used to organize concurrent tasks. Julia's `Base` multithreading capabilities provide `Channel`, together with `take!`, `put!`, `isready`;
11+
- knowing of the ["red/blue-colored functions" metaphor](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) can be valuable as well as learning of "promises" and "futures".
12+
13+
Programming discrete event simulations can be very similar to async parallel programming, except for the fact that in the simulation the "time" is fictitious (and tracking it is a big part of the value proposition in the simulation software). On the other hand, in usual parallel programming the goal is simply to do as much work as possible in the shortest (actual) time. In that context, one possible use of discrete event simulations is to cheaply model and optimize various parallel implementations of actual expensive algorithms (whether numerical computer algorithms or the algorithms used to schedule a real factory or a fleet of trucks).
14+
15+
In particular, the `ConcurrentSim.jl` package uses the async "coroutines" model of parallel programing. `ConcurrentSim` uses the `ResumableFunctions.jl` package to build its coroutines, which uses the `@resumable` macro to mark a function as an "async" coroutine and the `@yield` macro to yield between coroutines.
16+
17+
!!! warning "Base Julia coroutines vs ConcurrentSim coroutines"
18+
The `ConcurrentSim` and `ResumableFunctions` coroutines are currently incompatible with Julia's base coroutines (which based around `wait` and `fetch`). A separate coroutines implementation was necessary, because Julia's coroutines are designed for computationally heavy tasks and practical parallel algorithms, leading to significant overhead when they are used with extremely large numbers of computationally cheap tasks, as it is common in discrete event simulators. `ResumableFunctions`'s coroutines are single threaded but with drastically lower call overhead.
19+
A future long-term goal of ours is to unify the API used by `ResumableFunctions` and base Julia, but this will not be achieved in the near term, hence the need for pages like this one.
20+
21+
Without further ado, here is the typical API used with:
22+
23+
- `ConcurrentSim.Resource` which is used to represent scarce resource that can be used by only up to a fixed number of tasks. If the limit is just one task (the default), this is very similar to `Base.ReentrantLock`. `Resource` is a special case of `Container` with an integer "resource counter".
24+
- `ConcurrentSim.Store` which is used to represent a FILO stack.
25+
26+
```@raw html
27+
<div style="width:120%;min-width:120%;">
28+
```
29+
30+
||`Base` `ReentrantLock`|`Base` `Channel`|`ConcurrentSim` `Container`|`ConcurrentSim` `Resource`, i.e. `Container{Int}`|`ConcurrentSim` `Store`||
31+
|---|:---|:---|:---|:---|:---|:---:|
32+
|`put!`|||@yield|@yield|@yield|low-level "put an object in" API|
33+
|`take!`||block|||@yield|the `Channel`-like API for `Store`|
34+
|`lock`|block|||@yield||the `Lock`-like API for `Resource` (there is also `trylock`)|
35+
|`unlock`|✔️|||@yield||the `Lock`-like API for `Resource`|
36+
|`isready`||✔️|✔️|✔️|✔️|something is stored in the resource|
37+
|`islocked`|✔️||✔️|✔️|✔️|the resource can not store anything more|
38+
39+
```@raw html
40+
</div>
41+
```
42+
43+
The table denotes which methods exist (✔️), are blocking (block), need to be explicitly yielded with `ResumableFunctions` (@yield), or are not applicable (❌).
44+
45+
As you can see `Resource` shares some properties with `ReentrantLock` and avails itself of the `lock`/`unlock`/`trylock` Base API. `Store` similarly shares some properties with `Channel` and shares the `put!`/`take!` Base API. Of note is that when the Base API would be blocking, the corresponding `ConcurrentSim` methods actually give coroutines that need to be `@yield`-ed.
46+
47+
`take!` and `unlock` are both implemented on top of the lower level `get`.
48+
49+
The `Base.lock` and `Base.unlock` are aliased to `ConcurrentSim.request` and `ConcurrentSim.release` respectively for semantic convenience when working with `Resource`.
50+
51+
`unlock(::Resource)` is instantaneous so the `@yield` is not strictly necessary. Similarly for `put!(::Store)` if the store has infinite capacity.

src/ConcurrentSim.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module ConcurrentSim
1717
export @resumable, @yield
1818
export AbstractProcess, Simulation, run, now, active_process, StopSimulation
1919
export Process, @process, interrupt
20-
export Container, Resource, Store, put!, get, cancel, request, tryrequest
20+
export Container, Resource, Store, put!, get, cancel, request, tryrequest, release
2121
export nowDatetime
2222

2323
include("base.jl")
@@ -29,5 +29,5 @@ module ConcurrentSim
2929
include("resources/containers.jl")
3030
include("resources/stores.jl")
3131
include("utils/time.jl")
32-
include("deprecated.jl")
32+
include("deprecated_aliased.jl")
3333
end

src/deprecated.jl

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/deprecated_aliased.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Base.@deprecate put(args...; kwargs...) put!(args...; kwargs...)
2+
const request = lock
3+
const tryrequest = trylock
4+
const release = unlock

0 commit comments

Comments
 (0)