Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,3 @@
|
||||
## Description
|
||||
|
||||
`sync` provides cross platform handling of concurrency primitives.
|
||||
@@ -0,0 +1,38 @@
|
||||
fn test_shared_modification() {
|
||||
shared foo := &[2, 0, 5]
|
||||
lock foo {
|
||||
unsafe {
|
||||
foo[1] = 3
|
||||
foo[0] *= 7
|
||||
foo[1]--
|
||||
foo[2] -= 2
|
||||
}
|
||||
}
|
||||
rlock foo {
|
||||
unsafe {
|
||||
assert foo[0] == 14
|
||||
assert foo[1] == 2
|
||||
assert foo[2] == 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn test_shared_direct_modification() {
|
||||
shared foo := &[2, 0, 5]
|
||||
lock foo {
|
||||
unsafe {
|
||||
foo[1] = 3
|
||||
foo[0] *= 7
|
||||
foo[1]--
|
||||
foo[2] -= 2
|
||||
}
|
||||
}
|
||||
rlock foo {
|
||||
unsafe {
|
||||
assert foo[0] == 14
|
||||
assert foo[1] == 2
|
||||
assert foo[2] == 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import "log"
|
||||
import "os"
|
||||
import "time"
|
||||
import "strconv"
|
||||
|
||||
func assert_eq(a, b int64) {
|
||||
if a != b {
|
||||
log.Fatalf("assertion failed\nleft: %d, right: %d\n", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func do_rec(ch chan int32, resch chan int64, n int32) {
|
||||
var sum int64
|
||||
var i int32
|
||||
for i = 0; i < n; i++ {
|
||||
sum += int64(<- ch)
|
||||
}
|
||||
fmt.Println(sum)
|
||||
resch <- sum
|
||||
}
|
||||
|
||||
func do_send(ch chan int32, start, end int32) {
|
||||
for i := start; i < end; i++ {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 5 {
|
||||
log.Fatalf("usage:\n\t%s <nsend> <nrec> <buflen> <nobj>\n", os.Args[0])
|
||||
}
|
||||
nsend, _ := strconv.Atoi(os.Args[1])
|
||||
nrec, _ := strconv.Atoi(os.Args[2])
|
||||
buflen, _ := strconv.Atoi(os.Args[3])
|
||||
nobj, _ := strconv.Atoi(os.Args[4])
|
||||
stopwatch := time.Now()
|
||||
ch := make(chan int32, buflen)
|
||||
resch := make(chan int64, 0)
|
||||
no := nobj
|
||||
for i := 0; i < nrec; i++ {
|
||||
n := no / (nrec - i)
|
||||
go do_rec(ch, resch, int32(n))
|
||||
no -= n
|
||||
}
|
||||
assert_eq(int64(no), 0)
|
||||
no = nobj
|
||||
for i := 0; i < nsend; i++ {
|
||||
n := no / (nsend - i)
|
||||
end := no
|
||||
no -= n
|
||||
go do_send(ch, int32(no), int32(end))
|
||||
}
|
||||
assert_eq(int64(no), 0)
|
||||
var sum int64
|
||||
for i := 0; i < nrec; i++ {
|
||||
sum += <-resch
|
||||
}
|
||||
elapsed := time.Now().Sub(stopwatch)
|
||||
rate := float64(nobj)/float64(elapsed.Nanoseconds())*1000.0
|
||||
duration := 1.0e-09 * float64(elapsed.Nanoseconds())
|
||||
fmt.Printf("%d objects in %g s (%.2f objs/µs)\n", nobj, duration, rate)
|
||||
expected_sum := int64(nobj)*int64(nobj-1)/2
|
||||
fmt.Printf("got: %d, expected: %d\n", sum, expected_sum)
|
||||
assert_eq(sum, expected_sum)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Channel Benchmark
|
||||
//
|
||||
// `nobj` integers are sent thru a channel with queue length`buflen`
|
||||
// using `nsend` sender threads and `nrec` receiver threads.
|
||||
//
|
||||
// The receive threads add all received numbers and send them to the
|
||||
// main thread where the total sum is compare to the expected value.
|
||||
import time
|
||||
import os
|
||||
|
||||
fn do_rec(ch chan int, resch chan i64, n int) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. n {
|
||||
sum += <-ch
|
||||
}
|
||||
println(sum)
|
||||
resch <- sum
|
||||
}
|
||||
|
||||
fn do_send(ch chan int, start int, end int) {
|
||||
for i in start .. end {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if os.args.len != 5 {
|
||||
eprintln('usage:\n\t${os.args[0]} <nsend> <nrec> <buflen> <nobj>')
|
||||
exit(1)
|
||||
}
|
||||
nsend := os.args[1].int()
|
||||
nrec := os.args[2].int()
|
||||
buflen := os.args[3].int()
|
||||
nobj := os.args[4].int()
|
||||
stopwatch := time.new_stopwatch()
|
||||
ch := chan int{cap: buflen}
|
||||
resch := chan i64{}
|
||||
mut no := nobj
|
||||
for i in 0 .. nrec {
|
||||
n := no / (nrec - i)
|
||||
spawn do_rec(ch, resch, n)
|
||||
no -= n
|
||||
}
|
||||
$if debug {
|
||||
assert no == 0
|
||||
}
|
||||
no = nobj
|
||||
for i in 0 .. nsend {
|
||||
n := no / (nsend - i)
|
||||
end := no
|
||||
no -= n
|
||||
spawn do_send(ch, no, end)
|
||||
}
|
||||
assert no == 0
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. nrec {
|
||||
sum += <-resch
|
||||
}
|
||||
elapsed := stopwatch.elapsed()
|
||||
rate := f64(nobj) / elapsed * time.microsecond
|
||||
println('${nobj} objects in ${f64(elapsed) / time.second} s (${rate:.2f} objs/µs)')
|
||||
// use sum formula by Gauß to calculate the expected result
|
||||
expected_sum := i64(nobj) * (nobj - 1) / 2
|
||||
println('got: ${sum}, expected: ${expected_sum}')
|
||||
assert sum == expected_sum
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import os
|
||||
import os.cmdline
|
||||
import time
|
||||
import sync
|
||||
|
||||
// Usage:
|
||||
// many_writers_and_receivers_on_1_channel [-readers 1] [-writers 4] [-chan_cap 100] [-iterations 25000] > results.csv
|
||||
//
|
||||
// You can then open results.csv in Excel/Calc and for example plot the first vs the second column.
|
||||
enum EventKind {
|
||||
push
|
||||
pop
|
||||
}
|
||||
|
||||
struct Event {
|
||||
is_set bool
|
||||
id int
|
||||
gtime u64 // nanoseconds
|
||||
i int
|
||||
kind EventKind
|
||||
elapsed i64 // nanoseconds, elapsed after the previous event of the same kind
|
||||
}
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
n_iters int
|
||||
n_readers int
|
||||
n_writers int
|
||||
|
||||
pops_wg &sync.WaitGroup
|
||||
pops []Event
|
||||
|
||||
pushes_wg &sync.WaitGroup
|
||||
pushes []Event
|
||||
}
|
||||
|
||||
fn do_rec(ch chan int, id int, mut ctx Context) {
|
||||
eprintln('start of do_rec id: ${id}')
|
||||
mut timer_sw_x := time.new_stopwatch()
|
||||
mut tmp := int(0)
|
||||
mut i := int(0)
|
||||
// Note: a single receiver thread can get slightly more
|
||||
// than its fair share of sends, that is why
|
||||
// the receiver's Event array is much larger,
|
||||
// enough so a single receiver can potentially process all
|
||||
// writers pushes, and it is partitioned over all of
|
||||
// id, ctx.n_writers and n_iters:
|
||||
n_iters := ctx.n_iters
|
||||
base := id * n_iters * ctx.n_writers
|
||||
for {
|
||||
for ch.try_pop(mut tmp) == .success {
|
||||
ctx.pops[base + i] = Event{
|
||||
is_set: true
|
||||
id: id
|
||||
gtime: time.sys_mono_now()
|
||||
i: i
|
||||
kind: .pop
|
||||
elapsed: timer_sw_x.elapsed().nanoseconds()
|
||||
}
|
||||
timer_sw_x.restart()
|
||||
i++
|
||||
if tmp == 1 {
|
||||
ctx.pops_wg.done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send(ch chan int, id int, mut ctx Context) {
|
||||
eprintln('start of do_send id: ${id}')
|
||||
mut timer_sw_x := time.new_stopwatch()
|
||||
n_iters := ctx.n_iters
|
||||
base := n_iters * id // sender events can not overlap
|
||||
for i := 0; i < n_iters; i++ {
|
||||
idx := base + i
|
||||
ctx.pushes[idx] = Event{
|
||||
is_set: true
|
||||
id: id
|
||||
gtime: time.sys_mono_now()
|
||||
i: i
|
||||
kind: .push
|
||||
elapsed: timer_sw_x.elapsed().nanoseconds()
|
||||
}
|
||||
timer_sw_x.restart()
|
||||
tmp := int(0)
|
||||
ch <- tmp
|
||||
}
|
||||
ctx.pushes_wg.done()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
args := os.args[1..]
|
||||
if '-h' in args || '--help' in args {
|
||||
eprintln('Usage:\n many_writers_and_receivers_on_1_channel [-readers 1] [-writers 4] [-chan_cap 100] [-iterations 25000]')
|
||||
exit(0)
|
||||
}
|
||||
n_iters := cmdline.option(args, '-iterations', '25000').int()
|
||||
n_readers := cmdline.option(args, '-readers', '1').int()
|
||||
n_writers := cmdline.option(args, '-writers', '4').int()
|
||||
chan_cap := cmdline.option(args, '-chan_cap', '100').int()
|
||||
eprintln('> n_iters, ${n_iters}, n_writers, ${n_writers}, n_readers, ${n_readers}, chan_cap, ${chan_cap}')
|
||||
|
||||
ch := chan int{cap: chan_cap}
|
||||
max_number_of_pushes := n_writers * (n_iters + 2)
|
||||
max_number_of_pops := max_number_of_pushes * n_readers
|
||||
eprintln('> max_number_of_pushes, ${max_number_of_pushes}, max_number_of_pops (per receiver), ${max_number_of_pops}')
|
||||
mut ctx := &Context{
|
||||
n_iters: n_iters
|
||||
n_readers: n_readers
|
||||
n_writers: n_writers
|
||||
pushes_wg: sync.new_waitgroup()
|
||||
pops_wg: sync.new_waitgroup()
|
||||
pushes: []Event{len: max_number_of_pushes}
|
||||
pops: []Event{len: max_number_of_pops}
|
||||
}
|
||||
ctx.pops_wg.add(n_readers)
|
||||
for i := 0; i < n_readers; i++ {
|
||||
spawn do_rec(ch, i, mut ctx)
|
||||
}
|
||||
ctx.pushes_wg.add(n_writers)
|
||||
for i := 0; i < n_writers; i++ {
|
||||
spawn do_send(ch, i, mut ctx)
|
||||
}
|
||||
ctx.pushes_wg.wait()
|
||||
eprintln('>> all pushes done')
|
||||
for i := 0; i < n_readers; i++ {
|
||||
ch <- 1
|
||||
}
|
||||
ctx.pops_wg.wait()
|
||||
eprintln('>> all pops done')
|
||||
mut all_events := []Event{}
|
||||
all_events << ctx.pops
|
||||
all_events << ctx.pushes
|
||||
all_events.sort(a.elapsed < b.elapsed)
|
||||
mut i := 0
|
||||
for e in all_events {
|
||||
if !e.is_set {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if e.kind == .pop {
|
||||
println('${i:8} , ${e.elapsed:10}, ns , do_rec id:, ${e.id:3} , i=, ${e.i:5} , ${e.gtime:20}')
|
||||
}
|
||||
if e.kind == .push {
|
||||
println('${i:8} , ${e.elapsed:10}, ns , do_send id:, ${e.id:3} , i=, ${e.i:5} , ${e.gtime:20}')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Channel Benchmark Results
|
||||
|
||||
This documents lists several benchmark results for different platforms in order to
|
||||
identify performance regressions and improvements.
|
||||
|
||||
The are measured using the command
|
||||
|
||||
```
|
||||
> channel_bench_* <nsend> <nrec> <buflen> <nobj>
|
||||
|
||||
nsend ... number of threads that push objects into the channel
|
||||
nrec .... number of threads that pop objects from the channel
|
||||
buflen .. length of channel buffer queue - `0` means unbuffered channel
|
||||
nobj .... number of objects to pass thru the channel
|
||||
```
|
||||
|
||||
## AMD Ryzen 7 3800X, Ubuntu-20.04 x86_64
|
||||
|
||||
10000000 Objects transfered, results in Objects/µs
|
||||
|
||||
| nsend | nrec | buflen | **V (gcc -O2)** | **V (clang)** | **V (tcc)** | **Go (golang)** | **Go (gccgo -O2)** |
|
||||
| :---: | :---:| :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| 1 | 1 | 0 | 1.97 | 1.63 | 2.08 | 4.65 | 0.56 |
|
||||
| 1 | 1 | 100 | 3.05 | 2.29 | 1.93 | 18.90 | 6.08 |
|
||||
| 4 | 4 | 0 | 0.87 | 0.90 | 0.99 | 1.84 | 0.84 |
|
||||
| 4 | 4 | 100 | 3.35 | 3.07 | 2.92 | 7.43 | 3.71 |
|
||||
|
||||
## AMD Ryzen 7 3800X, Windows 10 2004 x64
|
||||
|
||||
| nsend | nrec | buflen | **V (gcc -O2)** | **V (msvc /O2)** | **V (tcc)** | **Go (golang)** |
|
||||
| :---: | :---:| :---: | :---: | :---: | :---: | :---: |
|
||||
| 1 | 1 | 0 | 2.30 | 3.76 | 2.02 | 4.67 |
|
||||
| 1 | 1 | 100 | 2.96 | 3.12 | 2.26 | 23.31 |
|
||||
| 4 | 4 | 0 | 0.90 | 1.05 | 0.83 | 1.38 |
|
||||
| 4 | 4 | 100 | 2.28 | 2.16 | 2.43 | 4.63 |
|
||||
|
||||
## Raspberry Pi 3B+, Void Linux musl 32 bit
|
||||
|
||||
10000000 Objects transfered, results in Objects/µs
|
||||
|
||||
| nsend | nrec | buflen | **V (gcc -O2)** | **Go (golang)** |
|
||||
| :---: | :---:| :---: | :---: | :---: |
|
||||
| 1 | 1 | 0 | 0.37 | 0.21 |
|
||||
| 1 | 1 | 100 | 1.03 | 0.74 |
|
||||
| 4 | 4 | 0 | 0.04 | 0.38 |
|
||||
| 4 | 4 | 100 | 2.78 | 2.63 |
|
||||
| 2 | 2 | 0 | 0.05 | 0.38 |
|
||||
| 2 | 2 | 100 | 1.26 | 0.75 |
|
||||
@@ -0,0 +1,89 @@
|
||||
// Usage: v run run_bench.v
|
||||
// It will generate a `result.md` in current dir.
|
||||
import os
|
||||
import arrays
|
||||
import strings
|
||||
|
||||
const compilers = [
|
||||
'tcc',
|
||||
'clang',
|
||||
'gcc',
|
||||
]
|
||||
|
||||
const run_iterations = 10
|
||||
|
||||
const nobj = 10000000
|
||||
const run_settings = [
|
||||
[1, 1, 0],
|
||||
[1, 1, 100],
|
||||
[4, 4, 0],
|
||||
[4, 4, 100],
|
||||
]
|
||||
|
||||
fn get_perf_from_result(result string) !f32 {
|
||||
lines := result.split_into_lines()
|
||||
for l in lines {
|
||||
if l.contains('objects') && l.contains('(') && l.contains(')') {
|
||||
f := l.find_between('(', ')').all_before('objs/µs').trim_space().f32()
|
||||
return f
|
||||
}
|
||||
}
|
||||
return error('run fail?')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut perf_result := []f32{}
|
||||
|
||||
for cc in compilers {
|
||||
// 1. compile
|
||||
compile_cmd := 'v channel_bench_v.v -cc ${cc}'
|
||||
println('compile_cmd: ${compile_cmd}')
|
||||
compile_result := os.execute(compile_cmd)
|
||||
if compile_result.exit_code != 0 {
|
||||
panic('compile fail with "${compile_cmd}"')
|
||||
}
|
||||
|
||||
// 2. run
|
||||
for s in run_settings {
|
||||
run_cmd := './channel_bench_v ${s[0]:-3} ${s[1]:-3} ${s[2]:-3} ${nobj}'
|
||||
println('-----------------------------------------------------------')
|
||||
mut iteration_result := []f32{}
|
||||
for i in 0 .. run_iterations {
|
||||
print('${i:3}: ${run_cmd}')
|
||||
run_result := os.execute(run_cmd)
|
||||
f := get_perf_from_result(run_result.output)!
|
||||
iteration_result << f
|
||||
println(' => ${f:.2} objs/µs')
|
||||
}
|
||||
avg := arrays.sum(iteration_result)! / run_iterations
|
||||
perf_result << avg
|
||||
}
|
||||
}
|
||||
|
||||
// 3. output result
|
||||
mut sb := strings.new_builder(8192)
|
||||
sb.write_string('\n| nsend | nrec | buflen |')
|
||||
for cc in compilers {
|
||||
sb.write_string(' **V (${cc:-5})** |')
|
||||
}
|
||||
sb.writeln('')
|
||||
sb.write_string('| :---: | :---:| :---: |')
|
||||
for _ in 0 .. compilers.len {
|
||||
sb.write_string(' :---: |')
|
||||
}
|
||||
sb.writeln('')
|
||||
for i, s in run_settings {
|
||||
sb.write_string('| ${s[0]:-3} | ${s[1]:-3} | ${s[2]:-3} |')
|
||||
for j in 0 .. compilers.len {
|
||||
sb.write_string(' ${perf_result[j * run_settings.len + i]:-5.2} |')
|
||||
}
|
||||
sb.writeln('')
|
||||
}
|
||||
sb.writeln('')
|
||||
println('***********************************************************')
|
||||
println('writing result to `result.md`...')
|
||||
println('***********************************************************')
|
||||
os.write_file('result.md', sb.str())!
|
||||
println(sb.str())
|
||||
os.rm('./channel_bench_v')!
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module sync
|
||||
|
||||
struct ChanStruct {
|
||||
workers []chan bool
|
||||
}
|
||||
|
||||
fn test_chan_mod_sync() {
|
||||
_ := chan bool{cap: 1}
|
||||
assert true
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const num_iterations = 10000
|
||||
|
||||
fn do_send(ch chan int) {
|
||||
for i in 0 .. num_iterations {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_channel_buffered() {
|
||||
ch := chan int{cap: 1000}
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. num_iterations {
|
||||
sum += <-ch
|
||||
}
|
||||
assert sum == u64(num_iterations) * (num_iterations - 1) / 2
|
||||
}
|
||||
|
||||
fn test_builtin_enum() {
|
||||
x := ChanState.closed
|
||||
assert x == .closed
|
||||
println(x)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const num_iterations = 10000
|
||||
|
||||
fn do_send(ch chan int) {
|
||||
for i in 0 .. num_iterations {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_channel_unbuffered() {
|
||||
ch := chan int{}
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. num_iterations {
|
||||
sum += <-ch
|
||||
}
|
||||
assert sum == u64(num_iterations) * (num_iterations - 1) / 2
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
fn do_rec(ch chan int, resch chan i64) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 2000 {
|
||||
sum += <-ch
|
||||
}
|
||||
println(sum)
|
||||
resch <- sum
|
||||
}
|
||||
|
||||
fn do_send(ch chan int) {
|
||||
for i in 0 .. 2000 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_channel_multi_unbuffered() {
|
||||
ch := chan int{}
|
||||
resch := chan i64{}
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_send(ch)
|
||||
spawn do_send(ch)
|
||||
spawn do_send(ch)
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 4 {
|
||||
sum += <-resch
|
||||
}
|
||||
assert sum == i64(4) * 2000 * (2000 - 1) / 2
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
fn do_rec(ch chan int, resch chan i64) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 2000 {
|
||||
sum += <-ch
|
||||
}
|
||||
println(sum)
|
||||
resch <- sum
|
||||
}
|
||||
|
||||
fn do_send(ch chan int) {
|
||||
for i in 0 .. 2000 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_channel_multi_buffered() {
|
||||
ch := chan int{cap: 100}
|
||||
resch := chan i64{}
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_send(ch)
|
||||
spawn do_send(ch)
|
||||
spawn do_send(ch)
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 4 {
|
||||
sum += <-resch
|
||||
}
|
||||
assert sum == i64(4) * 2000 * (2000 - 1) / 2
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
const num_iterations = 10000
|
||||
|
||||
struct St {
|
||||
mut:
|
||||
dummy i64
|
||||
dummy2 u32
|
||||
dummy3 i64
|
||||
n int
|
||||
dummy4 int
|
||||
}
|
||||
|
||||
// this function gets an array of channels for `St` references
|
||||
fn do_rec_calc_send(chs []chan mut St) {
|
||||
for {
|
||||
mut s := <-chs[0] or { break }
|
||||
s.n++
|
||||
chs[1] <- s
|
||||
}
|
||||
}
|
||||
|
||||
fn test_channel_array_mut() {
|
||||
mut chs := [chan mut St{cap: 1}, chan mut St{}]
|
||||
spawn do_rec_calc_send(chs)
|
||||
mut t := &St{
|
||||
n: 100
|
||||
}
|
||||
for _ in 0 .. num_iterations {
|
||||
chs[0] <- t
|
||||
t = <-chs[1]
|
||||
}
|
||||
chs[0].close()
|
||||
assert t.n == 100 + num_iterations
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import time
|
||||
|
||||
fn do_rec(ch chan int, resch chan i64) {
|
||||
mut sum := i64(0)
|
||||
for {
|
||||
a := <-ch or { break }
|
||||
sum += a
|
||||
}
|
||||
assert ch.closed == true
|
||||
println(sum)
|
||||
resch <- sum
|
||||
}
|
||||
|
||||
fn do_send(ch chan int) {
|
||||
for i in 0 .. 8000 {
|
||||
ch <- i
|
||||
}
|
||||
assert ch.closed == false
|
||||
ch.close()
|
||||
assert ch.closed == true
|
||||
}
|
||||
|
||||
fn test_channel_close_buffered_multi() {
|
||||
ch := chan int{cap: 10}
|
||||
resch := chan i64{}
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 4 {
|
||||
sum += <-resch
|
||||
}
|
||||
assert sum == i64(8000) * (8000 - 1) / 2
|
||||
}
|
||||
|
||||
fn test_channel_close_unbuffered_multi() {
|
||||
ch := chan int{}
|
||||
resch := chan i64{}
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 4 {
|
||||
sum += <-resch
|
||||
}
|
||||
assert sum == i64(8000) * (8000 - 1) / 2
|
||||
}
|
||||
|
||||
fn test_channel_close_buffered() {
|
||||
ch := chan int{cap: 100}
|
||||
resch := chan i64{}
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
sum += <-resch
|
||||
assert sum == i64(8000) * (8000 - 1) / 2
|
||||
}
|
||||
|
||||
fn test_channel_close_unbuffered() {
|
||||
ch := chan int{}
|
||||
resch := chan i64{cap: 100}
|
||||
spawn do_rec(ch, resch)
|
||||
spawn do_send(ch)
|
||||
mut sum := i64(0)
|
||||
sum += <-resch
|
||||
assert sum == i64(8000) * (8000 - 1) / 2
|
||||
}
|
||||
|
||||
fn test_channel_send_close_buffered() {
|
||||
ch := chan int{cap: 1}
|
||||
t := spawn fn (ch chan int) {
|
||||
ch <- 31
|
||||
mut x := 45
|
||||
ch <- 17 or { x = -133 }
|
||||
|
||||
assert x == -133
|
||||
}(ch)
|
||||
time.sleep(100 * time.millisecond)
|
||||
ch.close()
|
||||
mut r := <-ch
|
||||
r = <-ch or { 23 }
|
||||
assert r == 23
|
||||
t.wait()
|
||||
}
|
||||
|
||||
fn test_channel_send_close_unbuffered() {
|
||||
time.sleep(1 * time.second)
|
||||
ch := chan int{}
|
||||
t := spawn fn (ch chan int) {
|
||||
mut x := 31
|
||||
ch <- 177 or { x = -71 }
|
||||
|
||||
assert x == -71
|
||||
}(ch)
|
||||
time.sleep(100 * time.millisecond)
|
||||
ch.close()
|
||||
r := <-ch or { 238 }
|
||||
assert r == 238
|
||||
t.wait()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import sync
|
||||
|
||||
const queue_len = 1000
|
||||
const queue_fill = 763
|
||||
|
||||
fn do_send(ch chan int, mut fin sync.Semaphore) {
|
||||
for i in 0 .. queue_fill {
|
||||
ch <- i
|
||||
}
|
||||
fin.post()
|
||||
}
|
||||
|
||||
fn test_channel_len_cap() {
|
||||
ch := chan int{cap: queue_len}
|
||||
mut sem := sync.new_semaphore()
|
||||
spawn do_send(ch, mut sem)
|
||||
sem.wait()
|
||||
assert ch.cap == queue_len
|
||||
assert ch.len == queue_fill
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import sync
|
||||
|
||||
const num_iterations = 10000
|
||||
|
||||
fn get_val_from_chan(ch chan i64) ?i64 {
|
||||
r := <-ch?
|
||||
return r
|
||||
}
|
||||
|
||||
// this function gets an array of channels for `i64`
|
||||
fn do_rec_calc_send(chs []chan i64, mut sem sync.Semaphore) {
|
||||
mut msg := ''
|
||||
for {
|
||||
mut s := get_val_from_chan(chs[0]) or {
|
||||
msg = err.msg()
|
||||
break
|
||||
}
|
||||
s++
|
||||
chs[1] <- s
|
||||
}
|
||||
assert msg == 'channel closed'
|
||||
sem.post()
|
||||
}
|
||||
|
||||
fn test_channel_array_mut() {
|
||||
mut chs := [chan i64{}, chan i64{cap: 10}]
|
||||
mut sem := sync.new_semaphore()
|
||||
spawn do_rec_calc_send(chs, mut sem)
|
||||
mut t := i64(100)
|
||||
for _ in 0 .. num_iterations {
|
||||
chs[0] <- t
|
||||
t = <-chs[1]
|
||||
}
|
||||
chs[0].close()
|
||||
sem.wait()
|
||||
assert t == 100 + num_iterations
|
||||
}
|
||||
|
||||
fn test_channel_close_with_error_propagates_after_buffer_drain() {
|
||||
ch := chan i64{cap: 1}
|
||||
ch <- i64(7)
|
||||
ch.close(error('async failure'))
|
||||
assert <-ch == i64(7)
|
||||
_ := get_val_from_chan(ch) or {
|
||||
assert err.msg() == 'async failure'
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Channel Benchmark
|
||||
//
|
||||
// `nobj` integers are sent thru a channel with queue length`buflen`
|
||||
// using `nsend` sender threads and `nrec` receiver threads.
|
||||
//
|
||||
// The receive threads add all received numbers and send them to the
|
||||
// main thread where the total sum is compare to the expected value.
|
||||
const nsend = 2
|
||||
const nrec = 2
|
||||
const buflen = 100
|
||||
const nobj = 10000
|
||||
const objs_per_thread = 5000
|
||||
|
||||
fn do_rec(ch chan int, resch chan i64, n int) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. n {
|
||||
mut r := 0
|
||||
for ch.try_pop(mut r) != .success {
|
||||
}
|
||||
sum += r
|
||||
}
|
||||
println(sum)
|
||||
resch <- sum
|
||||
}
|
||||
|
||||
fn do_send(ch chan int, start int, end int) {
|
||||
for i in start .. end {
|
||||
for ch.try_push(i) != .success {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_channel_polling() {
|
||||
ch := chan int{cap: buflen}
|
||||
resch := chan i64{}
|
||||
for _ in 0 .. nrec {
|
||||
spawn do_rec(ch, resch, objs_per_thread)
|
||||
}
|
||||
mut n := nobj
|
||||
for _ in 0 .. nsend {
|
||||
end := n
|
||||
n -= objs_per_thread
|
||||
spawn do_send(ch, n, end)
|
||||
}
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. nrec {
|
||||
sum += <-resch
|
||||
println('> running sum: ${sum}')
|
||||
}
|
||||
// use sum formula by Gauß to calculate the expected result
|
||||
expected_sum := i64(nobj) * (nobj - 1) / 2
|
||||
println('expected sum: ${expected_sum} | sum: ${sum}')
|
||||
assert sum == expected_sum
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
const n = 1000
|
||||
|
||||
const c = 100
|
||||
|
||||
fn f(ch chan int) {
|
||||
for _ in 0 .. n {
|
||||
_ := <-ch
|
||||
}
|
||||
ch.close()
|
||||
}
|
||||
|
||||
fn test_push_or_unbuffered() {
|
||||
ch := chan int{}
|
||||
spawn f(ch)
|
||||
mut j := 0
|
||||
for {
|
||||
ch <- j or { break }
|
||||
|
||||
j++
|
||||
}
|
||||
assert j == n
|
||||
}
|
||||
|
||||
fn test_push_or_buffered() {
|
||||
ch := chan int{cap: c}
|
||||
spawn f(ch)
|
||||
mut j := 0
|
||||
for {
|
||||
ch <- j or { break }
|
||||
|
||||
j++
|
||||
}
|
||||
// we don't know how many elements are in the buffer when the channel
|
||||
// is closed, so check j against an interval
|
||||
assert j >= n
|
||||
assert j <= n + c
|
||||
}
|
||||
|
||||
fn g(ch chan int, res chan int) {
|
||||
mut j := 0
|
||||
for {
|
||||
ch <- j or { break }
|
||||
|
||||
j++
|
||||
}
|
||||
println('done ${j}')
|
||||
res <- j
|
||||
}
|
||||
|
||||
fn test_many_senders() {
|
||||
ch := chan int{}
|
||||
res := chan int{}
|
||||
spawn g(ch, res)
|
||||
spawn g(ch, res)
|
||||
spawn g(ch, res)
|
||||
mut k := 0
|
||||
for _ in 0 .. 3 * n {
|
||||
k = <-ch
|
||||
}
|
||||
ch.close()
|
||||
mut sum := <-res
|
||||
sum += <-res
|
||||
sum += <-res
|
||||
assert sum == 3 * n
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const n = 1000
|
||||
|
||||
fn f(ch chan f64) {
|
||||
mut s := 0.0
|
||||
for _ in 0 .. n {
|
||||
s += <-ch
|
||||
}
|
||||
assert s == f64(n * (n + 1) / 2)
|
||||
ch.close()
|
||||
}
|
||||
|
||||
fn do_send(ch chan f64, val f64) ?f64 {
|
||||
ch <- val?
|
||||
return val + 1.0
|
||||
}
|
||||
|
||||
fn test_push_propargate() {
|
||||
ch := chan f64{}
|
||||
spawn f(ch)
|
||||
mut s := 1.0
|
||||
for {
|
||||
s = do_send(ch, s) or { break }
|
||||
}
|
||||
assert s == f64(n + 1)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import time
|
||||
|
||||
fn do_rec_i64(ch chan i64) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 300 {
|
||||
sum += <-ch
|
||||
}
|
||||
assert sum == 300 * (300 - 1) / 2
|
||||
}
|
||||
|
||||
fn do_send_int(ch chan int) {
|
||||
for i in 0 .. 300 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send_u8(ch chan u8) {
|
||||
for i in 0 .. 300 {
|
||||
ch <- u8(i)
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send_i64(ch chan i64) {
|
||||
for i in 0 .. 300 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_select() {
|
||||
chi := chan int{}
|
||||
chl := chan i64{cap: 1}
|
||||
chb := chan u8{cap: 10}
|
||||
recch := chan i64{cap: 0}
|
||||
spawn do_rec_i64(recch)
|
||||
spawn do_send_int(chi)
|
||||
spawn do_send_u8(chb)
|
||||
spawn do_send_i64(chl)
|
||||
mut sum := i64(0)
|
||||
mut rl := i64(0)
|
||||
mut sl := i64(0)
|
||||
for _ in 0 .. 1200 {
|
||||
select {
|
||||
ri := <-chi {
|
||||
sum += ri
|
||||
}
|
||||
recch <- sl {
|
||||
sl++
|
||||
}
|
||||
rl = <-chl {
|
||||
sum += rl
|
||||
}
|
||||
rb := <-chb {
|
||||
sum += rb
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use Gauß' formula for the first 2 contributions
|
||||
// the 3rd contribution is `byte` and must be seen modulo 256
|
||||
expected_sum := 2 * (300 * (300 - 1) / 2) + 256 * (256 - 1) / 2 + 44 * (44 - 1) / 2
|
||||
assert sum == expected_sum
|
||||
time.sleep(20 * time.millisecond) // to give assert in coroutine enough time
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import time
|
||||
import sync
|
||||
|
||||
struct St {
|
||||
a int
|
||||
}
|
||||
|
||||
fn getint() int {
|
||||
return 8
|
||||
}
|
||||
|
||||
fn f1(ch1 chan int, ch2 chan St, ch3 chan int, ch4 chan int, ch5 chan int, mut sem sync.Semaphore) {
|
||||
mut a := 5
|
||||
select {
|
||||
a = <-ch3 {
|
||||
a = 0
|
||||
}
|
||||
b := <-ch2 {
|
||||
a = b.a
|
||||
}
|
||||
ch3 <- 5 {
|
||||
a = 1
|
||||
}
|
||||
ch2 <- St{
|
||||
a: 37
|
||||
} {
|
||||
a = 2
|
||||
}
|
||||
ch4 <- (6 + 7 * 9) {
|
||||
a = 8
|
||||
}
|
||||
ch5 <- getint() {
|
||||
a = 9
|
||||
}
|
||||
300 * time.millisecond {
|
||||
a = 3
|
||||
}
|
||||
}
|
||||
assert a == 3
|
||||
sem.post()
|
||||
}
|
||||
|
||||
fn f2(ch1 chan St, ch2 chan int, mut sem sync.Semaphore) {
|
||||
mut r := 23
|
||||
for i in 0 .. 2 {
|
||||
select {
|
||||
b := <-ch1 {
|
||||
r = b.a
|
||||
}
|
||||
ch2 <- r {
|
||||
r = 17
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
assert r == 17
|
||||
} else {
|
||||
assert r == 13
|
||||
}
|
||||
}
|
||||
sem.post()
|
||||
}
|
||||
|
||||
fn test_select_blocks() {
|
||||
ch1 := chan int{cap: 1}
|
||||
ch2 := chan St{}
|
||||
ch3 := chan int{}
|
||||
ch4 := chan int{}
|
||||
ch5 := chan int{}
|
||||
mut sem := sync.new_semaphore()
|
||||
mut r := false
|
||||
t := select {
|
||||
b := <-ch1 {
|
||||
println(b)
|
||||
}
|
||||
else {
|
||||
// no channel ready
|
||||
r = true
|
||||
}
|
||||
}
|
||||
assert r == true
|
||||
assert t == true
|
||||
spawn f2(ch2, ch3, mut sem)
|
||||
n := <-ch3
|
||||
assert n == 23
|
||||
ch2 <- St{
|
||||
a: 13
|
||||
}
|
||||
sem.wait()
|
||||
stopwatch := time.new_stopwatch()
|
||||
spawn f1(ch1, ch2, ch3, ch4, ch5, mut sem)
|
||||
sem.wait()
|
||||
elapsed_ms := f64(stopwatch.elapsed()) / time.millisecond
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/high-resolution-timers
|
||||
// > For example, for Windows running on an x86 processor, the default interval between
|
||||
// > system clock ticks is typically about 15 milliseconds, and the minimum interval
|
||||
// > between system clock ticks is about 1 millisecond.
|
||||
assert elapsed_ms >= 280.0 // 300 - (15ms + 5ms just in case)
|
||||
|
||||
ch1.close()
|
||||
ch2.close()
|
||||
mut h := 7
|
||||
mut is_open := true
|
||||
if select {
|
||||
_ := <-ch2 {
|
||||
h = 0
|
||||
}
|
||||
ch1 <- h {
|
||||
h = 1
|
||||
}
|
||||
else {
|
||||
h = 2
|
||||
}
|
||||
} {
|
||||
panic('channel is still open')
|
||||
} else {
|
||||
is_open = false
|
||||
}
|
||||
// no branch should have run
|
||||
assert h == 7
|
||||
// since all channels are closed `select` should return `false`
|
||||
assert is_open == false
|
||||
}
|
||||
|
||||
fn test_select_closed_receive_beats_timeout() {
|
||||
ch := chan bool{}
|
||||
ch.close()
|
||||
mut got := true
|
||||
mut timed_out := false
|
||||
select {
|
||||
got = <-ch {
|
||||
assert got == false
|
||||
}
|
||||
10 * time.millisecond {
|
||||
timed_out = true
|
||||
}
|
||||
}
|
||||
assert got == false
|
||||
assert timed_out == false
|
||||
}
|
||||
|
||||
fn test_select_else_skips_closed_buffered_receive() {
|
||||
ch := chan int{cap: 1}
|
||||
ch <- 1
|
||||
ch.close()
|
||||
mut received := []int{}
|
||||
mut did_else := false
|
||||
select {
|
||||
value := <-ch {
|
||||
received << value
|
||||
}
|
||||
else {
|
||||
did_else = true
|
||||
}
|
||||
}
|
||||
assert did_else
|
||||
assert received == []
|
||||
assert ch.len == 1
|
||||
assert <-ch == 1
|
||||
}
|
||||
|
||||
fn test_select_expr_false_on_closed_buffered_receive() {
|
||||
ch := chan int{cap: 1}
|
||||
ch <- 1
|
||||
ch.close()
|
||||
mut ran_branch := false
|
||||
mut ran_else := false
|
||||
if select {
|
||||
_ := <-ch {
|
||||
ran_branch = true
|
||||
}
|
||||
else {
|
||||
ran_else = true
|
||||
}
|
||||
} {
|
||||
assert false
|
||||
}
|
||||
assert ran_branch == false
|
||||
assert ran_else == false
|
||||
assert ch.len == 1
|
||||
assert <-ch == 1
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
fn do_rec_i64(ch chan i64, sumch chan i64) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 30000 {
|
||||
sum += <-ch
|
||||
}
|
||||
sumch <- sum
|
||||
}
|
||||
|
||||
fn do_send_int(ch chan int) {
|
||||
for i in 0 .. 30000 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_select() {
|
||||
chi := chan int{cap: 10}
|
||||
recch := chan i64{cap: 10}
|
||||
chsum := chan i64{}
|
||||
spawn do_rec_i64(recch, chsum)
|
||||
spawn do_send_int(chi)
|
||||
mut sum := i64(0)
|
||||
mut sl := i64(0)
|
||||
for _ in 0 .. 60000 + recch.cap {
|
||||
select {
|
||||
ri := <-chi {
|
||||
sum += ri
|
||||
}
|
||||
recch <- sl {
|
||||
sl++
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use Gauß' formula
|
||||
expected_sum := i64(30000) * (30000 - 1) / 2
|
||||
assert sum == expected_sum
|
||||
|
||||
mut sumrec := <-chsum
|
||||
// Empty receive buffer
|
||||
for _ in 0 .. recch.cap {
|
||||
sumrec += <-recch
|
||||
}
|
||||
assert sumrec == i64(30000 + recch.cap) * (30000 + recch.cap - 1) / 2
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
fn do_rec_i64(ch chan i64, sumch chan i64) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 10000 {
|
||||
sum += <-ch
|
||||
}
|
||||
sumch <- sum
|
||||
}
|
||||
|
||||
fn do_send_int(ch chan int) {
|
||||
for i in 0 .. 10000 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send_int2(ch chan int) {
|
||||
for i in 10000 .. 20000 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send_int3(ch chan int) {
|
||||
for i in 20000 .. 30000 {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
fn test_select() {
|
||||
chi := chan int{cap: 10}
|
||||
recch := chan i64{cap: 10}
|
||||
chsum := chan i64{}
|
||||
spawn do_rec_i64(recch, chsum)
|
||||
spawn do_rec_i64(recch, chsum)
|
||||
spawn do_rec_i64(recch, chsum)
|
||||
spawn do_send_int(chi)
|
||||
spawn do_send_int2(chi)
|
||||
spawn do_send_int3(chi)
|
||||
mut sum := i64(0)
|
||||
mut sl := i64(0)
|
||||
for _ in 0 .. 60000 + recch.cap {
|
||||
select {
|
||||
ri := <-chi {
|
||||
sum += ri
|
||||
}
|
||||
recch <- sl {
|
||||
sl++
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use Gauß' formula
|
||||
expected_sum := i64(30000) * (30000 - 1) / 2
|
||||
assert sum == expected_sum
|
||||
|
||||
mut sumrec := <-chsum
|
||||
sumrec += <-chsum
|
||||
sumrec += <-chsum
|
||||
// Empty receive buffer
|
||||
for _ in 0 .. recch.cap {
|
||||
sumrec += <-recch
|
||||
}
|
||||
assert sumrec == i64(30000 + recch.cap) * (30000 + recch.cap - 1) / 2
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// vtest retry: 5
|
||||
|
||||
// This test case runs concurrent 3 instances of `do_select` that
|
||||
// communicate with 6 other threads doing send and receive operations.
|
||||
// There are buffered and unbuffered channels - handled by one or two
|
||||
// concurrend threads on the other side
|
||||
|
||||
fn do_select(ch1 chan int, ch2 chan int, chf1 chan f64, chf2 chan f64, sumch1 chan i64, sumch2 chan i64) {
|
||||
mut sum1 := i64(0)
|
||||
mut sum2 := i64(0)
|
||||
f1 := 17.0
|
||||
f2 := 7.0
|
||||
for _ in 0 .. 20000 + chf1.cap / 3 {
|
||||
select {
|
||||
chf1 <- f1 {}
|
||||
i := <-ch1 {
|
||||
sum1 += i
|
||||
}
|
||||
j := <-ch2 {
|
||||
sum2 += j
|
||||
}
|
||||
chf2 <- f2 {}
|
||||
}
|
||||
}
|
||||
sumch1 <- sum1
|
||||
sumch2 <- sum2
|
||||
}
|
||||
|
||||
fn do_send_int(ch chan int, factor int) {
|
||||
for i in 0 .. 10000 {
|
||||
ch <- (i * factor)
|
||||
}
|
||||
}
|
||||
|
||||
fn do_rec_f64(ch chan f64, sumch chan f64) {
|
||||
mut sum := 0.0
|
||||
for _ in 0 .. 10000 {
|
||||
sum += <-ch
|
||||
}
|
||||
sumch <- sum
|
||||
}
|
||||
|
||||
fn test_select() {
|
||||
ch1 := chan int{cap: 3}
|
||||
ch2 := chan int{}
|
||||
// buffer length of chf1 mus be multiple of 3 (# select threads)
|
||||
chf1 := chan f64{cap: 30}
|
||||
chf2 := chan f64{}
|
||||
chsum1 := chan i64{}
|
||||
chsum2 := chan i64{}
|
||||
chsumf1 := chan f64{}
|
||||
chsumf2 := chan f64{}
|
||||
spawn do_send_int(ch1, 3)
|
||||
spawn do_select(ch1, ch2, chf1, chf2, chsum1, chsum2)
|
||||
spawn do_rec_f64(chf1, chsumf1)
|
||||
spawn do_rec_f64(chf2, chsumf2)
|
||||
spawn do_rec_f64(chf2, chsumf2)
|
||||
spawn do_select(ch1, ch2, chf1, chf2, chsum1, chsum2)
|
||||
spawn do_send_int(ch2, 7)
|
||||
spawn do_send_int(ch2, 17)
|
||||
spawn do_select(ch1, ch2, chf1, chf2, chsum1, chsum2)
|
||||
|
||||
sum1 := <-chsum1 + <-chsum1 + <-chsum1
|
||||
sum2 := <-chsum2 + <-chsum2 + <-chsum2
|
||||
mut sumf1 := <-chsumf1
|
||||
// empty channel buffer
|
||||
for _ in 0 .. chf1.cap {
|
||||
sumf1 += <-chf1
|
||||
}
|
||||
sumf2 := <-chsumf2 + <-chsumf2
|
||||
// Use Gauß' formula
|
||||
expected_sum := i64(10000) * (10000 - 1) / 2
|
||||
assert sum1 == 3 * expected_sum
|
||||
assert sum2 == (7 + 17) * expected_sum
|
||||
assert sumf1 == 17.0 * f64(10000 + chf1.cap)
|
||||
assert sumf2 == 7.0 * 20000
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
fn wait_select_once(ch &Channel, done chan string, label string) {
|
||||
mut channels := [ch]
|
||||
directions := [Direction.pop]
|
||||
mut value := 0
|
||||
mut objs := [voidptr(&value)]
|
||||
idx := channel_select(mut channels, directions, mut objs, time.infinite)
|
||||
match idx {
|
||||
0 {
|
||||
done <- '${label}:${value}'
|
||||
}
|
||||
-2 {
|
||||
done <- '${label}:closed'
|
||||
}
|
||||
else {
|
||||
done <- '${label}:idx=${idx}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_subscriber_count(ch &Channel) int {
|
||||
ch.read_sub_mtx.lock()
|
||||
defer {
|
||||
ch.read_sub_mtx.unlock()
|
||||
}
|
||||
mut count := 0
|
||||
mut node := ch.read_subscriber
|
||||
for unsafe { node != 0 } {
|
||||
count++
|
||||
node = node.nxt
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fn wait_for_pop_subscribers(ch &Channel, want int) {
|
||||
for _ in 0 .. 200 {
|
||||
if pop_subscriber_count(ch) == want {
|
||||
return
|
||||
}
|
||||
time.sleep(5 * time.millisecond)
|
||||
}
|
||||
assert false, 'timed out waiting for ${want} read select subscriber(s)'
|
||||
}
|
||||
|
||||
fn test_select_waiters_are_fifo() {
|
||||
mut ch := new_channel[int](0)
|
||||
done := chan string{cap: 2}
|
||||
spawn wait_select_once(ch, done, 'first')
|
||||
wait_for_pop_subscribers(ch, 1)
|
||||
spawn wait_select_once(ch, done, 'second')
|
||||
wait_for_pop_subscribers(ch, 2)
|
||||
// Let both goroutines settle into sem.wait() inside channel_select.
|
||||
// Subscriber registration happens before the first non-blocking try,
|
||||
// so there is a small window where the second goroutine is still in
|
||||
// its initial try_pop before blocking.
|
||||
time.sleep(50 * time.millisecond)
|
||||
value := 999
|
||||
ch.push(&value)
|
||||
assert <-done == 'first:999'
|
||||
ch.close()
|
||||
assert <-done == 'second:closed'
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
module sync
|
||||
|
||||
// vtest retry: 3
|
||||
|
||||
// ATTENTION! Do not use this file as an example!
|
||||
// For that, please look at `channel_select_2_test.v` or `channel_select_3_test.v`
|
||||
// This test case uses the implementation in `sync/channels.v` directly
|
||||
// in order to test it independently from the support in the core language
|
||||
import os
|
||||
import time
|
||||
|
||||
fn test_should_run_flaky_test() {
|
||||
if os.getenv('VTEST_RUN_FLAKY') != '1' {
|
||||
eprintln('> skipping running flaky test, set VTEST_RUN_FLAKY to 1, to run it')
|
||||
exit(0)
|
||||
}
|
||||
assert true
|
||||
}
|
||||
|
||||
fn do_rec_i64(mut ch Channel) {
|
||||
mut sum := i64(0)
|
||||
for _ in 0 .. 300 {
|
||||
mut a := i64(0)
|
||||
ch.pop(&a)
|
||||
sum += a
|
||||
}
|
||||
assert sum == 300 * (300 - 1) / 2
|
||||
}
|
||||
|
||||
fn do_send_int(mut ch Channel) {
|
||||
for i in 0 .. 300 {
|
||||
ch.push(&i)
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send_u8(mut ch Channel) {
|
||||
for i in 0 .. 300 {
|
||||
ii := u8(i)
|
||||
ch.push(&ii)
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send_i64(mut ch Channel) {
|
||||
for i in 0 .. 300 {
|
||||
ii := i64(i)
|
||||
ch.push(&ii)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_select() {
|
||||
mut chi := new_channel[int](0)
|
||||
mut chl := new_channel[i64](1)
|
||||
mut chb := new_channel[u8](10)
|
||||
mut recch := new_channel[i64](0)
|
||||
spawn do_rec_i64(mut recch)
|
||||
spawn do_send_int(mut chi)
|
||||
spawn do_send_u8(mut chb)
|
||||
spawn do_send_i64(mut chl)
|
||||
mut channels := [chi, recch, chl, chb]
|
||||
directions := [Direction.pop, .push, .pop, .pop]
|
||||
mut sum := i64(0)
|
||||
mut rl := i64(0)
|
||||
mut ri := int(0)
|
||||
mut rb := u8(0)
|
||||
mut sl := i64(0)
|
||||
mut objs := [voidptr(&ri), &sl, &rl, &rb]
|
||||
for _ in 0 .. 1200 {
|
||||
idx := channel_select(mut channels, directions, mut objs, time.infinite)
|
||||
match idx {
|
||||
0 {
|
||||
sum += ri
|
||||
}
|
||||
1 {
|
||||
sl++
|
||||
}
|
||||
2 {
|
||||
sum += rl
|
||||
}
|
||||
3 {
|
||||
sum += rb
|
||||
}
|
||||
else {
|
||||
println('got ${idx} (timeout)')
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use Gauß' formula for the first 2 contributions
|
||||
// the 3rd contribution is `byte` and must be seen modulo 256
|
||||
expected_sum := 2 * (300 * (300 - 1) / 2) + 256 * (256 - 1) / 2 + 44 * (44 - 1) / 2
|
||||
assert sum == expected_sum
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
fn test_channel_try_buffered() {
|
||||
ch := chan int{cap: 5}
|
||||
for z in 2 .. 13 {
|
||||
if ch.try_push(z) == .not_ready {
|
||||
assert z == 7
|
||||
break
|
||||
}
|
||||
}
|
||||
mut obj := int(0)
|
||||
for ch.try_pop(mut obj) == .success {
|
||||
println(obj)
|
||||
}
|
||||
assert obj == 6
|
||||
ch <- 17
|
||||
obj = <-ch
|
||||
assert obj == 17
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fn test_channel_try_unbuffered() {
|
||||
ch := chan int{}
|
||||
for z in 5 .. 8 {
|
||||
if ch.try_push(z) == .not_ready {
|
||||
assert z == 5
|
||||
break
|
||||
}
|
||||
panic('push on non-ready channel not detected')
|
||||
}
|
||||
mut obj := -17
|
||||
for ch.try_pop(mut obj) == .success {}
|
||||
assert obj == -17
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
module sync
|
||||
|
||||
import time
|
||||
import rand
|
||||
|
||||
// how often to try to get data without blocking before to wait for semaphore
|
||||
const spinloops = u32(750)
|
||||
const spinloops_sem = u32(4000)
|
||||
|
||||
enum BufferElemStat {
|
||||
unused = 0
|
||||
writing
|
||||
written
|
||||
reading
|
||||
}
|
||||
|
||||
struct Subscription {
|
||||
mut:
|
||||
sem &Semaphore = unsafe { nil }
|
||||
prev &&Subscription = unsafe { nil }
|
||||
nxt &Subscription = unsafe { nil }
|
||||
}
|
||||
|
||||
// append_subscription keeps select waiters in FIFO order, so the oldest waiter
|
||||
// is woken first when a channel becomes ready.
|
||||
fn append_subscription(head &&Subscription, sub &Subscription) {
|
||||
unsafe {
|
||||
mut link := head
|
||||
for *link != 0 {
|
||||
link = &(*link).nxt
|
||||
}
|
||||
sub.prev = link
|
||||
sub.nxt = nil
|
||||
*link = sub
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Direction {
|
||||
pop
|
||||
push
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.atomic_uintptr_t {}
|
||||
|
||||
pub struct Channel {
|
||||
ringbuf &u8 = unsafe { nil } // queue for buffered channels
|
||||
statusbuf &u8 = unsafe { nil } // flags to synchronize write/read in ringbuf
|
||||
objsize u32
|
||||
mut:
|
||||
// atomic
|
||||
writesem Semaphore // to wake thread that wanted to write, but buffer was full
|
||||
readsem Semaphore // to wake thread that wanted to read, but buffer was empty
|
||||
writesem_im Semaphore
|
||||
readsem_im Semaphore
|
||||
write_adr C.atomic_uintptr_t // if != NULL the next obj can be written here without wait
|
||||
read_adr C.atomic_uintptr_t // if != NULL an obj can be read from here without wait
|
||||
adr_read C.atomic_uintptr_t // used to identify origin of writesem
|
||||
adr_written C.atomic_uintptr_t // used to identify origin of readsem
|
||||
write_free u32 // for queue state
|
||||
read_avail u32
|
||||
buf_elem_write_idx u32
|
||||
buf_elem_read_idx u32
|
||||
// for select
|
||||
write_subscriber &Subscription = unsafe { nil }
|
||||
read_subscriber &Subscription = unsafe { nil }
|
||||
write_sub_mtx &SpinLock
|
||||
read_sub_mtx &SpinLock
|
||||
closed u16
|
||||
close_err IError = none
|
||||
pub:
|
||||
cap u32 // queue length in #objects
|
||||
}
|
||||
|
||||
pub fn new_channel[T](n u32) &Channel {
|
||||
st := if sizeof(T) > 0 { sizeof(T) } else { 1 }
|
||||
if isreftype(T) {
|
||||
return new_channel_st(n, st)
|
||||
} else {
|
||||
return new_channel_st_noscan(n, st)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_channel_st(n u32, st u32) &Channel {
|
||||
wsem := if n > 0 { n } else { 1 }
|
||||
rsem := if n > 0 { u32(0) } else { 1 }
|
||||
rbuf := if n > 0 { unsafe { malloc(int(n * st)) } } else { &u8(unsafe { nil }) }
|
||||
sbuf := if n > 0 { vcalloc_noscan(int(n * 2)) } else { &u8(unsafe { nil }) }
|
||||
mut ch := Channel{
|
||||
objsize: st
|
||||
cap: n
|
||||
write_free: n
|
||||
read_avail: 0
|
||||
ringbuf: rbuf
|
||||
statusbuf: sbuf
|
||||
write_subscriber: unsafe { nil }
|
||||
read_subscriber: unsafe { nil }
|
||||
write_sub_mtx: new_spin_lock()
|
||||
read_sub_mtx: new_spin_lock()
|
||||
}
|
||||
ch.writesem.init(wsem)
|
||||
ch.readsem.init(rsem)
|
||||
ch.writesem_im.init(0)
|
||||
ch.readsem_im.init(0)
|
||||
return &ch
|
||||
}
|
||||
|
||||
fn new_channel_st_noscan(n u32, st u32) &Channel {
|
||||
$if gcboehm_opt ? {
|
||||
wsem := if n > 0 { n } else { 1 }
|
||||
rsem := if n > 0 { u32(0) } else { 1 }
|
||||
rbuf := if n > 0 { unsafe { malloc_noscan(int(n * st)) } } else { &u8(unsafe { nil }) }
|
||||
sbuf := if n > 0 { vcalloc_noscan(int(n * 2)) } else { &u8(unsafe { nil }) }
|
||||
mut ch := Channel{
|
||||
objsize: st
|
||||
cap: n
|
||||
write_free: n
|
||||
read_avail: 0
|
||||
ringbuf: rbuf
|
||||
statusbuf: sbuf
|
||||
write_subscriber: unsafe { nil }
|
||||
read_subscriber: unsafe { nil }
|
||||
write_sub_mtx: new_spin_lock()
|
||||
read_sub_mtx: new_spin_lock()
|
||||
}
|
||||
ch.writesem.init(wsem)
|
||||
ch.readsem.init(rsem)
|
||||
ch.writesem_im.init(0)
|
||||
ch.readsem_im.init(0)
|
||||
return &ch
|
||||
} $else {
|
||||
return new_channel_st(n, st)
|
||||
}
|
||||
}
|
||||
|
||||
// close closes the channel and optionally stores an error that will be
|
||||
// returned by receive operations that use `or {}` or `?` after the
|
||||
// buffered values have been drained.
|
||||
pub fn (mut ch Channel) close(errs ...IError) {
|
||||
open_val := u16(0)
|
||||
if !C.atomic_compare_exchange_strong_u16(&ch.closed, &open_val, 1) {
|
||||
return
|
||||
}
|
||||
if errs.len > 0 {
|
||||
ch.close_err = errs[0]
|
||||
}
|
||||
mut nulladr := unsafe { nil }
|
||||
for !C.atomic_compare_exchange_weak_ptr(voidptr(&ch.adr_written), voidptr(&nulladr), isize(-1)) {
|
||||
nulladr = unsafe { nil }
|
||||
}
|
||||
ch.readsem_im.post()
|
||||
ch.readsem.post()
|
||||
ch.read_sub_mtx.lock()
|
||||
if ch.read_subscriber != unsafe { nil } {
|
||||
ch.read_subscriber.sem.post()
|
||||
}
|
||||
ch.read_sub_mtx.unlock()
|
||||
ch.write_sub_mtx.lock()
|
||||
if ch.write_subscriber != unsafe { nil } {
|
||||
ch.write_subscriber.sem.post()
|
||||
}
|
||||
ch.write_sub_mtx.unlock()
|
||||
ch.writesem.post()
|
||||
if ch.cap == 0 {
|
||||
C.atomic_store_ptr(unsafe { &voidptr(&ch.read_adr) }, unsafe { nil })
|
||||
}
|
||||
ch.writesem_im.post()
|
||||
|
||||
// Do not destroy `read_sub_mtx` and `write_sub_mtx` here,
|
||||
// because we can read from a closed channel later.
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (ch &Channel) closed_error() IError {
|
||||
if ch.close_err !is None__ {
|
||||
return ch.close_err
|
||||
}
|
||||
return error('channel closed')
|
||||
}
|
||||
|
||||
@[inline]
|
||||
pub fn (mut ch Channel) len() int {
|
||||
return int(C.atomic_load_u32(&ch.read_avail))
|
||||
}
|
||||
|
||||
@[inline]
|
||||
pub fn (mut ch Channel) closed() bool {
|
||||
return C.atomic_load_u16(&ch.closed) != 0
|
||||
}
|
||||
|
||||
@[inline]
|
||||
pub fn (mut ch Channel) push(src voidptr) {
|
||||
if ch.try_push_priv(src, false) == .closed {
|
||||
panic('push on closed channel')
|
||||
}
|
||||
}
|
||||
|
||||
@[inline]
|
||||
pub fn (mut ch Channel) try_push(src voidptr) ChanState {
|
||||
return ch.try_push_priv(src, true)
|
||||
}
|
||||
|
||||
fn (mut ch Channel) try_push_priv(src voidptr, no_block bool) ChanState {
|
||||
if C.atomic_load_u16(&ch.closed) != 0 {
|
||||
return .closed
|
||||
}
|
||||
spinloops_sem_, spinloops_ := if no_block { u32(1), u32(1) } else { spinloops, spinloops_sem }
|
||||
mut have_swapped := false
|
||||
for {
|
||||
mut got_sem := false
|
||||
mut wradr := C.atomic_load_ptr(unsafe { &voidptr(&ch.write_adr) })
|
||||
for wradr != C.NULL {
|
||||
if C.atomic_compare_exchange_strong_ptr(voidptr(&ch.write_adr), voidptr(&wradr),
|
||||
isize(0))
|
||||
{
|
||||
// there is a reader waiting for us
|
||||
unsafe { C.memcpy(wradr, src, ch.objsize) }
|
||||
mut nulladr := unsafe { nil }
|
||||
for !C.atomic_compare_exchange_weak_ptr(voidptr(&ch.adr_written),
|
||||
voidptr(&nulladr), isize(wradr)) {
|
||||
nulladr = unsafe { nil }
|
||||
}
|
||||
ch.readsem_im.post()
|
||||
return .success
|
||||
}
|
||||
}
|
||||
if no_block && ch.cap == 0 {
|
||||
return .not_ready
|
||||
}
|
||||
// get token to read
|
||||
for _ in 0 .. spinloops_sem_ {
|
||||
if got_sem {
|
||||
break
|
||||
}
|
||||
got_sem = ch.writesem.try_wait()
|
||||
}
|
||||
if !got_sem {
|
||||
if no_block {
|
||||
return .not_ready
|
||||
}
|
||||
ch.writesem.wait()
|
||||
}
|
||||
if C.atomic_load_u16(&ch.closed) != 0 {
|
||||
ch.writesem.post()
|
||||
return .closed
|
||||
}
|
||||
if ch.cap == 0 {
|
||||
// try to advertise current object as readable
|
||||
mut read_in_progress := false
|
||||
C.atomic_store_ptr(unsafe { &voidptr(&ch.read_adr) }, src)
|
||||
wradr = C.atomic_load_ptr(unsafe { &voidptr(&ch.write_adr) })
|
||||
if wradr != C.NULL {
|
||||
mut src2 := src
|
||||
if C.atomic_compare_exchange_strong_ptr(voidptr(&ch.read_adr), voidptr(&src2),
|
||||
isize(0))
|
||||
{
|
||||
ch.writesem.post()
|
||||
continue
|
||||
} else {
|
||||
read_in_progress = true
|
||||
}
|
||||
}
|
||||
if !read_in_progress {
|
||||
ch.read_sub_mtx.lock()
|
||||
if ch.read_subscriber != unsafe { nil } {
|
||||
ch.read_subscriber.sem.post()
|
||||
}
|
||||
ch.read_sub_mtx.unlock()
|
||||
}
|
||||
mut src2 := src
|
||||
for sp := u32(0); sp < spinloops_ || read_in_progress; sp++ {
|
||||
if C.atomic_compare_exchange_strong_ptr(voidptr(&ch.adr_read), voidptr(&src2),
|
||||
isize(0))
|
||||
{
|
||||
have_swapped = true
|
||||
read_in_progress = true
|
||||
break
|
||||
}
|
||||
src2 = src
|
||||
}
|
||||
mut got_im_sem := false
|
||||
for sp := u32(0); sp < spinloops_sem_ || read_in_progress; sp++ {
|
||||
got_im_sem = ch.writesem_im.try_wait()
|
||||
if got_im_sem {
|
||||
break
|
||||
}
|
||||
}
|
||||
for {
|
||||
if got_im_sem {
|
||||
got_im_sem = false
|
||||
} else {
|
||||
ch.writesem_im.wait()
|
||||
}
|
||||
if C.atomic_load_u16(&ch.closed) != 0 {
|
||||
if have_swapped
|
||||
|| C.atomic_compare_exchange_strong_ptr(voidptr(&ch.adr_read), voidptr(&src2), isize(0)) {
|
||||
ch.writesem.post()
|
||||
return .success
|
||||
} else {
|
||||
return .closed
|
||||
}
|
||||
}
|
||||
if have_swapped
|
||||
|| C.atomic_compare_exchange_strong_ptr(voidptr(&ch.adr_read), voidptr(&src2), isize(0)) {
|
||||
ch.writesem.post()
|
||||
break
|
||||
} else {
|
||||
// this semaphore was not for us - repost in
|
||||
ch.writesem_im.post()
|
||||
if src2 == voidptr(-1) {
|
||||
ch.readsem.post()
|
||||
return .closed
|
||||
}
|
||||
src2 = src
|
||||
}
|
||||
}
|
||||
return .success
|
||||
} else {
|
||||
// buffered channel
|
||||
mut space_in_queue := false
|
||||
mut wr_free := C.atomic_load_u32(&ch.write_free)
|
||||
for wr_free > 0 {
|
||||
space_in_queue = C.atomic_compare_exchange_weak_u32(&ch.write_free, &wr_free,
|
||||
wr_free - 1)
|
||||
if space_in_queue {
|
||||
break
|
||||
}
|
||||
}
|
||||
if space_in_queue {
|
||||
mut wr_idx := C.atomic_load_u32(&ch.buf_elem_write_idx)
|
||||
for {
|
||||
mut new_wr_idx := wr_idx + 1
|
||||
for new_wr_idx >= ch.cap {
|
||||
new_wr_idx -= ch.cap
|
||||
}
|
||||
if C.atomic_compare_exchange_strong_u32(&ch.buf_elem_write_idx, &wr_idx,
|
||||
new_wr_idx)
|
||||
{
|
||||
break
|
||||
}
|
||||
}
|
||||
mut wr_ptr := ch.ringbuf
|
||||
mut status_adr := ch.statusbuf
|
||||
unsafe {
|
||||
wr_ptr += (wr_idx * ch.objsize)
|
||||
status_adr += wr_idx * sizeof(u16)
|
||||
}
|
||||
mut expected_status := u16(BufferElemStat.unused)
|
||||
for !C.atomic_compare_exchange_weak_u16(status_adr, &expected_status,
|
||||
u16(BufferElemStat.writing)) {
|
||||
expected_status = u16(BufferElemStat.unused)
|
||||
}
|
||||
unsafe {
|
||||
C.memcpy(wr_ptr, src, ch.objsize)
|
||||
}
|
||||
C.atomic_store_u16(unsafe { &u16(status_adr) }, u16(BufferElemStat.written))
|
||||
C.atomic_fetch_add_u32(voidptr(&ch.read_avail), 1)
|
||||
ch.readsem.post()
|
||||
ch.read_sub_mtx.lock()
|
||||
if ch.read_subscriber != unsafe { nil } {
|
||||
ch.read_subscriber.sem.post()
|
||||
}
|
||||
ch.read_sub_mtx.unlock()
|
||||
return .success
|
||||
} else {
|
||||
if no_block {
|
||||
return .not_ready
|
||||
}
|
||||
ch.writesem.post()
|
||||
}
|
||||
}
|
||||
}
|
||||
// we should not get here but the V compiler want's to see a return statement
|
||||
panic('unknown `try_push_priv` state')
|
||||
}
|
||||
|
||||
@[inline]
|
||||
pub fn (mut ch Channel) pop(dest voidptr) bool {
|
||||
return ch.try_pop_priv(dest, false) == .success
|
||||
}
|
||||
|
||||
// try_pop returns `.success` if an object is popped without blocking.
|
||||
// Pass the destination as `mut`: `ch.try_pop(mut value)`, not `ch.try_pop(&value)`.
|
||||
@[inline]
|
||||
pub fn (mut ch Channel) try_pop(dest voidptr) ChanState {
|
||||
return ch.try_pop_priv(dest, true)
|
||||
}
|
||||
|
||||
// try_pop_select_priv treats already closed channels as unavailable for non-blocking `select ... else`.
|
||||
@[inline]
|
||||
fn (mut ch Channel) try_pop_select_priv(dest voidptr) ChanState {
|
||||
if C.atomic_load_u16(&ch.closed) != 0 {
|
||||
return .closed
|
||||
}
|
||||
return ch.try_pop_priv(dest, true)
|
||||
}
|
||||
|
||||
fn (mut ch Channel) try_pop_priv(dest voidptr, no_block bool) ChanState {
|
||||
spinloops_sem_, spinloops_ := if no_block { u32(1), u32(1) } else { spinloops, spinloops_sem }
|
||||
mut have_swapped := false
|
||||
mut write_in_progress := false
|
||||
for {
|
||||
mut got_sem := false
|
||||
if ch.cap == 0 {
|
||||
// unbuffered channel - first see if a `push()` has adversized
|
||||
mut rdadr := C.atomic_load_ptr(unsafe { &voidptr(&ch.read_adr) })
|
||||
for rdadr != C.NULL {
|
||||
if C.atomic_compare_exchange_strong_ptr(voidptr(&ch.read_adr), voidptr(&rdadr),
|
||||
isize(0))
|
||||
{
|
||||
// there is a writer waiting for us
|
||||
unsafe { C.memcpy(dest, rdadr, ch.objsize) }
|
||||
mut nulladr := unsafe { nil }
|
||||
for !C.atomic_compare_exchange_weak_ptr(voidptr(&ch.adr_read),
|
||||
voidptr(&nulladr), isize(rdadr)) {
|
||||
nulladr = unsafe { nil }
|
||||
}
|
||||
ch.writesem_im.post()
|
||||
return .success
|
||||
}
|
||||
}
|
||||
if no_block {
|
||||
if C.atomic_load_u16(&ch.closed) == 0 {
|
||||
return .not_ready
|
||||
} else {
|
||||
return .closed
|
||||
}
|
||||
}
|
||||
}
|
||||
// get token to read
|
||||
for _ in 0 .. spinloops_sem_ {
|
||||
if got_sem {
|
||||
break
|
||||
}
|
||||
got_sem = ch.readsem.try_wait()
|
||||
}
|
||||
if !got_sem {
|
||||
if no_block {
|
||||
if C.atomic_load_u16(&ch.closed) == 0 {
|
||||
return .not_ready
|
||||
} else {
|
||||
return .closed
|
||||
}
|
||||
}
|
||||
ch.readsem.wait()
|
||||
}
|
||||
if ch.cap > 0 {
|
||||
// try to get buffer token
|
||||
mut obj_in_queue := false
|
||||
mut rd_avail := C.atomic_load_u32(&ch.read_avail)
|
||||
for rd_avail > 0 {
|
||||
obj_in_queue = C.atomic_compare_exchange_weak_u32(&ch.read_avail, &rd_avail,
|
||||
rd_avail - 1)
|
||||
if obj_in_queue {
|
||||
break
|
||||
}
|
||||
}
|
||||
if obj_in_queue {
|
||||
mut rd_idx := C.atomic_load_u32(&ch.buf_elem_read_idx)
|
||||
for {
|
||||
mut new_rd_idx := rd_idx + 1
|
||||
for new_rd_idx >= ch.cap {
|
||||
new_rd_idx -= ch.cap
|
||||
}
|
||||
if C.atomic_compare_exchange_weak_u32(&ch.buf_elem_read_idx, &rd_idx,
|
||||
new_rd_idx)
|
||||
{
|
||||
break
|
||||
}
|
||||
}
|
||||
mut rd_ptr := ch.ringbuf
|
||||
mut status_adr := ch.statusbuf
|
||||
unsafe {
|
||||
rd_ptr += rd_idx * ch.objsize
|
||||
status_adr += rd_idx * sizeof(u16)
|
||||
}
|
||||
mut expected_status := u16(BufferElemStat.written)
|
||||
for !C.atomic_compare_exchange_weak_u16(status_adr, &expected_status,
|
||||
u16(BufferElemStat.reading)) {
|
||||
expected_status = u16(BufferElemStat.written)
|
||||
}
|
||||
unsafe {
|
||||
C.memcpy(dest, rd_ptr, ch.objsize)
|
||||
}
|
||||
C.atomic_store_u16(unsafe { &u16(status_adr) }, u16(BufferElemStat.unused))
|
||||
C.atomic_fetch_add_u32(voidptr(&ch.write_free), 1)
|
||||
ch.writesem.post()
|
||||
ch.write_sub_mtx.lock()
|
||||
if ch.write_subscriber != unsafe { nil } {
|
||||
ch.write_subscriber.sem.post()
|
||||
}
|
||||
ch.write_sub_mtx.unlock()
|
||||
return .success
|
||||
}
|
||||
}
|
||||
// try to advertise `dest` as writable
|
||||
C.atomic_store_ptr(unsafe { &voidptr(&ch.write_adr) }, dest)
|
||||
if ch.cap == 0 {
|
||||
mut rdadr := C.atomic_load_ptr(unsafe { &voidptr(&ch.read_adr) })
|
||||
if rdadr != C.NULL {
|
||||
mut dest2 := dest
|
||||
if C.atomic_compare_exchange_strong_ptr(voidptr(&ch.write_adr), voidptr(&dest2),
|
||||
isize(0))
|
||||
{
|
||||
ch.readsem.post()
|
||||
continue
|
||||
} else {
|
||||
write_in_progress = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if ch.cap == 0 && !write_in_progress {
|
||||
ch.write_sub_mtx.lock()
|
||||
if ch.write_subscriber != unsafe { nil } {
|
||||
ch.write_subscriber.sem.post()
|
||||
}
|
||||
ch.write_sub_mtx.unlock()
|
||||
}
|
||||
mut dest2 := dest
|
||||
for sp := u32(0); sp < spinloops_ || write_in_progress; sp++ {
|
||||
if C.atomic_compare_exchange_strong_ptr(voidptr(&ch.adr_written), voidptr(&dest2),
|
||||
isize(0))
|
||||
{
|
||||
have_swapped = true
|
||||
break
|
||||
} else if dest2 == voidptr(-1) {
|
||||
ch.readsem.post()
|
||||
return .closed
|
||||
}
|
||||
dest2 = dest
|
||||
}
|
||||
mut got_im_sem := false
|
||||
for sp := u32(0); sp < spinloops_sem_ || write_in_progress; sp++ {
|
||||
got_im_sem = ch.readsem_im.try_wait()
|
||||
if got_im_sem {
|
||||
break
|
||||
}
|
||||
}
|
||||
for {
|
||||
if got_im_sem {
|
||||
got_im_sem = false
|
||||
} else {
|
||||
ch.readsem_im.wait()
|
||||
}
|
||||
if have_swapped
|
||||
|| C.atomic_compare_exchange_strong_ptr(voidptr(&ch.adr_written), voidptr(&dest2), isize(0)) {
|
||||
ch.readsem.post()
|
||||
break
|
||||
} else {
|
||||
// this semaphore was not for us - repost in
|
||||
ch.readsem_im.post()
|
||||
if dest2 == voidptr(-1) {
|
||||
ch.readsem.post()
|
||||
return .closed
|
||||
}
|
||||
dest2 = dest
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return .success
|
||||
}
|
||||
|
||||
// channel_select waits `timeout` on any of `channels[i]` until one of them can
|
||||
// push (`dir[i] == .push`) or pop (`dir[i] == .pop`) the object referenced by
|
||||
// `objrefs[i]`. `timeout = time.infinite` means wait unlimited time.
|
||||
// `timeout <= 0` means return immediately if no transaction can be performed
|
||||
// without waiting. It returns the selected channel index, `-1` on timeout, and
|
||||
// `-2` when all channels are closed.
|
||||
pub fn channel_select(mut channels []&Channel, dir []Direction, mut objrefs []voidptr, timeout time.Duration) int {
|
||||
skip_closed_pop := timeout < 0
|
||||
actual_timeout := if skip_closed_pop { time.Duration(0) } else { timeout }
|
||||
closed_pop_mode := if skip_closed_pop {
|
||||
SelectClosedPopMode.skip
|
||||
} else {
|
||||
SelectClosedPopMode.closed
|
||||
}
|
||||
return channel_select_priv(mut channels, dir, mut objrefs, actual_timeout, closed_pop_mode)
|
||||
}
|
||||
|
||||
enum SelectClosedPopMode {
|
||||
closed
|
||||
ready
|
||||
skip
|
||||
}
|
||||
|
||||
// channel_select_lang is used by the language `select` implementation.
|
||||
// Closed receive cases stay selectable for blocking/timed selects to match
|
||||
// plain `<-ch` semantics, while `select ... else` still skips them.
|
||||
fn channel_select_lang(mut channels []&Channel, dir []Direction, mut objrefs []voidptr, timeout time.Duration) int {
|
||||
skip_closed_pop := timeout < 0
|
||||
actual_timeout := if skip_closed_pop { time.Duration(0) } else { timeout }
|
||||
closed_pop_mode := if skip_closed_pop {
|
||||
SelectClosedPopMode.skip
|
||||
} else {
|
||||
SelectClosedPopMode.ready
|
||||
}
|
||||
return channel_select_priv(mut channels, dir, mut objrefs, actual_timeout, closed_pop_mode)
|
||||
}
|
||||
|
||||
fn channel_select_priv(mut channels []&Channel, dir []Direction, mut objrefs []voidptr, timeout time.Duration, closed_pop_mode SelectClosedPopMode) int {
|
||||
$if debug_channels ? {
|
||||
assert channels.len == dir.len
|
||||
assert dir.len == objrefs.len
|
||||
}
|
||||
mut subscr := []Subscription{len: channels.len}
|
||||
mut sem := unsafe { Semaphore{} }
|
||||
sem.init(0)
|
||||
for i, ch in channels {
|
||||
subscr[i].sem = unsafe { &sem }
|
||||
sub_mtx, subscriber := if dir[i] == .push {
|
||||
ch.write_sub_mtx, &ch.write_subscriber
|
||||
} else {
|
||||
ch.read_sub_mtx, &ch.read_subscriber
|
||||
}
|
||||
sub_mtx.lock()
|
||||
unsafe {
|
||||
append_subscription(subscriber, &subscr[i])
|
||||
}
|
||||
sub_mtx.unlock()
|
||||
}
|
||||
stopwatch := if timeout == time.infinite || timeout <= 0 {
|
||||
time.StopWatch{}
|
||||
} else {
|
||||
time.new_stopwatch()
|
||||
}
|
||||
mut event_idx := -1 // negative index means `timed out`
|
||||
|
||||
outer: for {
|
||||
rnd := rand.intn(channels.len) or { 0 }
|
||||
mut num_closed := 0
|
||||
mut ready_closed_idx := -1
|
||||
for j, _ in channels {
|
||||
mut i := j + rnd
|
||||
if i >= channels.len {
|
||||
i -= channels.len
|
||||
}
|
||||
stat := if dir[i] == .push {
|
||||
channels[i].try_push_priv(objrefs[i], true)
|
||||
} else if closed_pop_mode == .skip {
|
||||
channels[i].try_pop_select_priv(objrefs[i])
|
||||
} else {
|
||||
channels[i].try_pop_priv(objrefs[i], true)
|
||||
}
|
||||
if stat == .success {
|
||||
event_idx = i
|
||||
break outer
|
||||
} else if stat == .closed && dir[i] == .pop && closed_pop_mode == .ready {
|
||||
unsafe {
|
||||
C.memset(objrefs[i], 0, channels[i].objsize)
|
||||
}
|
||||
if ready_closed_idx == -1 {
|
||||
ready_closed_idx = i
|
||||
}
|
||||
num_closed++
|
||||
} else if stat == .closed {
|
||||
num_closed++
|
||||
}
|
||||
}
|
||||
if ready_closed_idx >= 0 {
|
||||
event_idx = ready_closed_idx
|
||||
break outer
|
||||
}
|
||||
if num_closed == channels.len {
|
||||
event_idx = -2
|
||||
break outer
|
||||
}
|
||||
if timeout <= 0 {
|
||||
break outer
|
||||
}
|
||||
if timeout != time.infinite {
|
||||
remaining := timeout - stopwatch.elapsed()
|
||||
if !sem.timed_wait(remaining) {
|
||||
break outer
|
||||
}
|
||||
} else {
|
||||
sem.wait()
|
||||
}
|
||||
}
|
||||
// reset subscribers
|
||||
for i, ch in channels {
|
||||
sub_mtx := if dir[i] == .push {
|
||||
ch.write_sub_mtx
|
||||
} else {
|
||||
ch.read_sub_mtx
|
||||
}
|
||||
sub_mtx.lock()
|
||||
unsafe {
|
||||
*subscr[i].prev = subscr[i].nxt
|
||||
}
|
||||
if unsafe { subscr[i].nxt != 0 } {
|
||||
subscr[i].nxt.prev = subscr[i].prev
|
||||
subscr[i].nxt.sem.post()
|
||||
}
|
||||
sub_mtx.unlock()
|
||||
}
|
||||
sem.destroy()
|
||||
return event_idx
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
module sync
|
||||
|
||||
pub struct Channel {
|
||||
arr array
|
||||
}
|
||||
|
||||
pub fn new_channel[T](n u32) &Channel {
|
||||
return &Channel{arr, new_array()}
|
||||
}
|
||||
|
||||
pub fn (mut ch Channel) close() {}
|
||||
|
||||
pub fn (mut ch Channel) push(src voidptr) {
|
||||
#array_push(ch.val.arr,src)
|
||||
}
|
||||
|
||||
pub fn (ch Channel) len() int {
|
||||
return ch.arr.len
|
||||
}
|
||||
|
||||
pub fn (ch Channel) closed() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
pub fn (mut ch Channel) pop(dest voidptr) {
|
||||
#dest.val = array_pop(ch.val.arr)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
module sync
|
||||
|
||||
// str returns a string representation of the Mutex pointer.
|
||||
pub fn (m &Mutex) str() string {
|
||||
return 'Mutex(${voidptr(m)})'
|
||||
}
|
||||
|
||||
// str returns a string representation of the RwMutex pointer.
|
||||
pub fn (m &RwMutex) str() string {
|
||||
return 'RwMutex(${voidptr(m)})'
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
module sync
|
||||
|
||||
@[heap]
|
||||
pub struct Cond {
|
||||
mut:
|
||||
// Externally provided mutex for shared resource protection
|
||||
mutex &Mutex
|
||||
// Internal lock for protecting wait queue access
|
||||
inner_mutex Mutex
|
||||
// Queue of waiting channels
|
||||
waiters []chan bool
|
||||
}
|
||||
|
||||
// new_cond creates new condition variable associated with given mutex
|
||||
pub fn new_cond(m &Mutex) &Cond {
|
||||
return &Cond{
|
||||
mutex: m
|
||||
inner_mutex: new_mutex()
|
||||
waiters: []chan bool{}
|
||||
}
|
||||
}
|
||||
|
||||
// wait waits for condition notification.
|
||||
// NOTE: Spurious wakeups are possible; always use in a loop:
|
||||
// mutex.lock()
|
||||
// for !condition {
|
||||
// cond.wait()
|
||||
// }
|
||||
// mutex.unlock()
|
||||
@[direct_array_access]
|
||||
pub fn (mut c Cond) wait() {
|
||||
// Create a channel for this waiting operation with capacity 1
|
||||
ch := chan bool{cap: 1}
|
||||
defer {
|
||||
ch.close()
|
||||
}
|
||||
|
||||
// Add this channel to the waiters queue
|
||||
c.inner_mutex.lock()
|
||||
c.waiters << ch
|
||||
c.inner_mutex.unlock()
|
||||
|
||||
// Release external lock and suspend
|
||||
c.mutex.unlock()
|
||||
_ := <-ch // Block until signaled
|
||||
|
||||
c.inner_mutex.lock()
|
||||
for i := c.waiters.len - 1; i >= 0; i-- {
|
||||
if c.waiters[i] == ch {
|
||||
c.waiters.delete(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
c.inner_mutex.unlock()
|
||||
// Re-acquire external lock before returning
|
||||
c.mutex.lock()
|
||||
}
|
||||
|
||||
// signal wakes one waiting thread.
|
||||
@[direct_array_access]
|
||||
pub fn (mut c Cond) signal() {
|
||||
c.inner_mutex.lock()
|
||||
defer { c.inner_mutex.unlock() }
|
||||
if c.waiters.len > 0 {
|
||||
// Remove first waiter from queue
|
||||
mut waiter := c.waiters[0]
|
||||
c.waiters.delete(0)
|
||||
if !waiter.closed {
|
||||
waiter <- true // Wake up the thread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast wakes all waiting threads.
|
||||
@[direct_array_access]
|
||||
pub fn (mut c Cond) broadcast() {
|
||||
c.inner_mutex.lock()
|
||||
defer { c.inner_mutex.unlock() }
|
||||
// Release all waiting ch
|
||||
for i in 0 .. c.waiters.len {
|
||||
mut waiter := c.waiters[i]
|
||||
if !waiter.closed {
|
||||
waiter <- true // Wake up the thread
|
||||
}
|
||||
}
|
||||
c.waiters.clear()
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import sync
|
||||
import sync.stdatomic { new_atomic }
|
||||
|
||||
// Test single thread wake-up scenario for condition variable
|
||||
fn test_single_thread_wakeup() {
|
||||
mut mutex := sync.new_mutex()
|
||||
mut cond := sync.new_cond(mutex)
|
||||
mut done := new_atomic(false)
|
||||
mut wake_count := new_atomic(0)
|
||||
ready_ch := chan bool{cap: 1}
|
||||
done_ch := chan bool{cap: 1}
|
||||
defer {
|
||||
ready_ch.close()
|
||||
done_ch.close()
|
||||
}
|
||||
|
||||
// Spawn waiting thread
|
||||
spawn fn [mut done, mut wake_count, mut cond, mut mutex, ready_ch, done_ch] () {
|
||||
mutex.lock()
|
||||
defer {
|
||||
mutex.unlock()
|
||||
}
|
||||
ready_ch <- true // Notify main thread of readiness
|
||||
|
||||
// Wait loop for conditional signals
|
||||
for !done.load() {
|
||||
cond.wait()
|
||||
wake_count.add(1)
|
||||
}
|
||||
done_ch <- true // Notify completion
|
||||
}()
|
||||
|
||||
// Wait for the worker to enter waiting state
|
||||
_ := <-ready_ch
|
||||
|
||||
// Trigger signaling sequence
|
||||
mutex.lock()
|
||||
cond.signal() // Wake the waiting thread
|
||||
done.store(true) // Terminate worker loop
|
||||
cond.signal() // Extra signal for loop exit
|
||||
mutex.unlock()
|
||||
|
||||
// Verify result
|
||||
_ := <-done_ch
|
||||
assert wake_count.load() == 1, 'Should wake exactly 1 thread'
|
||||
}
|
||||
|
||||
// Test broadcast wake-up of multiple waiting threads
|
||||
fn test_broadcast_wakeup() {
|
||||
mut mutex := sync.new_mutex()
|
||||
mut cond := sync.new_cond(mutex)
|
||||
num_threads := 5
|
||||
mut wake_counter := new_atomic(0)
|
||||
ready_ch := chan bool{cap: num_threads}
|
||||
done_ch := chan bool{cap: num_threads}
|
||||
defer {
|
||||
ready_ch.close()
|
||||
done_ch.close()
|
||||
}
|
||||
|
||||
// Spawn multiple waiting threads
|
||||
for _ in 0 .. num_threads {
|
||||
spawn fn [mut wake_counter, mut cond, mut mutex, ready_ch, done_ch] () {
|
||||
mutex.lock()
|
||||
defer {
|
||||
mutex.unlock()
|
||||
}
|
||||
ready_ch <- true // Notify readiness
|
||||
cond.wait() // Wait for broadcast
|
||||
wake_counter.add(1)
|
||||
done_ch <- true // Notify completion
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all threads to enter waiting state
|
||||
for _ in 0 .. num_threads {
|
||||
_ := <-ready_ch
|
||||
}
|
||||
|
||||
// Trigger broadcast wake-up
|
||||
mutex.lock()
|
||||
cond.broadcast()
|
||||
mutex.unlock()
|
||||
|
||||
// Verify all threads completed
|
||||
for _ in 0 .. num_threads {
|
||||
_ := <-done_ch
|
||||
}
|
||||
assert wake_counter.load() == num_threads, 'Should wake all threads'
|
||||
}
|
||||
|
||||
// Test consecutive signal delivery sequencing
|
||||
fn test_multiple_signals() {
|
||||
mut mutex := sync.new_mutex()
|
||||
mut cond := sync.new_cond(mutex)
|
||||
mut counter := new_atomic(0)
|
||||
num_signals := 3
|
||||
ready_ch := chan bool{cap: 1}
|
||||
wait_sync_ch := chan bool{cap: 1} // Synchronization for wait-sequence tracking
|
||||
done_ch := chan bool{cap: 1}
|
||||
defer {
|
||||
ready_ch.close()
|
||||
wait_sync_ch.close()
|
||||
done_ch.close()
|
||||
}
|
||||
|
||||
spawn fn [num_signals, mut counter, mut cond, mut mutex, ready_ch, wait_sync_ch, done_ch] () {
|
||||
mutex.lock()
|
||||
defer {
|
||||
mutex.unlock()
|
||||
}
|
||||
|
||||
ready_ch <- true // Initial readiness notification
|
||||
|
||||
// Process multiple signals sequentially
|
||||
for _ in 0 .. num_signals {
|
||||
cond.wait()
|
||||
counter.add(1)
|
||||
wait_sync_ch <- true // Signal processing complete
|
||||
}
|
||||
done_ch <- true
|
||||
}()
|
||||
|
||||
// Wait for initial setup
|
||||
_ := <-ready_ch
|
||||
|
||||
// Send first signal
|
||||
mutex.lock()
|
||||
cond.signal()
|
||||
mutex.unlock()
|
||||
|
||||
// Send subsequent signals with synchronization
|
||||
for _ in 1 .. num_signals {
|
||||
_ := <-wait_sync_ch // Wait for previous signal processing
|
||||
mutex.lock()
|
||||
cond.signal()
|
||||
mutex.unlock()
|
||||
}
|
||||
|
||||
_ := <-done_ch
|
||||
assert counter.load() == num_signals, 'Signal count should match counter value'
|
||||
}
|
||||
|
||||
// Test lock reacquisition mechanics after wait()
|
||||
fn test_lock_reacquire() {
|
||||
mut mutex := sync.new_mutex()
|
||||
mut cond := sync.new_cond(mutex)
|
||||
mut lock_held := new_atomic(false)
|
||||
ready_ch := chan bool{cap: 1}
|
||||
done_ch := chan bool{cap: 1}
|
||||
defer {
|
||||
ready_ch.close()
|
||||
done_ch.close()
|
||||
}
|
||||
|
||||
spawn fn [mut lock_held, mut cond, mut mutex, ready_ch, done_ch] () {
|
||||
mutex.lock()
|
||||
defer {
|
||||
mutex.unlock()
|
||||
}
|
||||
ready_ch <- true
|
||||
|
||||
cond.wait()
|
||||
// Test lock state after wakeup
|
||||
lock_held.store(!mutex.try_lock()) // Should fail -> store true
|
||||
done_ch <- true
|
||||
}()
|
||||
|
||||
_ := <-ready_ch
|
||||
|
||||
mutex.lock()
|
||||
cond.signal()
|
||||
mutex.unlock()
|
||||
|
||||
_ := <-done_ch
|
||||
assert lock_held.load(), 'Mutex should be reacquired automatically after wait()'
|
||||
}
|
||||
|
||||
// Test empty signal/broadcast scenario
|
||||
fn test_signal_without_waiters() {
|
||||
mut mutex := sync.new_mutex()
|
||||
mut cond := sync.new_cond(mutex)
|
||||
|
||||
// Verify no panic occurs
|
||||
mutex.lock()
|
||||
cond.signal() // No-op with no waiters
|
||||
cond.broadcast() // No-op with no waiters
|
||||
mutex.unlock()
|
||||
|
||||
assert true, 'Should handle empty signal operations safely'
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
struct User {
|
||||
}
|
||||
|
||||
fn test_empty_struct_chan_init() {
|
||||
user_ch := chan User{cap: 10}
|
||||
println(user_ch)
|
||||
assert true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
module sync
|
||||
|
||||
import sync.stdatomic
|
||||
|
||||
pub struct ManyTimes {
|
||||
mut:
|
||||
m RwMutex
|
||||
pub:
|
||||
times u64 = 1
|
||||
count u64
|
||||
}
|
||||
|
||||
// new_many_times return a new ManyTimes struct.
|
||||
pub fn new_many_times(times u64) &ManyTimes {
|
||||
mut many_times := &ManyTimes{
|
||||
times: times
|
||||
}
|
||||
many_times.m.init()
|
||||
return many_times
|
||||
}
|
||||
|
||||
// do execute the function only setting times.
|
||||
pub fn (mut m ManyTimes) do(f fn ()) {
|
||||
if stdatomic.load_u64(&m.count) < m.times {
|
||||
m.do_slow(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut m ManyTimes) do_slow(f fn ()) {
|
||||
m.m.lock()
|
||||
if m.count < m.times {
|
||||
stdatomic.store_u64(&m.count, m.count + 1)
|
||||
f()
|
||||
}
|
||||
m.m.unlock()
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// vtest build: !windows && (amd64 || arm64)
|
||||
import sync
|
||||
|
||||
struct Counter {
|
||||
pub mut:
|
||||
i int
|
||||
}
|
||||
|
||||
fn (mut c Counter) add(i int) {
|
||||
c.i = c.i + i
|
||||
}
|
||||
|
||||
fn run(mut m sync.ManyTimes, mut co Counter, c chan bool) {
|
||||
m.do(fn [mut co] () {
|
||||
co.add(5)
|
||||
})
|
||||
c <- true
|
||||
}
|
||||
|
||||
fn test_many_times_once() {
|
||||
mut co := &Counter{}
|
||||
mut m := sync.new_many_times(1)
|
||||
c := chan bool{}
|
||||
n := 10
|
||||
|
||||
// It is executed 10 times, but only once actually.
|
||||
for i := 0; i < n; i++ {
|
||||
spawn run(mut m, mut co, c)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
<-c
|
||||
}
|
||||
assert co.i == 5
|
||||
}
|
||||
|
||||
fn test_many_times_fifth() {
|
||||
mut co := &Counter{}
|
||||
mut m := sync.new_many_times(5)
|
||||
c := chan bool{}
|
||||
n := 10
|
||||
|
||||
// It is executed 10 times, but only 5 times actually.
|
||||
for i := 0; i < n; i++ {
|
||||
spawn run(mut m, mut co, c)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
<-c
|
||||
}
|
||||
assert co.i == 25
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import sync
|
||||
|
||||
struct Counter {
|
||||
pub mut:
|
||||
i int
|
||||
}
|
||||
|
||||
fn write_10000(mut co Counter, mut mx sync.Mutex) {
|
||||
mx.lock()
|
||||
co.i = 10000
|
||||
mx.unlock()
|
||||
}
|
||||
|
||||
fn test_mutex() {
|
||||
mut co := &Counter{10086}
|
||||
mut mx := sync.new_mutex()
|
||||
mx.lock()
|
||||
co.i = 888
|
||||
th := spawn write_10000(mut co, mut mx)
|
||||
mx.unlock() // after mx unlock, thread write_10000 can continue
|
||||
th.wait()
|
||||
mx.destroy()
|
||||
assert co.i == 10000
|
||||
}
|
||||
|
||||
fn test_try_lock_mutex() {
|
||||
// In Windows, try_lock only avalible after Windows 7
|
||||
$if windows {
|
||||
$if !windows_7 ? {
|
||||
return
|
||||
}
|
||||
}
|
||||
mut mx := sync.new_mutex()
|
||||
mx.lock()
|
||||
try_fail := mx.try_lock()
|
||||
assert try_fail == false
|
||||
mx.unlock()
|
||||
try_success := mx.try_lock()
|
||||
assert try_success == true
|
||||
mx.unlock() // you must unlock it, after try_lock success
|
||||
mx.destroy()
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
module sync
|
||||
|
||||
import sync.stdatomic
|
||||
|
||||
pub struct Once {
|
||||
mut:
|
||||
m RwMutex
|
||||
pub:
|
||||
count u64
|
||||
}
|
||||
|
||||
// new_once return a new Once struct.
|
||||
pub fn new_once() &Once {
|
||||
mut once := &Once{}
|
||||
once.m.init()
|
||||
return once
|
||||
}
|
||||
|
||||
// do executes the function `f()` only once.
|
||||
pub fn (mut o Once) do(f fn ()) {
|
||||
if stdatomic.load_u64(&o.count) < 1 {
|
||||
o.do_slow(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut o Once) do_slow(f fn ()) {
|
||||
o.m.lock()
|
||||
if o.count < 1 {
|
||||
stdatomic.store_u64(&o.count, 1)
|
||||
f()
|
||||
}
|
||||
o.m.unlock()
|
||||
}
|
||||
|
||||
// do_with_param executes `f(param)` only once.
|
||||
// This method can be used as a workaround for passing closures to once.do/1 on Windows
|
||||
// (they are not implemented there yet) - just pass your data explicitly.
|
||||
// i.e. instead of:
|
||||
// ```v
|
||||
// once.do(fn [mut o] () {
|
||||
// o.add(5)
|
||||
// })
|
||||
// ```
|
||||
//
|
||||
// ... you can use:
|
||||
// ```v
|
||||
// once.do_with_param(fn (mut o One) {
|
||||
// o.add(5)
|
||||
// }, o)
|
||||
// ```
|
||||
|
||||
// do_with_param executes the function `f()` with parameter `param` only once.
|
||||
pub fn (mut o Once) do_with_param(f fn (voidptr), param voidptr) {
|
||||
if stdatomic.load_u64(&o.count) < 1 {
|
||||
o.do_slow_with_param(f, param)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut o Once) do_slow_with_param(f fn (p voidptr), param voidptr) {
|
||||
o.m.lock()
|
||||
if o.count < 1 {
|
||||
stdatomic.store_u64(&o.count, 1)
|
||||
f(param)
|
||||
}
|
||||
o.m.unlock()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// vtest build: !windows && (amd64 || arm64)
|
||||
import sync
|
||||
|
||||
struct One {
|
||||
pub mut:
|
||||
i int
|
||||
}
|
||||
|
||||
fn (mut o One) add(i int) {
|
||||
o.i = o.i + i
|
||||
}
|
||||
|
||||
fn run(mut once sync.Once, mut o One, c chan bool) {
|
||||
once.do(fn [mut o] () {
|
||||
o.add(5)
|
||||
})
|
||||
c <- true
|
||||
}
|
||||
|
||||
fn test_once() {
|
||||
mut o := &One{}
|
||||
mut once := sync.new_once()
|
||||
c := chan bool{}
|
||||
n := 10
|
||||
|
||||
// It is executed 10 times, but only once actually.
|
||||
for i := 0; i < n; i++ {
|
||||
spawn run(mut once, mut o, c)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
<-c
|
||||
}
|
||||
assert o.i == 5
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import sync
|
||||
|
||||
// Note: this is the same test as `vlib/sync/once_test.v`, but
|
||||
// it uses an explicit passing of the voidptr parameter in
|
||||
// once.do_with_param/2, instead of passing a closure of it
|
||||
// in once.do/1.
|
||||
|
||||
struct One {
|
||||
pub mut:
|
||||
i int
|
||||
}
|
||||
|
||||
fn (mut o One) add(i int) {
|
||||
o.i = o.i + i
|
||||
}
|
||||
|
||||
fn run(mut once sync.Once, mut o One, c chan bool) {
|
||||
once.do_with_param(fn (mut o One) {
|
||||
o.add(5)
|
||||
}, o)
|
||||
c <- true
|
||||
}
|
||||
|
||||
fn test_once() {
|
||||
mut o := &One{}
|
||||
mut once := sync.new_once()
|
||||
c := chan bool{}
|
||||
n := 10
|
||||
|
||||
// It is executed 10 times, but only once actually.
|
||||
for i := 0; i < n; i++ {
|
||||
spawn run(mut once, mut o, c)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
<-c
|
||||
}
|
||||
assert o.i == 5
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
The `sync.pool` module provides a convenient way to run identical tasks over
|
||||
an array of items *in parallel*, without worrying about thread synchronization,
|
||||
waitgroups, mutexes etc.., you just need to supply a callback function, that
|
||||
will be called once per each item in your input array.
|
||||
|
||||
After all the work is done in parallel by the worker threads in the pool,
|
||||
pool.work_on_items will return. You can then call pool.get_results[Result]()
|
||||
to retrieve a list of all the results, that the worker callbacks returned
|
||||
for each input item. Example:
|
||||
|
||||
```v
|
||||
import sync.pool
|
||||
|
||||
pub struct SResult {
|
||||
s string
|
||||
}
|
||||
|
||||
fn sprocess(mut pp pool.PoolProcessor, idx int, wid int) &SResult {
|
||||
item := pp.get_item[string](idx)
|
||||
println('idx: ${idx}, wid: ${wid}, item: ' + item)
|
||||
return &SResult{item.reverse()}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut pp := pool.new_pool_processor(callback: sprocess)
|
||||
pp.work_on_items(['1abc', '2abc', '3abc', '4abc', '5abc', '6abc', '7abc'])
|
||||
// optionally, you can iterate over the results too:
|
||||
for x in pp.get_results[SResult]() {
|
||||
println('result: ${x.s}')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See https://github.com/vlang/v/blob/master/vlib/sync/pool/pool_test.v for a
|
||||
more detailed usage example.
|
||||
@@ -0,0 +1,191 @@
|
||||
module pool
|
||||
|
||||
import sync
|
||||
import runtime
|
||||
|
||||
@[trusted]
|
||||
fn C.atomic_fetch_add_u32(voidptr, u32) u32
|
||||
|
||||
pub const no_result = unsafe { nil }
|
||||
|
||||
pub struct PoolProcessor {
|
||||
thread_cb voidptr
|
||||
mut:
|
||||
njobs int
|
||||
items []voidptr
|
||||
results shared []voidptr
|
||||
ntask u32 // reading/writing to this should be atomic
|
||||
waitgroup sync.WaitGroup
|
||||
shared_context voidptr
|
||||
thread_contexts []voidptr
|
||||
}
|
||||
|
||||
pub type ThreadCB = fn (mut p PoolProcessor, idx int, task_id int) voidptr
|
||||
|
||||
fn empty_cb(mut _p PoolProcessor, _idx int, _task_id int) voidptr {
|
||||
unsafe {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolProcessorConfig {
|
||||
pub:
|
||||
maxjobs int
|
||||
callback ThreadCB = empty_cb
|
||||
}
|
||||
|
||||
// new_pool_processor returns a new PoolProcessor instance.
|
||||
// The parameters of new_pool_processor are:
|
||||
// context.maxjobs: when 0 (the default), the PoolProcessor will use a
|
||||
// number of threads, that is optimal for your system to process your items.
|
||||
// context.callback: this should be a callback function, that each worker
|
||||
// thread in the pool will run for each item.
|
||||
// The callback function will receive as parameters:
|
||||
// 1) the PoolProcessor instance, so it can call
|
||||
// p.get_item[int](idx) to get the actual item at index idx
|
||||
// 2) idx - the index of the currently processed item
|
||||
// 3) task_id - the index of the worker thread in which the callback
|
||||
// function is running.
|
||||
pub fn new_pool_processor(context PoolProcessorConfig) &PoolProcessor {
|
||||
if context.callback == unsafe { nil } {
|
||||
panic('You need to pass a valid callback to new_pool_processor.')
|
||||
}
|
||||
mut pool := PoolProcessor{
|
||||
items: []
|
||||
results: []
|
||||
shared_context: unsafe { nil }
|
||||
thread_contexts: []
|
||||
njobs: context.maxjobs
|
||||
ntask: 0
|
||||
thread_cb: voidptr(context.callback)
|
||||
}
|
||||
pool.waitgroup.init()
|
||||
return &pool
|
||||
}
|
||||
|
||||
// set_max_jobs gives you the ability to override the number
|
||||
// of jobs *after* the PoolProcessor had been created already.
|
||||
pub fn (mut pool PoolProcessor) set_max_jobs(njobs int) {
|
||||
pool.njobs = njobs
|
||||
}
|
||||
|
||||
// work_on_items receives a list of items of type T,
|
||||
// then starts a work pool of pool.njobs threads, each running
|
||||
// pool.thread_cb in a loop, until all items in the list,
|
||||
// are processed.
|
||||
// When pool.njobs is 0, the number of jobs is determined
|
||||
// by the number of available cores on the system.
|
||||
// work_on_items returns *after* all threads finish.
|
||||
// You can optionally call get_results after that.
|
||||
pub fn (mut pool PoolProcessor) work_on_items[T](items []T) {
|
||||
pool.work_on_pointers(unsafe { items.pointers() })
|
||||
}
|
||||
|
||||
pub fn (mut pool PoolProcessor) work_on_pointers(items []voidptr) {
|
||||
mut njobs := runtime.nr_jobs()
|
||||
if pool.njobs > 0 {
|
||||
njobs = pool.njobs
|
||||
}
|
||||
unsafe {
|
||||
pool.ntask = 0
|
||||
pool.thread_contexts = []voidptr{len: items.len}
|
||||
lock pool.results {
|
||||
pool.results = []voidptr{len: items.len}
|
||||
}
|
||||
pool.items = []voidptr{cap: items.len}
|
||||
pool.items << items
|
||||
pool.waitgroup.add(njobs)
|
||||
for i := 0; i < njobs; i++ {
|
||||
if njobs > 1 {
|
||||
spawn process_in_thread(mut pool, i)
|
||||
} else {
|
||||
// do not run concurrently, just use the same thread:
|
||||
process_in_thread(mut pool, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
pool.waitgroup.wait()
|
||||
}
|
||||
|
||||
// process_in_thread does the actual work of worker thread.
|
||||
// It is a workaround for the current inability to pass a
|
||||
// method in a callback.
|
||||
fn process_in_thread(mut pool PoolProcessor, task_id int) {
|
||||
cb := ThreadCB(pool.thread_cb)
|
||||
ilen := pool.items.len
|
||||
for {
|
||||
idx := int(C.atomic_fetch_add_u32(voidptr(&pool.ntask), 1))
|
||||
if idx >= ilen {
|
||||
break
|
||||
}
|
||||
res := cb(mut pool, idx, task_id)
|
||||
lock pool.results {
|
||||
pool.results[idx] = res
|
||||
}
|
||||
}
|
||||
pool.waitgroup.done()
|
||||
}
|
||||
|
||||
// get_item - called by the worker callback.
|
||||
// Retrieves a type safe instance of the currently processed item
|
||||
pub fn (pool &PoolProcessor) get_item[T](idx int) T {
|
||||
return unsafe { *(&T(pool.items[idx])) }
|
||||
}
|
||||
|
||||
// get_result - called by the main thread to get a specific result.
|
||||
// Retrieves a type safe instance of the produced result.
|
||||
pub fn (pool &PoolProcessor) get_result[T](idx int) T {
|
||||
rlock pool.results {
|
||||
return unsafe { *(&T(pool.results[idx])) }
|
||||
}
|
||||
}
|
||||
|
||||
// get_results - get a list of type safe results in the main thread.
|
||||
pub fn (pool &PoolProcessor) get_results[T]() []T {
|
||||
mut res := []T{cap: pool.results.len}
|
||||
for i in 0 .. pool.results.len {
|
||||
rlock pool.results {
|
||||
res << unsafe { *(&T(pool.results[i])) }
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// get_results_ref - get a list of type safe results in the main thread.
|
||||
pub fn (pool &PoolProcessor) get_results_ref[T]() []&T {
|
||||
mut res := []&T{cap: pool.results.len}
|
||||
for i in 0 .. pool.results.len {
|
||||
rlock pool.results {
|
||||
res << unsafe { &T(pool.results[i]) }
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// set_shared_context - can be called during the setup so that you can
|
||||
// provide a context that is shared between all worker threads, like
|
||||
// common options/settings.
|
||||
pub fn (mut pool PoolProcessor) set_shared_context(context voidptr) {
|
||||
pool.shared_context = context
|
||||
}
|
||||
|
||||
// get_shared_context - can be called in each worker callback, to get
|
||||
// the context set by pool.set_shared_context
|
||||
pub fn (pool &PoolProcessor) get_shared_context() voidptr {
|
||||
return pool.shared_context
|
||||
}
|
||||
|
||||
// set_thread_context - can be called during the setup at the start of
|
||||
// each worker callback, so that the worker callback can have some thread
|
||||
// local storage area where it can write/read information that is private
|
||||
// to the given thread, without worrying that it will get overwritten by
|
||||
// another thread
|
||||
pub fn (mut pool PoolProcessor) set_thread_context(idx int, context voidptr) {
|
||||
pool.thread_contexts[idx] = context
|
||||
}
|
||||
|
||||
// get_thread_context - returns a pointer, that was set with
|
||||
// pool.set_thread_context . This pointer is private to each thread.
|
||||
pub fn (pool &PoolProcessor) get_thread_context(idx int) voidptr {
|
||||
return pool.thread_contexts[idx]
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import time
|
||||
import sync
|
||||
import sync.pool
|
||||
|
||||
pub struct SResult {
|
||||
s string
|
||||
}
|
||||
|
||||
pub struct IResult {
|
||||
i int
|
||||
}
|
||||
|
||||
struct SeenContext {
|
||||
mut:
|
||||
mutex &sync.Mutex = sync.new_mutex()
|
||||
seen []int
|
||||
}
|
||||
|
||||
fn worker_s(mut p pool.PoolProcessor, idx int, worker_id int) &SResult {
|
||||
item := p.get_item[string](idx)
|
||||
println('worker_s worker_id: ${worker_id} | idx: ${idx} | item: ${item}')
|
||||
time.sleep(3 * time.millisecond)
|
||||
return &SResult{'${item} ${item}'}
|
||||
}
|
||||
|
||||
fn worker_i(mut p pool.PoolProcessor, idx int, worker_id int) &IResult {
|
||||
item := p.get_item[int](idx)
|
||||
println('worker_i worker_id: ${worker_id} | idx: ${idx} | item: ${item}')
|
||||
time.sleep(5 * time.millisecond)
|
||||
return &IResult{item * 1000}
|
||||
}
|
||||
|
||||
fn worker_reuse(mut p pool.PoolProcessor, idx int, _ int) voidptr {
|
||||
item := p.get_item[int](idx)
|
||||
mut ctx := unsafe { &SeenContext(p.get_shared_context()) }
|
||||
ctx.mutex.lock()
|
||||
ctx.seen << item
|
||||
ctx.mutex.unlock()
|
||||
return pool.no_result
|
||||
}
|
||||
|
||||
fn test_work_on_strings() {
|
||||
mut pool_s := pool.new_pool_processor(
|
||||
callback: worker_s
|
||||
maxjobs: 8
|
||||
)
|
||||
|
||||
pool_s.work_on_items(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'])
|
||||
for x in pool_s.get_results[SResult]() {
|
||||
println(x.s)
|
||||
assert x.s.len > 1
|
||||
}
|
||||
println('---------- pool_s.get_results_ref: --------------')
|
||||
for x in pool_s.get_results_ref[SResult]() {
|
||||
println(x.s)
|
||||
assert x.s.len > 1
|
||||
}
|
||||
}
|
||||
|
||||
fn test_work_on_ints() {
|
||||
// Note: since maxjobs is left empty here,
|
||||
// the pool processor will use njobs = runtime.nr_jobs so that
|
||||
// it will work optimally without overloading the system
|
||||
mut pool_i := pool.new_pool_processor(
|
||||
callback: worker_i
|
||||
)
|
||||
|
||||
pool_i.work_on_items([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
for x in pool_i.get_results[IResult]() {
|
||||
println(x.i)
|
||||
assert x.i > 100
|
||||
}
|
||||
println('---------- pool_i.get_results_ref: --------------')
|
||||
for x in pool_i.get_results_ref[IResult]() {
|
||||
println(x.i)
|
||||
assert x.i > 100
|
||||
}
|
||||
}
|
||||
|
||||
fn test_pool_can_be_reused() {
|
||||
mut ctx := &SeenContext{}
|
||||
mut pool_i := pool.new_pool_processor(
|
||||
callback: worker_reuse
|
||||
maxjobs: 2
|
||||
)
|
||||
pool_i.set_shared_context(ctx)
|
||||
pool_i.work_on_items([1, 2, 3])
|
||||
ctx.mutex.lock()
|
||||
mut first_seen := ctx.seen.clone()
|
||||
ctx.seen = []int{}
|
||||
ctx.mutex.unlock()
|
||||
first_seen.sort()
|
||||
assert first_seen == [1, 2, 3]
|
||||
pool_i.work_on_items([4, 5])
|
||||
ctx.mutex.lock()
|
||||
mut second_seen := ctx.seen.clone()
|
||||
ctx.mutex.unlock()
|
||||
second_seen.sort()
|
||||
assert second_seen == [4, 5]
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import sync
|
||||
import time
|
||||
|
||||
struct Counter {
|
||||
pub mut:
|
||||
i int
|
||||
}
|
||||
|
||||
fn write_10000(mut co Counter, mut mx sync.RwMutex) {
|
||||
mx.lock()
|
||||
co.i = 10000
|
||||
mx.unlock()
|
||||
}
|
||||
|
||||
fn test_rwmutex() {
|
||||
mut co := &Counter{10086}
|
||||
mut mx := sync.new_rwmutex()
|
||||
mx.lock()
|
||||
co.i = 888
|
||||
th1 := spawn write_10000(mut co, mut mx)
|
||||
mx.unlock() // after mx unlock, thread write_10000 can continue
|
||||
th1.wait()
|
||||
assert co.i == 10000
|
||||
|
||||
mx.rlock()
|
||||
th2 := spawn write_10000(mut co, mut mx) // write_10000 will be blocked
|
||||
co.i = 999 // for demo purpose, don't modify data in rlock!
|
||||
time.sleep(1 * time.millisecond)
|
||||
assert co.i == 999
|
||||
mx.runlock() // after rlock released, write_10000 can continue
|
||||
th2.wait()
|
||||
assert co.i == 10000
|
||||
mx.destroy()
|
||||
}
|
||||
|
||||
fn test_try_lock_rwmutex() {
|
||||
// In Windows, try_lock only avalible after Windows 7
|
||||
$if windows {
|
||||
$if !windows_7 ? {
|
||||
return
|
||||
}
|
||||
}
|
||||
mut mx := sync.new_rwmutex()
|
||||
|
||||
// try_rlock will always fail when mx locked
|
||||
mx.lock()
|
||||
try_fail_reading1 := mx.try_rlock()
|
||||
try_fail_writing1 := mx.try_wlock()
|
||||
assert try_fail_reading1 == false
|
||||
assert try_fail_writing1 == false
|
||||
|
||||
mx.unlock()
|
||||
|
||||
// try_rlock will always succeed when mx unlocked,
|
||||
// multiple try_rlock can apply to the same mx
|
||||
try_success_reading2 := mx.try_rlock()
|
||||
try_success_reading3 := mx.try_rlock()
|
||||
assert try_success_reading2 == true
|
||||
assert try_success_reading3 == true
|
||||
|
||||
// if mx is rlocked, then the try_wlock will fail
|
||||
try_fail_writing2 := mx.try_wlock()
|
||||
assert try_fail_writing2 == false
|
||||
|
||||
mx.runlock()
|
||||
mx.runlock() // you must release rlock multiple times, as it was rlocked multiple times
|
||||
|
||||
// after mx release all rlock, try_wlock will succeed
|
||||
try_success_writing3 := mx.try_wlock()
|
||||
assert try_success_writing3 == true
|
||||
|
||||
mx.unlock() // you must unlock it, after try_wlock success
|
||||
mx.destroy()
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// vtest retry: 3
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
fn do_rec_i64(mut ch Channel) {
|
||||
mut sum := i64(0)
|
||||
for i in 0 .. 300 {
|
||||
if i == 200 {
|
||||
ch.close()
|
||||
}
|
||||
mut a := i64(0)
|
||||
if ch.pop(&a) {
|
||||
sum += a
|
||||
}
|
||||
}
|
||||
assert sum == 200 * (200 - 1) / 2
|
||||
}
|
||||
|
||||
fn do_send_int(mut ch Channel) {
|
||||
for i in 0 .. 300 {
|
||||
ch.push(&i)
|
||||
}
|
||||
ch.close()
|
||||
}
|
||||
|
||||
fn do_send_u8(mut ch Channel) {
|
||||
for i in 0 .. 300 {
|
||||
ii := u8(i)
|
||||
ch.push(&ii)
|
||||
}
|
||||
ch.close()
|
||||
}
|
||||
|
||||
fn do_send_i64(mut ch Channel) {
|
||||
for i in 0 .. 300 {
|
||||
ii := i64(i)
|
||||
ch.push(&ii)
|
||||
}
|
||||
ch.close()
|
||||
}
|
||||
|
||||
fn test_select() {
|
||||
mut chi := new_channel[int](0)
|
||||
mut chl := new_channel[i64](1)
|
||||
mut chb := new_channel[u8](10)
|
||||
mut recch := new_channel[i64](0)
|
||||
spawn do_rec_i64(mut recch)
|
||||
spawn do_send_int(mut chi)
|
||||
spawn do_send_u8(mut chb)
|
||||
spawn do_send_i64(mut chl)
|
||||
mut channels := [chi, recch, chl, chb]
|
||||
directions := [Direction.pop, .push, .pop, .pop]
|
||||
mut sum := i64(0)
|
||||
mut rl := i64(0)
|
||||
mut ri := int(0)
|
||||
mut rb := u8(0)
|
||||
mut sl := i64(0)
|
||||
mut objs := [voidptr(&ri), &sl, &rl, &rb]
|
||||
for j in 0 .. 1101 {
|
||||
idx := channel_select(mut channels, directions, mut objs, time.infinite)
|
||||
match idx {
|
||||
0 {
|
||||
sum += ri
|
||||
}
|
||||
1 {
|
||||
sl++
|
||||
}
|
||||
2 {
|
||||
sum += rl
|
||||
}
|
||||
3 {
|
||||
sum += rb
|
||||
}
|
||||
-2 {
|
||||
// channel was closed - last item
|
||||
assert j == 1100
|
||||
}
|
||||
else {
|
||||
println('got ${idx} (timeout)')
|
||||
assert false
|
||||
}
|
||||
}
|
||||
|
||||
if j == 1100 {
|
||||
// check also in other direction
|
||||
assert idx == -2
|
||||
}
|
||||
}
|
||||
// Use Gauß' formula for the first 2 contributions
|
||||
// the 3rd contribution is `byte` and must be seen modulo 256
|
||||
expected_sum := 2 * (300 * (300 - 1) / 2) + 256 * (256 - 1) / 2 + 44 * (44 - 1) / 2
|
||||
assert sum == expected_sum
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import sync
|
||||
import rand
|
||||
import time
|
||||
|
||||
fn test_spinlock() {
|
||||
mut counter := 0
|
||||
mut s := sync.new_spin_lock()
|
||||
defer {
|
||||
s.destroy()
|
||||
}
|
||||
num_threads := 10
|
||||
iterations := 10
|
||||
mut wg := sync.new_waitgroup()
|
||||
wg.add(num_threads)
|
||||
|
||||
for _ in 0 .. num_threads {
|
||||
spawn fn (mut wg sync.WaitGroup, s &sync.SpinLock, counter_ref &int, iterations int) {
|
||||
for _ in 0 .. iterations {
|
||||
s.lock()
|
||||
|
||||
unsafe {
|
||||
tmp := *counter_ref
|
||||
randval := rand.intn(100) or { 1 }
|
||||
time.sleep(randval * time.nanosecond)
|
||||
|
||||
(*counter_ref) = tmp + 1
|
||||
}
|
||||
s.unlock()
|
||||
}
|
||||
wg.done()
|
||||
}(mut wg, s, &counter, iterations)
|
||||
}
|
||||
wg.wait()
|
||||
assert counter == num_threads * iterations
|
||||
|
||||
// test try_lock()
|
||||
s.lock()
|
||||
assert s.try_lock() == false
|
||||
s.unlock()
|
||||
assert s.try_lock() == true
|
||||
assert s.try_lock() == false
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
module stdatomic
|
||||
|
||||
// The @VEXEROOT/thirdparty/stdatomic contains compatibility headers
|
||||
// for stdatomic, that supports both nix, windows and c++.
|
||||
|
||||
$if windows {
|
||||
#flag -I @VEXEROOT/thirdparty/stdatomic/win
|
||||
#insert "@VEXEROOT/thirdparty/stdatomic/win/atomic.h"
|
||||
} $else {
|
||||
#flag -I @VEXEROOT/thirdparty/stdatomic/nix
|
||||
#insert "@VEXEROOT/thirdparty/stdatomic/nix/atomic.h"
|
||||
}
|
||||
|
||||
$if linux {
|
||||
$if tinyc {
|
||||
$if amd64 {
|
||||
// Redhat/CentOS:
|
||||
#flag $when_first_existing('/usr/lib/gcc/x86_64-redhat-linux/6/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/7/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/8/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/9/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/10/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/11/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/12/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/13/libatomic.a','/usr/lib/gcc/x86_64-redhat-linux/14/libatomic.a')
|
||||
// Gentoo:
|
||||
#flag $when_first_existing('/usr/lib/gcc/x86_64-pc-linux-gnu/6/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/7/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/8/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/9/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/10/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/11/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/12/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/13/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-gnu/14/libatomic.a')
|
||||
// OpenSUSE:
|
||||
#flag $when_first_existing('/usr/lib64/gcc/x86_64-suse-linux/6/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/7/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/8/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/9/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/10/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/11/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/12/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/13/libatomic.a','/usr/lib64/gcc/x86_64-suse-linux/14/libatomic.a')
|
||||
// ALT Linux:
|
||||
#flag $when_first_existing('/usr/lib64/gcc/x86_64-alt-linux/6/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/7/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/8/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/9/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/10/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/11/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/12/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/13/libatomic.a','/usr/lib64/gcc/x86_64-alt-linux/14/libatomic.a')
|
||||
$if musl ? {
|
||||
// Alpine:
|
||||
#flag $when_first_existing('/usr/lib/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/6/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/7/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/8/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/9/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/10/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/11/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/12/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/13/libatomic.a','/usr/lib/gcc/x86_64-pc-linux-musl/14/libatomic.a')
|
||||
#flag @VEXEROOT/thirdparty/stdatomic/nix/atomic.S
|
||||
}
|
||||
} $else $if arm64 {
|
||||
// Note: Use `.so` instead of `.a`.
|
||||
// This is because `libatomic.a` atomic symbols, such as `__atomic_fetch_add_4`, are indirect(IFUNC) symbols:
|
||||
// This can not be handled correctly by `tcc` right now.
|
||||
|
||||
// Symbol table '.symtab' contains 14 entries:
|
||||
// Num: Value Size Type Bind Vis Ndx Name
|
||||
// 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
|
||||
// 1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
|
||||
// 2: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 $x
|
||||
// 3: 0000000000000030 12 FUNC LOCAL DEFAULT 1 select_fetch_add_4
|
||||
// 4: 0000000000000040 12 FUNC LOCAL DEFAULT 1 select_add_fetch_4
|
||||
// 5: 0000000000000014 0 NOTYPE LOCAL DEFAULT 6 $d
|
||||
// 6: 0000000000000000 0 SECTION LOCAL DEFAULT 3
|
||||
// 7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
|
||||
// 8: 0000000000000000 0 SECTION LOCAL DEFAULT 5
|
||||
// 9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
|
||||
// 10: 0000000000000000 24 FUNC GLOBAL HIDDEN 1 libat_fetch_add_4
|
||||
// 11: 0000000000000018 24 FUNC GLOBAL HIDDEN 1 libat_add_fetch_4
|
||||
// 12: 0000000000000030 12 IFUNC GLOBAL DEFAULT 1 __atomic_fetch_add_4
|
||||
// 13: 0000000000000040 12 IFUNC GLOBAL DEFAULT 1 __atomic_add_fetch_4
|
||||
|
||||
// Debian/Ubuntu:
|
||||
#flag $when_first_existing('/usr/lib/gcc/aarch64-linux-gnu/6/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/7/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/8/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/9/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/10/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/11/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/12/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/13/libatomic.so','/usr/lib/gcc/aarch64-linux-gnu/14/libatomic.so')
|
||||
// Redhat/CentOS:
|
||||
#flag $when_first_existing('/usr/lib/gcc/aarch64-redhat-linux/6/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/7/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/8/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/9/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/10/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/11/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/12/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/13/libatomic.so','/usr/lib/gcc/aarch64-redhat-linux/14/libatomic.so')
|
||||
// Gentoo:
|
||||
#flag $when_first_existing('/usr/lib/gcc/aarch64-pc-linux-gnu/6/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/7/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/8/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/9/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/10/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/11/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/12/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/13/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-gnu/14/libatomic.so')
|
||||
// OpenSUSE:
|
||||
#flag $when_first_existing('/usr/lib64/gcc/aarch64-suse-linux/6/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/7/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/8/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/9/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/10/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/11/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/12/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/13/libatomic.so','/usr/lib64/gcc/aarch64-suse-linux/14/libatomic.so')
|
||||
// ALT Linux:
|
||||
#flag $when_first_existing('/usr/lib64/gcc/aarch64-alt-linux/6/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/7/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/8/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/9/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/10/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/11/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/12/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/13/libatomic.so','/usr/lib64/gcc/aarch64-alt-linux/14/libatomic.so')
|
||||
$if musl ? {
|
||||
// Alpine:
|
||||
#flag $when_first_existing('/usr/lib/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/6/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/7/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/8/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/9/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/10/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/11/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/12/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/13/libatomic.so','/usr/lib/gcc/aarch64-pc-linux-musl/14/libatomic.so')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The following functions are actually generic in C
|
||||
fn C.atomic_load_ptr(voidptr) voidptr
|
||||
fn C.atomic_store_ptr(voidptr, voidptr)
|
||||
fn C.atomic_compare_exchange_weak_ptr(voidptr, voidptr, isize) bool
|
||||
fn C.atomic_compare_exchange_strong_ptr(voidptr, voidptr, isize) bool
|
||||
fn C.atomic_exchange_ptr(voidptr, voidptr) voidptr
|
||||
fn C.atomic_fetch_add_ptr(voidptr, voidptr) voidptr
|
||||
fn C.atomic_fetch_sub_ptr(voidptr, voidptr) voidptr
|
||||
|
||||
fn C.atomic_load_byte(voidptr) u8
|
||||
fn C.atomic_store_byte(voidptr, u8)
|
||||
fn C.atomic_compare_exchange_weak_byte(voidptr, voidptr, u8) bool
|
||||
fn C.atomic_compare_exchange_strong_byte(voidptr, voidptr, u8) bool
|
||||
fn C.atomic_exchange_byte(voidptr, u8) u8
|
||||
fn C.atomic_fetch_add_byte(voidptr, u8) u8
|
||||
fn C.atomic_fetch_sub_byte(voidptr, u8) u8
|
||||
|
||||
fn C.atomic_load_u16(voidptr) u16
|
||||
fn C.atomic_store_u16(voidptr, u16)
|
||||
fn C.atomic_compare_exchange_weak_u16(voidptr, voidptr, u16) bool
|
||||
fn C.atomic_compare_exchange_strong_u16(voidptr, voidptr, u16) bool
|
||||
fn C.atomic_exchange_u16(voidptr, u16) u16
|
||||
fn C.atomic_fetch_add_u16(voidptr, u16) u16
|
||||
fn C.atomic_fetch_sub_u16(voidptr, u16) u16
|
||||
|
||||
fn C.atomic_load_u32(voidptr) u32
|
||||
fn C.atomic_store_u32(voidptr, u32)
|
||||
fn C.atomic_compare_exchange_weak_u32(voidptr, voidptr, u32) bool
|
||||
fn C.atomic_compare_exchange_strong_u32(voidptr, voidptr, u32) bool
|
||||
fn C.atomic_exchange_u32(voidptr, u32) u32
|
||||
fn C.atomic_fetch_add_u32(voidptr, u32) u32
|
||||
fn C.atomic_fetch_sub_u32(voidptr, u32) u32
|
||||
|
||||
fn C.atomic_load_u64(voidptr) u64
|
||||
fn C.atomic_store_u64(voidptr, u64)
|
||||
fn C.atomic_compare_exchange_weak_u64(voidptr, voidptr, u64) bool
|
||||
fn C.atomic_compare_exchange_strong_u64(voidptr, voidptr, u64) bool
|
||||
fn C.atomic_exchange_u64(voidptr, u64) u64
|
||||
fn C.atomic_fetch_add_u64(voidptr, u64) u64
|
||||
fn C.atomic_fetch_sub_u64(voidptr, u64) u64
|
||||
|
||||
fn C.atomic_thread_fence(i32)
|
||||
fn C.cpu_relax()
|
||||
|
||||
pub const C.memory_order_relaxed i32
|
||||
pub const C.memory_order_acquire i32
|
||||
pub const C.memory_order_release i32
|
||||
|
||||
fn C.ANNOTATE_RWLOCK_CREATE(voidptr)
|
||||
fn C.ANNOTATE_RWLOCK_ACQUIRED(voidptr, i32)
|
||||
fn C.ANNOTATE_RWLOCK_RELEASED(voidptr, i32)
|
||||
fn C.ANNOTATE_RWLOCK_DESTROY(voidptr)
|
||||
|
||||
$if valgrind ? {
|
||||
#flag -I/usr/include/valgrind
|
||||
#include <valgrind/helgrind.h>
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
module stdatomic
|
||||
|
||||
// Implement the atomic operations. For now TCC does not support the atomic
|
||||
// versions on nix so it uses locks to simulate the same behavior.
|
||||
//
|
||||
// On windows tcc can simulate with other atomic operations.
|
||||
//
|
||||
// Note: this implementations should be regarded as alpha stage and be tested
|
||||
// much more.
|
||||
|
||||
// add_u64 adds provided delta as an atomic operation
|
||||
@[inline]
|
||||
pub fn add_u64(ptr &u64, delta int) u64 {
|
||||
C.atomic_fetch_add_u64(voidptr(ptr), delta)
|
||||
return *ptr
|
||||
}
|
||||
|
||||
// sub_u64 subtracts provided delta as an atomic operation
|
||||
@[inline]
|
||||
pub fn sub_u64(ptr &u64, delta int) u64 {
|
||||
C.atomic_fetch_sub_u64(voidptr(ptr), delta)
|
||||
return *ptr
|
||||
}
|
||||
|
||||
// add_i64 adds provided delta as an atomic operation
|
||||
@[inline]
|
||||
pub fn add_i64(ptr &i64, delta int) i64 {
|
||||
C.atomic_fetch_add_u64(voidptr(ptr), delta)
|
||||
return *ptr
|
||||
}
|
||||
|
||||
// add_i64 subtracts provided delta as an atomic operation
|
||||
@[inline]
|
||||
pub fn sub_i64(ptr &i64, delta int) i64 {
|
||||
C.atomic_fetch_sub_u64(voidptr(ptr), delta)
|
||||
return *ptr
|
||||
}
|
||||
|
||||
// atomic store/load operations have to be used when there might be another concurrent access
|
||||
// atomicall set a value
|
||||
@[inline]
|
||||
pub fn store_u64(ptr &u64, val u64) {
|
||||
C.atomic_store_u64(voidptr(ptr), val)
|
||||
}
|
||||
|
||||
// atomicall get a value
|
||||
@[inline]
|
||||
pub fn load_u64(ptr &u64) u64 {
|
||||
return C.atomic_load_u64(voidptr(ptr))
|
||||
}
|
||||
|
||||
// atomicall set a value
|
||||
@[inline]
|
||||
pub fn store_i64(ptr &i64, val i64) {
|
||||
C.atomic_store_u64(voidptr(ptr), val)
|
||||
}
|
||||
|
||||
// atomicall get a value
|
||||
@[inline]
|
||||
pub fn load_i64(ptr &i64) i64 {
|
||||
return i64(C.atomic_load_u64(voidptr(ptr)))
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct AtomicVal[T] {
|
||||
val T
|
||||
}
|
||||
|
||||
// new_atomic creates a new atomic value of `T` type
|
||||
@[inline]
|
||||
pub fn new_atomic[T](val T) &AtomicVal[T] {
|
||||
$if T is $int {
|
||||
return &AtomicVal[T]{
|
||||
val: val
|
||||
}
|
||||
} $else $if T is bool {
|
||||
return &AtomicVal[T]{
|
||||
val: val
|
||||
}
|
||||
} $else $if T is voidptr {
|
||||
return &AtomicVal[T]{
|
||||
val: val
|
||||
}
|
||||
} $else {
|
||||
$compile_error('atomic: only support number, bool, and voidptr types')
|
||||
}
|
||||
return unsafe { nil }
|
||||
}
|
||||
|
||||
// load returns current value of the atomic value
|
||||
@[inline]
|
||||
pub fn (mut a AtomicVal[T]) load() T {
|
||||
$if T is bool {
|
||||
return C.atomic_load_byte(voidptr(&a.val)) != 0
|
||||
} $else $if T is u8 || T is i8 {
|
||||
return T(C.atomic_load_byte(voidptr(&a.val)))
|
||||
} $else $if T is u16 || T is i16 {
|
||||
return T(C.atomic_load_u16(voidptr(&a.val)))
|
||||
} $else $if T is u32 || T is i32 {
|
||||
return T(C.atomic_load_u32(voidptr(&a.val)))
|
||||
} $else $if T is u64 || T is i64 {
|
||||
return T(C.atomic_load_u64(voidptr(&a.val)))
|
||||
} $else $if T is int {
|
||||
if sizeof(int) == 4 {
|
||||
return int(C.atomic_load_u32(voidptr(&a.val)))
|
||||
} else {
|
||||
return int(C.atomic_load_u64(voidptr(&a.val)))
|
||||
}
|
||||
} $else $if T is isize || T is usize {
|
||||
if sizeof(isize) == 4 {
|
||||
return T(C.atomic_load_u32(voidptr(&a.val)))
|
||||
} else {
|
||||
return T(C.atomic_load_u64(voidptr(&a.val)))
|
||||
}
|
||||
} $else $if T is voidptr {
|
||||
// TODO: this should be $if sizeof(T) == 4
|
||||
$if x32 {
|
||||
return T(C.atomic_load_u32(voidptr(&a.val)))
|
||||
} $else {
|
||||
return T(C.atomic_load_u64(voidptr(&a.val)))
|
||||
}
|
||||
}
|
||||
return a.val
|
||||
}
|
||||
|
||||
// store updates the atomic value with `val`
|
||||
@[inline]
|
||||
pub fn (mut a AtomicVal[T]) store(val T) {
|
||||
$if T is bool {
|
||||
C.atomic_store_byte(voidptr(&a.val), u8(val))
|
||||
} $else $if T is u8 || T is i8 {
|
||||
C.atomic_store_byte(voidptr(&a.val), u8(val))
|
||||
} $else $if T is u16 || T is i16 {
|
||||
C.atomic_store_u16(voidptr(&a.val), u16(val))
|
||||
} $else $if T is u32 || T is i32 {
|
||||
C.atomic_store_u32(voidptr(&a.val), u32(val))
|
||||
} $else $if T is u64 || T is i64 {
|
||||
C.atomic_store_u64(voidptr(&a.val), u64(val))
|
||||
} $else $if T is int {
|
||||
if sizeof(int) == 4 {
|
||||
C.atomic_store_u32(voidptr(&a.val), u32(val))
|
||||
} else {
|
||||
C.atomic_store_u64(voidptr(&a.val), u64(val))
|
||||
}
|
||||
} $else $if T is isize || T is usize {
|
||||
if sizeof(isize) == 4 {
|
||||
C.atomic_store_u32(voidptr(&a.val), u32(val))
|
||||
} else {
|
||||
C.atomic_store_u64(voidptr(&a.val), u64(val))
|
||||
}
|
||||
} $else $if T is voidptr {
|
||||
// TODO: this should be $if sizeof(T) == 4
|
||||
$if x32 {
|
||||
C.atomic_store_u32(voidptr(&a.val), u32(val))
|
||||
} $else {
|
||||
C.atomic_store_u64(voidptr(&a.val), u64(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add adds the atomic value with `delta` and returns the previous value
|
||||
@[inline]
|
||||
pub fn (mut a AtomicVal[T]) add(delta T) T {
|
||||
$if T is bool {
|
||||
panic('atomic: add() not supported for bool type')
|
||||
} $else $if T is voidptr {
|
||||
panic('atomic: add() not supported for voidptr type')
|
||||
} $else $if T is u8 || T is i8 {
|
||||
old := C.atomic_fetch_add_byte(voidptr(&a.val), u8(delta))
|
||||
return T(old)
|
||||
} $else $if T is u16 || T is i16 {
|
||||
old := C.atomic_fetch_add_u16(voidptr(&a.val), u16(delta))
|
||||
return T(old)
|
||||
} $else $if T is u32 || T is i32 {
|
||||
old := C.atomic_fetch_add_u32(voidptr(&a.val), u32(delta))
|
||||
return T(old)
|
||||
} $else $if T is u64 || T is i64 {
|
||||
old := C.atomic_fetch_add_u64(voidptr(&a.val), u64(delta))
|
||||
return T(old)
|
||||
} $else $if T is int {
|
||||
if sizeof(int) == 4 {
|
||||
old := C.atomic_fetch_add_u32(voidptr(&a.val), u32(delta))
|
||||
return T(old)
|
||||
} else {
|
||||
old := C.atomic_fetch_add_u64(voidptr(&a.val), u64(delta))
|
||||
return T(old)
|
||||
}
|
||||
} $else $if T is isize || T is usize {
|
||||
if sizeof(isize) == 4 {
|
||||
old := C.atomic_fetch_add_u32(voidptr(&a.val), u32(delta))
|
||||
return T(old)
|
||||
} else {
|
||||
old := C.atomic_fetch_add_u64(voidptr(&a.val), u64(delta))
|
||||
return T(old)
|
||||
}
|
||||
}
|
||||
panic('unreachable')
|
||||
}
|
||||
|
||||
// sub subtracts the atomic value with `delta` and returns the previous value
|
||||
@[inline]
|
||||
pub fn (mut a AtomicVal[T]) sub(delta T) T {
|
||||
$if T is bool {
|
||||
panic('atomic: sub() not supported for bool type')
|
||||
} $else $if T is voidptr {
|
||||
panic('atomic: sub() not supported for voidptr type')
|
||||
} $else $if T is u8 || T is i8 {
|
||||
old := C.atomic_fetch_sub_byte(voidptr(&a.val), u8(delta))
|
||||
return T(old)
|
||||
} $else $if T is u16 || T is i16 {
|
||||
old := C.atomic_fetch_sub_u16(voidptr(&a.val), u16(delta))
|
||||
return T(old)
|
||||
} $else $if T is u32 || T is i32 {
|
||||
old := C.atomic_fetch_sub_u32(voidptr(&a.val), u32(delta))
|
||||
return T(old)
|
||||
} $else $if T is u64 || T is i64 {
|
||||
old := C.atomic_fetch_sub_u64(voidptr(&a.val), u64(delta))
|
||||
return T(old)
|
||||
} $else $if T is int {
|
||||
if sizeof(int) == 4 {
|
||||
old := C.atomic_fetch_sub_u32(voidptr(&a.val), u32(delta))
|
||||
return T(old)
|
||||
} else {
|
||||
old := C.atomic_fetch_sub_u64(voidptr(&a.val), u64(delta))
|
||||
return T(old)
|
||||
}
|
||||
} $else $if T is isize || T is usize {
|
||||
if sizeof(isize) == 4 {
|
||||
old := C.atomic_fetch_sub_u32(voidptr(&a.val), u32(delta))
|
||||
return T(old)
|
||||
} else {
|
||||
old := C.atomic_fetch_sub_u64(voidptr(&a.val), u64(delta))
|
||||
return T(old)
|
||||
}
|
||||
}
|
||||
panic('unreachable')
|
||||
}
|
||||
|
||||
// swap sets the `new` value and returns the previous value
|
||||
@[inline]
|
||||
pub fn (mut a AtomicVal[T]) swap(new T) T {
|
||||
$if T is bool {
|
||||
old := C.atomic_exchange_byte(voidptr(&a.val), u8(new))
|
||||
return old != 0
|
||||
} $else $if T is u8 || T is i8 {
|
||||
old := C.atomic_exchange_byte(voidptr(&a.val), u8(new))
|
||||
return T(old)
|
||||
} $else $if T is u16 || T is i16 {
|
||||
old := C.atomic_exchange_u16(voidptr(&a.val), u16(new))
|
||||
return T(old)
|
||||
} $else $if T is u32 || T is i32 {
|
||||
old := C.atomic_exchange_u32(voidptr(&a.val), u32(new))
|
||||
return T(old)
|
||||
} $else $if T is u64 || T is i64 {
|
||||
old := C.atomic_exchange_u64(voidptr(&a.val), u64(new))
|
||||
return T(old)
|
||||
} $else $if T is int {
|
||||
if sizeof(int) == 4 {
|
||||
old := C.atomic_exchange_u32(voidptr(&a.val), u32(new))
|
||||
return T(old)
|
||||
} else {
|
||||
old := C.atomic_exchange_u64(voidptr(&a.val), u64(new))
|
||||
return T(old)
|
||||
}
|
||||
} $else $if T is isize || T is usize {
|
||||
if sizeof(isize) == 4 {
|
||||
old := C.atomic_exchange_u32(voidptr(&a.val), u32(new))
|
||||
return T(old)
|
||||
} else {
|
||||
old := C.atomic_exchange_u64(voidptr(&a.val), u64(new))
|
||||
return T(old)
|
||||
}
|
||||
} $else $if T is voidptr {
|
||||
// TODO: this should be $if sizeof(T) == 4
|
||||
$if x32 {
|
||||
old := C.atomic_exchange_u32(voidptr(&a.val), u32(new))
|
||||
return T(old)
|
||||
} $else {
|
||||
old := C.atomic_exchange_u64(voidptr(&a.val), u64(new))
|
||||
return T(old)
|
||||
}
|
||||
}
|
||||
panic('unreachable')
|
||||
}
|
||||
|
||||
// compare_and_swap executes the compare-and-swap(CAS) operation
|
||||
// if atomic value == `expected`, then it will be set to `new`, and return true
|
||||
// else return false, and the atomic value remains unchanged
|
||||
@[inline]
|
||||
pub fn (mut a AtomicVal[T]) compare_and_swap(expected T, new T) bool {
|
||||
$if T is bool {
|
||||
mut exp := u8(expected)
|
||||
return C.atomic_compare_exchange_strong_byte(voidptr(&a.val), &exp, u8(new))
|
||||
} $else $if T is u8 || T is i8 {
|
||||
mut exp := u8(expected)
|
||||
return C.atomic_compare_exchange_strong_byte(voidptr(&a.val), &exp, u8(new))
|
||||
} $else $if T is u16 || T is i16 {
|
||||
mut exp := u16(expected)
|
||||
return C.atomic_compare_exchange_strong_u16(voidptr(&a.val), &exp, u16(new))
|
||||
} $else $if T is u32 || T is i32 {
|
||||
mut exp := u32(expected)
|
||||
return C.atomic_compare_exchange_strong_u32(voidptr(&a.val), &exp, u32(new))
|
||||
} $else $if T is u64 || T is i64 {
|
||||
mut exp := u64(expected)
|
||||
return C.atomic_compare_exchange_strong_u64(voidptr(&a.val), &exp, u64(new))
|
||||
} $else $if T is int {
|
||||
if sizeof(int) == 4 {
|
||||
mut exp := u32(expected)
|
||||
return C.atomic_compare_exchange_strong_u32(voidptr(&a.val), &exp, u32(new))
|
||||
} else {
|
||||
mut exp := u64(expected)
|
||||
return C.atomic_compare_exchange_strong_u64(voidptr(&a.val), &exp, u64(new))
|
||||
}
|
||||
} $else $if T is isize || T is usize {
|
||||
if sizeof(isize) == 4 {
|
||||
mut exp := u32(expected)
|
||||
return C.atomic_compare_exchange_strong_u32(voidptr(&a.val), &exp, u32(new))
|
||||
} else {
|
||||
mut exp := u64(expected)
|
||||
return C.atomic_compare_exchange_strong_u64(voidptr(&a.val), &exp, u64(new))
|
||||
}
|
||||
} $else $if T is voidptr {
|
||||
// TODO: this should be $if sizeof(T) == 4
|
||||
$if x32 {
|
||||
mut exp := u32(expected)
|
||||
return C.atomic_compare_exchange_strong_u32(voidptr(&a.val), &exp, u32(new))
|
||||
} $else {
|
||||
mut exp := u64(expected)
|
||||
return C.atomic_compare_exchange_strong_u64(voidptr(&a.val), &exp, u64(new))
|
||||
}
|
||||
}
|
||||
panic('unreachable')
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import sync
|
||||
import sync.stdatomic
|
||||
|
||||
const iterations_per_cycle = 100_000
|
||||
|
||||
struct Counter {
|
||||
mut:
|
||||
counter u64
|
||||
}
|
||||
|
||||
// without proper synchronization this would fail
|
||||
fn test_count_10_times_1_cycle_should_result_10_cycles_with_sync() {
|
||||
desired_iterations := 10 * iterations_per_cycle
|
||||
mut wg := sync.new_waitgroup()
|
||||
mut counter := &Counter{}
|
||||
wg.add(10)
|
||||
for i := 0; i < 10; i++ {
|
||||
spawn count_one_cycle(mut counter, mut wg)
|
||||
}
|
||||
wg.wait()
|
||||
assert counter.counter == u64(desired_iterations)
|
||||
eprintln(' with synchronization the counter is: ${counter.counter:10} , expectedly == ${desired_iterations:10}')
|
||||
}
|
||||
|
||||
// This test just to make sure that we have an anti-test to prove it works
|
||||
fn test_count_10_times_1_cycle_should_not_be_10_cycles_without_sync() {
|
||||
desired_iterations := 10 * iterations_per_cycle
|
||||
mut wg := sync.new_waitgroup()
|
||||
mut counter := &Counter{}
|
||||
wg.add(10)
|
||||
for i := 0; i < 10; i++ {
|
||||
spawn count_one_cycle_without_sync(mut counter, mut wg)
|
||||
}
|
||||
wg.wait()
|
||||
// Note: we do not assert here, just print, because sometimes by chance counter.counter may be == desired_iterations
|
||||
eprintln('without synchronization the counter is: ${counter.counter:10} , expectedly != ${desired_iterations:10}')
|
||||
}
|
||||
|
||||
fn test_atomic_count_plus_one_u64() {
|
||||
mut c := u64(0)
|
||||
assert stdatomic.add_u64(&c, 1) == 1
|
||||
}
|
||||
|
||||
fn test_atomic_count_plus_one_i64() {
|
||||
mut c := i64(0)
|
||||
assert stdatomic.add_i64(&c, 1) == 1
|
||||
}
|
||||
|
||||
fn test_atomic_count_plus_greater_than_one_u64() {
|
||||
mut c := u64(0)
|
||||
assert stdatomic.add_u64(&c, 10) == 10
|
||||
}
|
||||
|
||||
fn test_atomic_count_plus_greater_than_one_i64() {
|
||||
mut c := i64(0)
|
||||
assert stdatomic.add_i64(&c, 10) == 10
|
||||
}
|
||||
|
||||
fn test_atomic_count_minus_one_u64() {
|
||||
mut c := u64(1)
|
||||
assert stdatomic.sub_u64(&c, 1) == 0
|
||||
}
|
||||
|
||||
fn test_atomic_count_minus_one_i64() {
|
||||
mut c := i64(0)
|
||||
assert stdatomic.sub_i64(&c, 1) == -1
|
||||
}
|
||||
|
||||
fn test_atomic_count_minus_greater_than_one_u64() {
|
||||
mut c := u64(0)
|
||||
stdatomic.store_u64(&c, 10)
|
||||
assert stdatomic.sub_u64(&c, 10) == 0
|
||||
}
|
||||
|
||||
fn test_atomic_count_minus_greater_than_one_i64() {
|
||||
mut c := i64(0)
|
||||
stdatomic.store_i64(&c, 10)
|
||||
assert stdatomic.sub_i64(&c, 20) == -10
|
||||
}
|
||||
|
||||
// count_one_cycle counts the common counter iterations_per_cycle times in thread-safe way
|
||||
fn count_one_cycle(mut counter Counter, mut group sync.WaitGroup) {
|
||||
for i := 0; i < iterations_per_cycle; i++ {
|
||||
stdatomic.add_u64(&counter.counter, 1)
|
||||
}
|
||||
group.done()
|
||||
}
|
||||
|
||||
// count_one_cycle_without_sync counts the common counter iterations_per_cycle times in none thread-safe way
|
||||
fn count_one_cycle_without_sync(mut counter Counter, mut group sync.WaitGroup) {
|
||||
for i := 0; i < iterations_per_cycle; i++ {
|
||||
counter.counter++
|
||||
}
|
||||
group.done()
|
||||
}
|
||||
|
||||
fn test_atomic_vals() {
|
||||
mut v_bool := stdatomic.new_atomic(false)
|
||||
v_bool.store(true)
|
||||
assert v_bool.load() == true
|
||||
v_bool.store(false)
|
||||
assert v_bool.load() == false
|
||||
assert v_bool.swap(true) == false
|
||||
assert v_bool.swap(false) == true
|
||||
assert v_bool.compare_and_swap(false, true) == true
|
||||
assert v_bool.load() == true
|
||||
|
||||
mut v_i8 := stdatomic.new_atomic(i8(-33))
|
||||
v_i8.store(-34)
|
||||
assert v_i8.load() == -34
|
||||
assert v_i8.add(10) == -34
|
||||
assert v_i8.load() == -24
|
||||
assert v_i8.sub(7) == -24
|
||||
assert v_i8.load() == -31
|
||||
mut new_i8 := i8(-20)
|
||||
assert v_i8.swap(new_i8) == -31
|
||||
assert v_i8.swap(-31) == new_i8
|
||||
assert v_i8.compare_and_swap(-31, new_i8) == true
|
||||
assert v_i8.compare_and_swap(new_i8, -32) == true
|
||||
assert v_i8.load() == -32
|
||||
|
||||
mut v_u8 := stdatomic.new_atomic(u8(33))
|
||||
v_u8.store(34)
|
||||
assert v_u8.load() == 34
|
||||
assert v_u8.add(10) == 34
|
||||
assert v_u8.load() == 44
|
||||
assert v_u8.sub(7) == 44
|
||||
assert v_u8.load() == 37
|
||||
mut new_u8 := u8(20)
|
||||
assert v_u8.swap(new_u8) == 37
|
||||
assert v_u8.swap(37) == new_u8
|
||||
assert v_u8.compare_and_swap(37, new_u8) == true
|
||||
assert v_u8.compare_and_swap(new_u8, 38) == true
|
||||
assert v_u8.load() == 38
|
||||
|
||||
mut v_i16 := stdatomic.new_atomic(i16(-333))
|
||||
v_i16.store(-334)
|
||||
assert v_i16.load() == -334
|
||||
assert v_i16.add(10) == -334
|
||||
assert v_i16.load() == -324
|
||||
assert v_i16.sub(7) == -324
|
||||
assert v_i16.load() == -331
|
||||
mut new_i16 := i16(-200)
|
||||
assert v_i16.swap(new_i16) == -331
|
||||
assert v_i16.swap(-331) == new_i16
|
||||
assert v_i16.compare_and_swap(-331, new_i16) == true
|
||||
assert v_i16.compare_and_swap(new_i16, -332) == true
|
||||
assert v_i16.load() == -332
|
||||
|
||||
mut v_u16 := stdatomic.new_atomic(u16(333))
|
||||
v_u16.store(334)
|
||||
assert v_u16.load() == 334
|
||||
assert v_u16.add(10) == 334
|
||||
assert v_u16.load() == 344
|
||||
assert v_u16.sub(7) == 344
|
||||
assert v_u16.load() == 337
|
||||
mut new_u16 := u16(200)
|
||||
assert v_u16.swap(new_u16) == 337
|
||||
assert v_u16.swap(337) == new_u16
|
||||
assert v_u16.compare_and_swap(337, new_u16) == true
|
||||
assert v_u16.compare_and_swap(new_u16, 332) == true
|
||||
assert v_u16.load() == 332
|
||||
|
||||
mut v_i32 := stdatomic.new_atomic(i32(-3333))
|
||||
v_i32.store(-3334)
|
||||
assert v_i32.load() == -3334
|
||||
assert v_i32.add(10) == -3334
|
||||
assert v_i32.load() == -3324
|
||||
assert v_i32.sub(7) == -3324
|
||||
assert v_i32.load() == -3331
|
||||
mut new_i32 := i32(-2000)
|
||||
assert v_i32.swap(new_i32) == -3331
|
||||
assert v_i32.swap(-3331) == new_i32
|
||||
assert v_i32.compare_and_swap(-3331, new_i32) == true
|
||||
assert v_i32.compare_and_swap(new_i32, -3332) == true
|
||||
assert v_i32.load() == -3332
|
||||
|
||||
mut v_u32 := stdatomic.new_atomic(u32(3333))
|
||||
v_u32.store(3334)
|
||||
assert v_u32.load() == 3334
|
||||
assert v_u32.add(10) == 3334
|
||||
assert v_u32.load() == 3344
|
||||
assert v_u32.sub(7) == 3344
|
||||
assert v_u32.load() == 3337
|
||||
mut new_u32 := u32(2000)
|
||||
assert v_u32.swap(new_u32) == 3337
|
||||
assert v_u32.swap(3337) == new_u32
|
||||
assert v_u32.compare_and_swap(3337, new_u32) == true
|
||||
assert v_u32.compare_and_swap(new_u32, 3338) == true
|
||||
assert v_u32.load() == 3338
|
||||
|
||||
mut v_i64 := stdatomic.new_atomic(i64(-33333))
|
||||
v_i64.store(-33334)
|
||||
assert v_i64.load() == -33334
|
||||
assert v_i64.add(10) == -33334
|
||||
assert v_i64.load() == -33324
|
||||
assert v_i64.sub(7) == -33324
|
||||
assert v_i64.load() == -33331
|
||||
mut new_i64 := i64(-20000)
|
||||
assert v_i64.swap(new_i64) == -33331
|
||||
assert v_i64.swap(-33331) == new_i64
|
||||
assert v_i64.compare_and_swap(-33331, new_i64) == true
|
||||
assert v_i64.compare_and_swap(new_i64, -33332) == true
|
||||
assert v_i64.load() == -33332
|
||||
|
||||
mut v_u64 := stdatomic.new_atomic(u64(33333))
|
||||
v_u64.store(33334)
|
||||
assert v_u64.load() == 33334
|
||||
assert v_u64.add(10) == 33334
|
||||
assert v_u64.load() == 33344
|
||||
assert v_u64.sub(7) == 33344
|
||||
assert v_u64.load() == 33337
|
||||
mut new_u64 := u64(20000)
|
||||
assert v_u64.swap(new_u64) == 33337
|
||||
assert v_u64.swap(33337) == new_u64
|
||||
assert v_u64.compare_and_swap(33337, new_u64) == true
|
||||
assert v_u64.compare_and_swap(new_u64, 33338) == true
|
||||
assert v_u64.load() == 33338
|
||||
|
||||
mut v_int := stdatomic.new_atomic(int(-44))
|
||||
v_int.store(-45)
|
||||
assert v_int.load() == -45
|
||||
assert v_int.add(10) == -45
|
||||
assert v_int.load() == -35
|
||||
assert v_int.sub(7) == -35
|
||||
assert v_int.load() == -42
|
||||
mut new_int := int(-40000)
|
||||
assert v_int.swap(new_int) == -42
|
||||
assert v_int.swap(-42) == new_int
|
||||
assert v_int.compare_and_swap(-42, new_int) == true
|
||||
assert v_int.compare_and_swap(new_int, -40001) == true
|
||||
assert v_int.load() == -40001
|
||||
|
||||
mut v_isize := stdatomic.new_atomic(isize(-55))
|
||||
v_isize.store(-56)
|
||||
assert v_isize.load() == -56
|
||||
assert v_isize.add(10) == -56
|
||||
assert v_isize.load() == -46
|
||||
assert v_isize.sub(7) == -46
|
||||
assert v_isize.load() == -53
|
||||
mut new_isize := isize(-50000)
|
||||
assert v_isize.swap(new_isize) == -53
|
||||
assert v_isize.swap(-53) == new_isize
|
||||
assert v_isize.compare_and_swap(-53, new_isize) == true
|
||||
assert v_isize.compare_and_swap(new_isize, -50001) == true
|
||||
assert v_isize.load() == -50001
|
||||
|
||||
mut v_usize := stdatomic.new_atomic(usize(55))
|
||||
v_usize.store(56)
|
||||
assert v_usize.load() == 56
|
||||
assert v_usize.add(10) == 56
|
||||
assert v_usize.load() == 66
|
||||
assert v_usize.sub(7) == 66
|
||||
assert v_usize.load() == 59
|
||||
mut new_usize := usize(50000)
|
||||
assert v_usize.swap(new_usize) == 59
|
||||
assert v_usize.swap(59) == new_usize
|
||||
assert v_usize.compare_and_swap(59, new_usize) == true
|
||||
assert v_usize.compare_and_swap(new_usize, 60) == true
|
||||
assert v_usize.load() == 60
|
||||
|
||||
mut val_1 := int(100)
|
||||
mut ptr_1 := voidptr(&val_1)
|
||||
mut val_2 := int(200)
|
||||
mut ptr_2 := voidptr(&val_2)
|
||||
mut v_voidptr := stdatomic.new_atomic(ptr_1)
|
||||
assert v_voidptr.load() == ptr_1
|
||||
v_voidptr.store(ptr_2)
|
||||
assert v_voidptr.load() == ptr_2
|
||||
assert v_voidptr.swap(ptr_1) == ptr_2
|
||||
assert v_voidptr.swap(ptr_2) == ptr_1
|
||||
assert v_voidptr.compare_and_swap(ptr_2, ptr_1) == true
|
||||
assert v_voidptr.load() == ptr_1
|
||||
|
||||
// just for compile
|
||||
C.atomic_thread_fence(C.memory_order_relaxed)
|
||||
C.cpu_relax()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
struct Abc {
|
||||
ch chan int
|
||||
}
|
||||
|
||||
fn f(st Abc) {
|
||||
st.ch <- 47
|
||||
}
|
||||
|
||||
fn test_chan_init() {
|
||||
st := Abc{}
|
||||
spawn f(st)
|
||||
i := <-st.ch
|
||||
assert i == 47
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
@[noreturn]
|
||||
fn cpanic(res int) {
|
||||
panic(unsafe { tos_clone(&u8(C.strerror(res))) })
|
||||
}
|
||||
|
||||
@[noreturn]
|
||||
fn cpanic_errno() {
|
||||
cpanic(C.errno)
|
||||
}
|
||||
|
||||
fn should_be_zero(res int) {
|
||||
if res != 0 {
|
||||
cpanic(res)
|
||||
}
|
||||
}
|
||||
|
||||
// SpinLock is a mutual exclusion lock that busy-waits (spins) when locked.
|
||||
// When one thread holds the lock, any other thread attempting to acquire it
|
||||
// will loop repeatedly until the lock becomes available.
|
||||
@[noinit]
|
||||
pub struct SpinLock {
|
||||
mut:
|
||||
locked u8 // Lock state: 0 = unlocked, 1 = locked
|
||||
padding [63]u8 // Cache line padding (fills to 64 bytes total)
|
||||
}
|
||||
|
||||
// new_spin_lock creates and returns a new SpinLock instance initialized to unlocked state.
|
||||
pub fn new_spin_lock() &SpinLock {
|
||||
mut the_lock := &SpinLock{
|
||||
locked: 0
|
||||
}
|
||||
// Ensure initialization visibility across threads
|
||||
C.atomic_thread_fence(C.memory_order_release)
|
||||
$if valgrind ? {
|
||||
C.ANNOTATE_RWLOCK_CREATE(&the_lock.locked)
|
||||
}
|
||||
return the_lock
|
||||
}
|
||||
|
||||
// lock acquires the spin lock. If the lock is currently held by another thread,
|
||||
// this function will spin (busy-wait) until the lock becomes available.
|
||||
@[inline]
|
||||
pub fn (s &SpinLock) lock() {
|
||||
// Expected value starts as unlocked (0)
|
||||
mut expected := u8(0)
|
||||
mut spin_count := 0
|
||||
max_spins := 100
|
||||
base_delay := 100 // nanosecond
|
||||
max_delay := 10000 // nanoseconds (10μs)
|
||||
|
||||
// Busy-wait until lock is acquired
|
||||
for {
|
||||
// Attempt atomic compare-and-swap:
|
||||
// Succeeds if current value matches expected (0),
|
||||
// then swaps to locked (1)
|
||||
if C.atomic_compare_exchange_weak_byte(&s.locked, &expected, 1) {
|
||||
$if valgrind ? {
|
||||
C.ANNOTATE_RWLOCK_ACQUIRED(&s.locked, 1) // 1 = write lock
|
||||
}
|
||||
// Prevent critical section reordering
|
||||
C.atomic_thread_fence(C.memory_order_acquire)
|
||||
return
|
||||
}
|
||||
|
||||
spin_count++
|
||||
// Exponential backoff after max_spins
|
||||
if spin_count > max_spins {
|
||||
// Calculate delay with cap: 100ns to 10μs
|
||||
exponent := int_min(spin_count / max_spins, 10)
|
||||
delay := int_min(base_delay * (1 << exponent), max_delay)
|
||||
time.sleep(delay * time.nanosecond)
|
||||
} else {
|
||||
// Reduce power/bus contention during spinning
|
||||
C.cpu_relax()
|
||||
}
|
||||
|
||||
expected = 0
|
||||
}
|
||||
}
|
||||
|
||||
// try_lock try to lock the spin lock instance and return immediately.
|
||||
// If the spin lock was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (s &SpinLock) try_lock() bool {
|
||||
// First do a relaxed load to check if lock is free in order to prevent
|
||||
// unnecessary cache misses if someone does while(!try_lock())
|
||||
// TODO: make a `relaxed` load
|
||||
if C.atomic_load_byte(&s.locked) == 0 {
|
||||
mut expected := u8(0)
|
||||
if C.atomic_compare_exchange_weak_byte(&s.locked, &expected, 1) {
|
||||
$if valgrind ? {
|
||||
C.ANNOTATE_RWLOCK_ACQUIRED(&s.locked, 1)
|
||||
}
|
||||
C.atomic_thread_fence(C.memory_order_acquire)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// unlock releases the spin lock, making it available to other threads.
|
||||
// IMPORTANT: Must only be called by the thread that currently holds the lock.
|
||||
@[inline]
|
||||
pub fn (s &SpinLock) unlock() {
|
||||
$if valgrind ? {
|
||||
C.ANNOTATE_RWLOCK_RELEASED(&s.locked, 1) // 1 = write lock
|
||||
}
|
||||
// Ensure critical section completes before release
|
||||
C.atomic_thread_fence(C.memory_order_release)
|
||||
|
||||
// Atomically reset to unlocked state
|
||||
C.atomic_store_byte(&s.locked, 0)
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the spin lock instance.
|
||||
pub fn (s &SpinLock) destroy() {
|
||||
$if valgrind ? {
|
||||
C.ANNOTATE_RWLOCK_DESTROY(&s.locked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
#flag -lpthread
|
||||
|
||||
@[trusted]
|
||||
fn C.pthread_mutex_init(voidptr, voidptr) i32
|
||||
fn C.pthread_mutex_lock(voidptr) i32
|
||||
fn C.pthread_mutex_trylock(voidptr) i32
|
||||
fn C.pthread_mutex_unlock(voidptr) i32
|
||||
fn C.pthread_mutex_destroy(voidptr) i32
|
||||
fn C.pthread_rwlockattr_init(voidptr) i32
|
||||
fn C.pthread_rwlockattr_setkind_np(voidptr, i32) i32
|
||||
fn C.pthread_rwlock_init(voidptr, voidptr) i32
|
||||
fn C.pthread_rwlock_rdlock(voidptr) i32
|
||||
fn C.pthread_rwlock_wrlock(voidptr) i32
|
||||
fn C.pthread_rwlock_tryrdlock(voidptr) i32
|
||||
fn C.pthread_rwlock_trywrlock(voidptr) i32
|
||||
fn C.pthread_rwlock_unlock(voidptr) i32
|
||||
fn C.pthread_rwlock_destroy(voidptr) i32
|
||||
fn C.pthread_condattr_init(voidptr) i32
|
||||
fn C.pthread_condattr_setpshared(voidptr, i32) i32
|
||||
fn C.pthread_condattr_destroy(voidptr) i32
|
||||
fn C.pthread_cond_init(voidptr, voidptr) i32
|
||||
fn C.pthread_cond_signal(voidptr) i32
|
||||
fn C.pthread_cond_wait(voidptr, voidptr) i32
|
||||
fn C.pthread_cond_timedwait(voidptr, voidptr, voidptr) i32
|
||||
fn C.pthread_cond_destroy(voidptr) i32
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_mutex_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_cond_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_rwlock_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_rwlockattr_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.sem_t {}
|
||||
|
||||
// [init_with=new_mutex] // TODO: implement support for this struct attribute, and disallow Mutex{} from outside the sync.new_mutex() function.
|
||||
@[heap]
|
||||
pub struct Mutex {
|
||||
mutex C.pthread_mutex_t
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct RwMutex {
|
||||
mutex C.pthread_rwlock_t
|
||||
inited u32
|
||||
}
|
||||
|
||||
struct RwMutexAttr {
|
||||
attr C.pthread_rwlockattr_t
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_condattr_t {}
|
||||
|
||||
struct CondAttr {
|
||||
attr C.pthread_condattr_t
|
||||
}
|
||||
|
||||
/*
|
||||
MacOSX has no unnamed semaphores and no `timed_wait()` at all
|
||||
so we emulate the behaviour with other devices
|
||||
*/
|
||||
@[heap]
|
||||
pub struct Semaphore {
|
||||
mtx C.pthread_mutex_t
|
||||
cond C.pthread_cond_t
|
||||
mut:
|
||||
count u32
|
||||
}
|
||||
|
||||
// new_mutex creates and initialises a new mutex instance on the heap, then returns a pointer to it.
|
||||
pub fn new_mutex() &Mutex {
|
||||
mut m := &Mutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// init initialises the mutex. It should be called once before the mutex is used,
|
||||
// since it creates the associated resources needed for the mutex to work properly.
|
||||
pub fn (mut m Mutex) init() {
|
||||
should_be_zero(C.pthread_mutex_init(&m.mutex, C.NULL))
|
||||
}
|
||||
|
||||
// new_rwmutex creates a new read/write mutex instance on the heap, and returns a pointer to it.
|
||||
pub fn new_rwmutex() &RwMutex {
|
||||
mut m := &RwMutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// init initialises the RwMutex instance. It should be called once before the rw mutex is used,
|
||||
// since it creates the associated resources needed for the mutex to work properly.
|
||||
pub fn (mut m RwMutex) init() {
|
||||
a := RwMutexAttr{}
|
||||
should_be_zero(C.pthread_rwlockattr_init(&a.attr))
|
||||
// Give writer priority over readers
|
||||
C.pthread_rwlockattr_setkind_np(&a.attr, C.PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
|
||||
should_be_zero(C.pthread_rwlock_init(&m.mutex, &a.attr))
|
||||
C.atomic_store_u32(&m.inited, 1)
|
||||
}
|
||||
|
||||
fn (mut m RwMutex) lazy_init() {
|
||||
if C.atomic_load_u32(&m.inited) == 0 {
|
||||
mut expected := u32(0)
|
||||
if C.atomic_compare_exchange_strong_u32(&m.inited, &expected, 1) {
|
||||
a := RwMutexAttr{}
|
||||
C.pthread_rwlockattr_init(&a.attr)
|
||||
C.pthread_rwlockattr_setkind_np(&a.attr, C.PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
|
||||
C.pthread_rwlock_init(&m.mutex, &a.attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lock locks the mutex instance (`lock` is a keyword).
|
||||
// If the mutex was already locked, it will block, till it is unlocked.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) lock() {
|
||||
C.pthread_mutex_lock(&m.mutex)
|
||||
}
|
||||
|
||||
// try_lock try to lock the mutex instance and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) try_lock() bool {
|
||||
return C.pthread_mutex_trylock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// unlock unlocks the mutex instance. The mutex is released, and one of
|
||||
// the other threads, that were blocked, because they called lock can continue.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) unlock() {
|
||||
C.pthread_mutex_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the mutex instance.
|
||||
// Note: the mutex itself is not freed.
|
||||
pub fn (mut m Mutex) destroy() {
|
||||
should_be_zero(C.pthread_mutex_destroy(&m.mutex))
|
||||
}
|
||||
|
||||
// rlock locks the given RwMutex instance for reading.
|
||||
// If the mutex was already locked, it will block, and will try to get the lock,
|
||||
// once the lock is released by another thread calling unlock.
|
||||
// Once it succeds, it returns.
|
||||
// Note: there may be several threads that are waiting for the same lock.
|
||||
// Note: RwMutex has separate read and write locks.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) rlock() {
|
||||
m.lazy_init()
|
||||
should_be_zero(C.pthread_rwlock_rdlock(&m.mutex))
|
||||
}
|
||||
|
||||
// lock locks the given RwMutex instance for writing.
|
||||
// If the mutex was already locked, it will block, till it is unlocked,
|
||||
// then it will try to get the lock, and if it can, it will return, otherwise
|
||||
// it will continue waiting for the mutex to become unlocked.
|
||||
// Note: there may be several threads that are waiting for the same lock.
|
||||
// Note: RwMutex has separate read and write locks.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) lock() {
|
||||
m.lazy_init()
|
||||
should_be_zero(C.pthread_rwlock_wrlock(&m.mutex))
|
||||
}
|
||||
|
||||
// try_rlock try to lock the given RwMutex instance for reading and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) try_rlock() bool {
|
||||
return C.pthread_rwlock_tryrdlock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// try_wlock try to lock the given RwMutex instance for writing and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) try_wlock() bool {
|
||||
return C.pthread_rwlock_trywrlock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the rwmutex instance.
|
||||
// Note: the mutex itself is not freed.
|
||||
pub fn (mut m RwMutex) destroy() {
|
||||
should_be_zero(C.pthread_rwlock_destroy(&m.mutex))
|
||||
}
|
||||
|
||||
// runlock unlocks the RwMutex instance, locked for reading.
|
||||
// Note: Windows SRWLocks have different function to unlocking.
|
||||
// To have a common portable API, there are two methods for
|
||||
// unlocking here as well, even though that they do the same
|
||||
// on !windows platforms.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) runlock() {
|
||||
C.pthread_rwlock_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// unlock unlocks the RwMutex instance, locked for writing.
|
||||
// Note: Windows SRWLocks have different function to unlocking.
|
||||
// To have a common portable API, there are two methods for
|
||||
// unlocking here as well, even though that they do the same
|
||||
// on !windows platforms.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) unlock() {
|
||||
C.pthread_rwlock_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// new_semaphore creates a new initialised Semaphore instance on the heap, and returns a pointer to it.
|
||||
// The initial counter value of the semaphore is 0.
|
||||
pub fn new_semaphore() &Semaphore {
|
||||
return new_semaphore_init(0)
|
||||
}
|
||||
|
||||
// new_semaphore_init creates a new initialised Semaphore instance on the heap, and returns a pointer to it.
|
||||
// The `n` parameter can be used to set the initial counter value of the semaphore.
|
||||
pub fn new_semaphore_init(n u32) &Semaphore {
|
||||
mut sem := &Semaphore{}
|
||||
sem.init(n)
|
||||
return sem
|
||||
}
|
||||
|
||||
// init initialises the Semaphore instance with `n` as its initial counter value.
|
||||
// It should be called once before the semaphore is used, since it creates the associated
|
||||
// resources needed for the semaphore to work properly.
|
||||
pub fn (mut sem Semaphore) init(n u32) {
|
||||
C.atomic_store_u32(&sem.count, n)
|
||||
should_be_zero(C.pthread_mutex_init(&sem.mtx, C.NULL))
|
||||
attr := CondAttr{}
|
||||
should_be_zero(C.pthread_condattr_init(&attr.attr))
|
||||
C.pthread_condattr_setpshared(&attr.attr, C.PTHREAD_PROCESS_PRIVATE)
|
||||
C.pthread_cond_init(&sem.cond, &attr.attr)
|
||||
C.pthread_condattr_destroy(&attr.attr)
|
||||
}
|
||||
|
||||
// post increases the counter of the semaphore by 1.
|
||||
// If the resulting counter value is > 0, and if there is a thread waiting
|
||||
// on the semaphore, the waiting thread will decrement the counter by 1, and
|
||||
// then will continue running. See also .wait() .
|
||||
pub fn (mut sem Semaphore) post() {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 1 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c + 1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
C.pthread_mutex_lock(&sem.mtx)
|
||||
c = C.atomic_fetch_add_u32(&sem.count, 1)
|
||||
if c == 0 {
|
||||
C.pthread_cond_signal(&sem.cond)
|
||||
}
|
||||
C.pthread_mutex_unlock(&sem.mtx)
|
||||
}
|
||||
|
||||
// wait will just decrement the semaphore count, if it was positive.
|
||||
// It it was not positive, it will waits for the semaphore count to reach a positive number.
|
||||
// When that happens, it will decrease the semaphore count, and will return.
|
||||
// In effect, it allows you to block threads, until the semaphore, is posted by another thread.
|
||||
// See also .post() .
|
||||
pub fn (mut sem Semaphore) wait() {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
C.pthread_mutex_lock(&sem.mtx)
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
outer: for {
|
||||
if c == 0 {
|
||||
C.pthread_cond_wait(&sem.cond, &sem.mtx)
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
}
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
if c > 1 {
|
||||
C.pthread_cond_signal(&sem.cond)
|
||||
}
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
C.pthread_mutex_unlock(&sem.mtx)
|
||||
}
|
||||
|
||||
// try_wait tries to decrease the semaphore count by 1, if it was positive.
|
||||
// If it succeeds in that, it returns true, otherwise it returns false.
|
||||
// Note: try_wait should return as fast as possible so error handling is only
|
||||
// done when debugging
|
||||
pub fn (mut sem Semaphore) try_wait() bool {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// timed_wait is similar to .wait(), but it also accepts a timeout duration,
|
||||
// thus it can return false early, if the timeout passed before the semaphore was posted.
|
||||
pub fn (mut sem Semaphore) timed_wait(timeout time.Duration) bool {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
C.pthread_mutex_lock(&sem.mtx)
|
||||
t_spec := timeout.timespec()
|
||||
mut res := 0
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
|
||||
outer: for {
|
||||
if c == 0 {
|
||||
res = C.pthread_cond_timedwait(&sem.cond, &sem.mtx, &t_spec)
|
||||
if res == C.ETIMEDOUT {
|
||||
break outer
|
||||
}
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
}
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
if c > 1 {
|
||||
C.pthread_cond_signal(&sem.cond)
|
||||
}
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
C.pthread_mutex_unlock(&sem.mtx)
|
||||
return res == 0
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the Semaphore instance.
|
||||
// Note: the semaphore instance itself is not freed.
|
||||
pub fn (mut sem Semaphore) destroy() {
|
||||
should_be_zero(C.pthread_cond_destroy(&sem.cond))
|
||||
should_be_zero(C.pthread_mutex_destroy(&sem.mtx))
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
// There's no additional linking (-lpthread) needed for Android.
|
||||
// See https://stackoverflow.com/a/31277163/1904615
|
||||
$if !android {
|
||||
#flag -lpthread
|
||||
}
|
||||
|
||||
#include <semaphore.h>
|
||||
|
||||
@[trusted]
|
||||
fn C.pthread_mutex_init(voidptr, voidptr) i32
|
||||
fn C.pthread_mutex_lock(voidptr) i32
|
||||
fn C.pthread_mutex_trylock(voidptr) i32
|
||||
fn C.pthread_mutex_unlock(voidptr) i32
|
||||
fn C.pthread_mutex_destroy(voidptr) i32
|
||||
fn C.pthread_rwlockattr_init(voidptr) i32
|
||||
fn C.pthread_rwlockattr_setkind_np(voidptr, i32) i32
|
||||
fn C.pthread_rwlockattr_destroy(voidptr) i32
|
||||
fn C.pthread_rwlock_init(voidptr, voidptr) i32
|
||||
fn C.pthread_rwlock_rdlock(voidptr) i32
|
||||
fn C.pthread_rwlock_wrlock(voidptr) i32
|
||||
fn C.pthread_rwlock_tryrdlock(voidptr) i32
|
||||
fn C.pthread_rwlock_trywrlock(voidptr) i32
|
||||
fn C.pthread_rwlock_unlock(voidptr) i32
|
||||
fn C.pthread_rwlock_destroy(voidptr) i32
|
||||
fn C.sem_init(voidptr, i32, u32) i32
|
||||
fn C.sem_post(voidptr) i32
|
||||
fn C.sem_wait(voidptr) i32
|
||||
fn C.sem_trywait(voidptr) i32
|
||||
fn C.sem_timedwait(voidptr, voidptr) i32
|
||||
fn C.sem_destroy(voidptr) i32
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_mutex_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_rwlock_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_rwlockattr_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.sem_t {}
|
||||
|
||||
// [init_with=new_mutex] // TODO: implement support for this struct attribute, and disallow Mutex{} from outside the sync.new_mutex() function.
|
||||
@[heap]
|
||||
pub struct Mutex {
|
||||
mutex C.pthread_mutex_t
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct RwMutex {
|
||||
mutex C.pthread_rwlock_t
|
||||
inited u32
|
||||
}
|
||||
|
||||
struct RwMutexAttr {
|
||||
attr C.pthread_rwlockattr_t
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct Semaphore {
|
||||
sem C.sem_t
|
||||
}
|
||||
|
||||
// new_mutex creates and initialises a new mutex instance on the heap, then returns a pointer to it.
|
||||
pub fn new_mutex() &Mutex {
|
||||
mut m := &Mutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// init initialises the mutex. It should be called once before the mutex is used,
|
||||
// since it creates the associated resources needed for the mutex to work properly.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) init() {
|
||||
C.pthread_mutex_init(&m.mutex, C.NULL)
|
||||
}
|
||||
|
||||
// new_rwmutex creates a new read/write mutex instance on the heap, and returns a pointer to it.
|
||||
pub fn new_rwmutex() &RwMutex {
|
||||
mut m := &RwMutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// init initialises the RwMutex instance. It should be called once before the rw mutex is used,
|
||||
// since it creates the associated resources needed for the mutex to work properly.
|
||||
pub fn (mut m RwMutex) init() {
|
||||
a := RwMutexAttr{}
|
||||
C.pthread_rwlockattr_init(&a.attr)
|
||||
// Give writer priority over readers
|
||||
C.pthread_rwlockattr_setkind_np(&a.attr, C.PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
|
||||
C.pthread_rwlock_init(&m.mutex, &a.attr)
|
||||
C.pthread_rwlockattr_destroy(&a.attr) // destroy the attr when done
|
||||
C.atomic_store_u32(&m.inited, 1)
|
||||
}
|
||||
|
||||
fn (mut m RwMutex) lazy_init() {
|
||||
if C.atomic_load_u32(&m.inited) == 0 {
|
||||
mut expected := u32(0)
|
||||
if C.atomic_compare_exchange_strong_u32(&m.inited, &expected, 1) {
|
||||
a := RwMutexAttr{}
|
||||
C.pthread_rwlockattr_init(&a.attr)
|
||||
C.pthread_rwlockattr_setkind_np(&a.attr, C.PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
|
||||
C.pthread_rwlock_init(&m.mutex, &a.attr)
|
||||
C.pthread_rwlockattr_destroy(&a.attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lock locks the mutex instance (`lock` is a keyword).
|
||||
// If the mutex was already locked, it will block, till it is unlocked.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) lock() {
|
||||
C.pthread_mutex_lock(&m.mutex)
|
||||
}
|
||||
|
||||
// try_lock try to lock the mutex instance and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) try_lock() bool {
|
||||
return C.pthread_mutex_trylock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// unlock unlocks the mutex instance. The mutex is released, and one of
|
||||
// the other threads, that were blocked, because they called lock can continue.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) unlock() {
|
||||
C.pthread_mutex_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the mutex instance.
|
||||
// Note: the mutex itself is not freed.
|
||||
pub fn (mut m Mutex) destroy() {
|
||||
should_be_zero(C.pthread_mutex_destroy(&m.mutex))
|
||||
}
|
||||
|
||||
// rlock locks the given RwMutex instance for reading.
|
||||
// If the mutex was already locked, it will block, and will try to get the lock,
|
||||
// once the lock is released by another thread calling unlock.
|
||||
// Once it succeds, it returns.
|
||||
// Note: there may be several threads that are waiting for the same lock.
|
||||
// Note: RwMutex has separate read and write locks.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) rlock() {
|
||||
m.lazy_init()
|
||||
should_be_zero(C.pthread_rwlock_rdlock(&m.mutex))
|
||||
}
|
||||
|
||||
// lock locks the given RwMutex instance for writing.
|
||||
// If the mutex was already locked, it will block, till it is unlocked,
|
||||
// then it will try to get the lock, and if it can, it will return, otherwise
|
||||
// it will continue waiting for the mutex to become unlocked.
|
||||
// Note: there may be several threads that are waiting for the same lock.
|
||||
// Note: RwMutex has separate read and write locks.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) lock() {
|
||||
m.lazy_init()
|
||||
should_be_zero(C.pthread_rwlock_wrlock(&m.mutex))
|
||||
}
|
||||
|
||||
// try_rlock try to lock the given RwMutex instance for reading and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) try_rlock() bool {
|
||||
return C.pthread_rwlock_tryrdlock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// try_wlock try to lock the given RwMutex instance for writing and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) try_wlock() bool {
|
||||
return C.pthread_rwlock_trywrlock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the rwmutex instance.
|
||||
// Note: the mutex itself is not freed.
|
||||
pub fn (mut m RwMutex) destroy() {
|
||||
should_be_zero(C.pthread_rwlock_destroy(&m.mutex))
|
||||
}
|
||||
|
||||
// runlock unlocks the RwMutex instance, locked for reading.
|
||||
// Note: Windows SRWLocks have different function to unlocking.
|
||||
// To have a common portable API, there are two methods for
|
||||
// unlocking here as well, even though that they do the same
|
||||
// on !windows platforms.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) runlock() {
|
||||
C.pthread_rwlock_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// unlock unlocks the RwMutex instance, locked for writing.
|
||||
// Note: Windows SRWLocks have different function to unlocking.
|
||||
// To have a common portable API, there are two methods for
|
||||
// unlocking here as well, even though that they do the same
|
||||
// on !windows platforms.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) unlock() {
|
||||
C.pthread_rwlock_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// new_semaphore creates a new initialised Semaphore instance on the heap, and returns a pointer to it.
|
||||
// The initial counter value of the semaphore is 0.
|
||||
@[inline]
|
||||
pub fn new_semaphore() &Semaphore {
|
||||
return new_semaphore_init(0)
|
||||
}
|
||||
|
||||
// new_semaphore_init creates a new initialised Semaphore instance on the heap, and returns a pointer to it.
|
||||
// The `n` parameter can be used to set the initial counter value of the semaphore.
|
||||
pub fn new_semaphore_init(n u32) &Semaphore {
|
||||
mut sem := &Semaphore{}
|
||||
sem.init(n)
|
||||
return sem
|
||||
}
|
||||
|
||||
// init initialises the Semaphore instance with `n` as its initial counter value.
|
||||
// It should be called once before the semaphore is used, since it creates the associated
|
||||
// resources needed for the semaphore to work properly.
|
||||
@[inline]
|
||||
pub fn (mut sem Semaphore) init(n u32) {
|
||||
C.sem_init(&sem.sem, 0, n)
|
||||
}
|
||||
|
||||
// post increases/unlocks the counter of the semaphore by 1.
|
||||
// If the resulting counter value is > 0, and if there is another thread waiting
|
||||
// on the semaphore, the waiting thread will decrement the counter by 1
|
||||
// (locking the semaphore), and then will continue running. See also .wait().
|
||||
@[inline]
|
||||
pub fn (mut sem Semaphore) post() {
|
||||
C.sem_post(&sem.sem)
|
||||
}
|
||||
|
||||
// wait will just decrement the semaphore count, if it was positive.
|
||||
// It it was not positive, it will waits for the semaphore count to reach a positive number.
|
||||
// When that happens, it will decrease the semaphore count (lock the semaphore), and will return.
|
||||
// In effect, it allows you to block threads, until the semaphore, is posted by another thread.
|
||||
// See also .post().
|
||||
pub fn (mut sem Semaphore) wait() {
|
||||
for {
|
||||
if C.sem_wait(&sem.sem) == 0 {
|
||||
return
|
||||
}
|
||||
e := C.errno
|
||||
match e {
|
||||
C.EINTR {
|
||||
continue // interrupted by signal
|
||||
}
|
||||
else {
|
||||
cpanic_errno()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try_wait tries to decrease the semaphore count by 1, if it was positive.
|
||||
// If it succeeds in that, it returns true, otherwise it returns false.
|
||||
// try_wait should return as fast as possible so error handling is only
|
||||
// done when debugging.
|
||||
pub fn (mut sem Semaphore) try_wait() bool {
|
||||
$if !debug {
|
||||
return C.sem_trywait(&sem.sem) == 0
|
||||
} $else {
|
||||
if C.sem_trywait(&sem.sem) != 0 {
|
||||
e := C.errno
|
||||
match e {
|
||||
C.EAGAIN {
|
||||
return false
|
||||
}
|
||||
else {
|
||||
cpanic_errno()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// timed_wait is similar to .wait(), but it also accepts a timeout duration,
|
||||
// thus it can return false early, if the timeout passed before the semaphore was posted.
|
||||
pub fn (mut sem Semaphore) timed_wait(timeout time.Duration) bool {
|
||||
$if macos {
|
||||
time.sleep(timeout)
|
||||
return true
|
||||
}
|
||||
t_spec := timeout.timespec()
|
||||
for {
|
||||
$if !macos {
|
||||
if C.sem_timedwait(&sem.sem, &t_spec) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
e := C.errno
|
||||
match e {
|
||||
C.EINTR {
|
||||
continue // interrupted by signal
|
||||
}
|
||||
C.ETIMEDOUT {
|
||||
break
|
||||
}
|
||||
else {
|
||||
cpanic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the Semaphore instance.
|
||||
// Note: the semaphore instance itself is not freed.
|
||||
pub fn (mut sem Semaphore) destroy() {
|
||||
should_be_zero(C.sem_destroy(&sem.sem))
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
// There's no additional linking (-lpthread) needed for Android.
|
||||
// See https://stackoverflow.com/a/31277163/1904615
|
||||
$if !android {
|
||||
#flag -lpthread
|
||||
}
|
||||
|
||||
#include <semaphore.h>
|
||||
#include <pthread_np.h>
|
||||
#include <errno.h>
|
||||
|
||||
fn C.pthread_getthreadid_np() i32
|
||||
|
||||
@[trusted]
|
||||
fn C.pthread_mutex_init(voidptr, voidptr) i32
|
||||
fn C.pthread_mutex_lock(voidptr) i32
|
||||
fn C.pthread_mutex_trylock(voidptr) i32
|
||||
fn C.pthread_mutex_unlock(voidptr) i32
|
||||
fn C.pthread_mutex_destroy(voidptr) i32
|
||||
fn C.pthread_rwlockattr_init(voidptr) i32
|
||||
fn C.pthread_rwlockattr_setkind_np(voidptr, i32) i32
|
||||
fn C.pthread_rwlockattr_destroy(voidptr) i32
|
||||
fn C.pthread_rwlock_init(voidptr, voidptr) i32
|
||||
fn C.pthread_rwlock_rdlock(voidptr) i32
|
||||
fn C.pthread_rwlock_wrlock(voidptr) i32
|
||||
fn C.pthread_rwlock_tryrdlock(voidptr) i32
|
||||
fn C.pthread_rwlock_trywrlock(voidptr) i32
|
||||
fn C.pthread_rwlock_unlock(voidptr) i32
|
||||
fn C.pthread_rwlock_destroy(voidptr) i32
|
||||
fn C.sem_init(voidptr, i32, u32) i32
|
||||
fn C.sem_post(voidptr) i32
|
||||
fn C.sem_wait(voidptr) i32
|
||||
fn C.sem_trywait(voidptr) i32
|
||||
fn C.sem_timedwait(voidptr, voidptr) i32
|
||||
fn C.sem_destroy(voidptr) i32
|
||||
|
||||
pub struct C.pthread_mutex {}
|
||||
|
||||
pub struct C.pthread_rwlock {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.pthread_rwlockattr_t {}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.sem_t {}
|
||||
|
||||
// [init_with=new_mutex] // TODO: implement support for this struct attribute, and disallow Mutex{} from outside the sync.new_mutex() function.
|
||||
@[heap]
|
||||
pub struct Mutex {
|
||||
mutex &C.pthread_mutex = unsafe { nil }
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct RwMutex {
|
||||
mutex &C.pthread_rwlock = unsafe { nil }
|
||||
inited u32
|
||||
writer u32 // tid of current write-lock holder, 0 when none; used for self-deadlock detection
|
||||
}
|
||||
|
||||
struct RwMutexAttr {
|
||||
attr C.pthread_rwlockattr_t
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct Semaphore {
|
||||
sem C.sem_t
|
||||
}
|
||||
|
||||
// new_mutex creates and initialises a new mutex instance on the heap, then returns a pointer to it.
|
||||
pub fn new_mutex() &Mutex {
|
||||
mut m := &Mutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// init initialises the mutex. It should be called once before the mutex is used,
|
||||
// since it creates the associated resources needed for the mutex to work properly.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) init() {
|
||||
C.pthread_mutex_init(&m.mutex, C.NULL)
|
||||
}
|
||||
|
||||
// new_rwmutex creates a new read/write mutex instance on the heap, and returns a pointer to it.
|
||||
pub fn new_rwmutex() &RwMutex {
|
||||
mut m := &RwMutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// init initialises the RwMutex instance. It should be called once before the rw mutex is used,
|
||||
// since it creates the associated resources needed for the mutex to work properly.
|
||||
pub fn (mut m RwMutex) init() {
|
||||
a := RwMutexAttr{}
|
||||
C.pthread_rwlockattr_init(&a.attr)
|
||||
// Give writer priority over readers
|
||||
C.pthread_rwlockattr_setkind_np(&a.attr, C.PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
|
||||
C.pthread_rwlock_init(&m.mutex, &a.attr)
|
||||
C.pthread_rwlockattr_destroy(&a.attr) // destroy the attr when done
|
||||
C.atomic_store_u32(&m.inited, 1)
|
||||
}
|
||||
|
||||
fn (mut m RwMutex) lazy_init() {
|
||||
if C.atomic_load_u32(&m.inited) == 0 {
|
||||
mut expected := u32(0)
|
||||
if C.atomic_compare_exchange_strong_u32(&m.inited, &expected, 1) {
|
||||
a := RwMutexAttr{}
|
||||
C.pthread_rwlockattr_init(&a.attr)
|
||||
C.pthread_rwlockattr_setkind_np(&a.attr, C.PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
|
||||
C.pthread_rwlock_init(&m.mutex, &a.attr)
|
||||
C.pthread_rwlockattr_destroy(&a.attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lock locks the mutex instance (`lock` is a keyword).
|
||||
// If the mutex was already locked, it will block, till it is unlocked.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) lock() {
|
||||
C.pthread_mutex_lock(&m.mutex)
|
||||
}
|
||||
|
||||
// try_lock try to lock the mutex instance and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) try_lock() bool {
|
||||
return C.pthread_mutex_trylock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// unlock unlocks the mutex instance. The mutex is released, and one of
|
||||
// the other threads, that were blocked, because they called lock can continue.
|
||||
@[inline]
|
||||
pub fn (mut m Mutex) unlock() {
|
||||
C.pthread_mutex_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the mutex instance.
|
||||
// Note: the mutex itself is not freed.
|
||||
pub fn (mut m Mutex) destroy() {
|
||||
should_be_zero(C.pthread_mutex_destroy(&m.mutex))
|
||||
}
|
||||
|
||||
// rlock locks the given RwMutex instance for reading.
|
||||
// If the mutex was already locked, it will block, and will try to get the lock,
|
||||
// once the lock is released by another thread calling unlock.
|
||||
// Once it succeds, it returns.
|
||||
// Note: there may be several threads that are waiting for the same lock.
|
||||
// Note: RwMutex has separate read and write locks.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) rlock() {
|
||||
m.lazy_init()
|
||||
// FreeBSD libthr does not return EDEADLK on self-deadlock, it hangs forever.
|
||||
// Match the Linux glibc nonrecursive-writer behavior by failing fast.
|
||||
tid := u32(C.pthread_getthreadid_np())
|
||||
if C.atomic_load_u32(&m.writer) == tid {
|
||||
cpanic(C.EDEADLK)
|
||||
}
|
||||
should_be_zero(C.pthread_rwlock_rdlock(&m.mutex))
|
||||
}
|
||||
|
||||
// lock locks the given RwMutex instance for writing.
|
||||
// If the mutex was already locked, it will block, till it is unlocked,
|
||||
// then it will try to get the lock, and if it can, it will return, otherwise
|
||||
// it will continue waiting for the mutex to become unlocked.
|
||||
// Note: there may be several threads that are waiting for the same lock.
|
||||
// Note: RwMutex has separate read and write locks.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) lock() {
|
||||
m.lazy_init()
|
||||
tid := u32(C.pthread_getthreadid_np())
|
||||
if C.atomic_load_u32(&m.writer) == tid {
|
||||
cpanic(C.EDEADLK)
|
||||
}
|
||||
should_be_zero(C.pthread_rwlock_wrlock(&m.mutex))
|
||||
C.atomic_store_u32(&m.writer, tid)
|
||||
}
|
||||
|
||||
// try_rlock try to lock the given RwMutex instance for reading and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) try_rlock() bool {
|
||||
return C.pthread_rwlock_tryrdlock(&m.mutex) == 0
|
||||
}
|
||||
|
||||
// try_wlock try to lock the given RwMutex instance for writing and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) try_wlock() bool {
|
||||
if C.pthread_rwlock_trywrlock(&m.mutex) == 0 {
|
||||
C.atomic_store_u32(&m.writer, u32(C.pthread_getthreadid_np()))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the rwmutex instance.
|
||||
// Note: the mutex itself is not freed.
|
||||
pub fn (mut m RwMutex) destroy() {
|
||||
should_be_zero(C.pthread_rwlock_destroy(&m.mutex))
|
||||
}
|
||||
|
||||
// runlock unlocks the RwMutex instance, locked for reading.
|
||||
// Note: Windows SRWLocks have different function to unlocking.
|
||||
// To have a common portable API, there are two methods for
|
||||
// unlocking here as well, even though that they do the same
|
||||
// on !windows platforms.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) runlock() {
|
||||
C.pthread_rwlock_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// unlock unlocks the RwMutex instance, locked for writing.
|
||||
// Note: Windows SRWLocks have different function to unlocking.
|
||||
// To have a common portable API, there are two methods for
|
||||
// unlocking here as well, even though that they do the same
|
||||
// on !windows platforms.
|
||||
@[inline]
|
||||
pub fn (mut m RwMutex) unlock() {
|
||||
C.atomic_store_u32(&m.writer, 0)
|
||||
C.pthread_rwlock_unlock(&m.mutex)
|
||||
}
|
||||
|
||||
// new_semaphore creates a new initialised Semaphore instance on the heap, and returns a pointer to it.
|
||||
// The initial counter value of the semaphore is 0.
|
||||
@[inline]
|
||||
pub fn new_semaphore() &Semaphore {
|
||||
return new_semaphore_init(0)
|
||||
}
|
||||
|
||||
// new_semaphore_init creates a new initialised Semaphore instance on the heap, and returns a pointer to it.
|
||||
// The `n` parameter can be used to set the initial counter value of the semaphore.
|
||||
pub fn new_semaphore_init(n u32) &Semaphore {
|
||||
mut sem := &Semaphore{}
|
||||
sem.init(n)
|
||||
return sem
|
||||
}
|
||||
|
||||
// init initialises the Semaphore instance with `n` as its initial counter value.
|
||||
// It should be called once before the semaphore is used, since it creates the associated
|
||||
// resources needed for the semaphore to work properly.
|
||||
@[inline]
|
||||
pub fn (mut sem Semaphore) init(n u32) {
|
||||
C.sem_init(&sem.sem, 0, n)
|
||||
}
|
||||
|
||||
// post increases/unlocks the counter of the semaphore by 1.
|
||||
// If the resulting counter value is > 0, and if there is another thread waiting
|
||||
// on the semaphore, the waiting thread will decrement the counter by 1
|
||||
// (locking the semaphore), and then will continue running. See also .wait() .
|
||||
@[inline]
|
||||
pub fn (mut sem Semaphore) post() {
|
||||
C.sem_post(&sem.sem)
|
||||
}
|
||||
|
||||
// wait will just decrement the semaphore count, if it was positive.
|
||||
// It it was not positive, it will waits for the semaphore count to reach a positive number.
|
||||
// When that happens, it will decrease the semaphore count (lock the semaphore), and will return.
|
||||
// In effect, it allows you to block threads, until the semaphore, is posted by another thread.
|
||||
// See also .post() .
|
||||
pub fn (mut sem Semaphore) wait() {
|
||||
for {
|
||||
if C.sem_wait(&sem.sem) == 0 {
|
||||
return
|
||||
}
|
||||
e := C.errno
|
||||
match e {
|
||||
C.EINTR {
|
||||
continue // interrupted by signal
|
||||
}
|
||||
else {
|
||||
cpanic_errno()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try_wait tries to decrease the semaphore count by 1, if it was positive.
|
||||
// If it succeeds in that, it returns true, otherwise it returns false.
|
||||
// try_wait should return as fast as possible so error handling is only
|
||||
// done when debugging
|
||||
pub fn (mut sem Semaphore) try_wait() bool {
|
||||
$if !debug {
|
||||
return C.sem_trywait(&sem.sem) == 0
|
||||
} $else {
|
||||
if C.sem_trywait(&sem.sem) != 0 {
|
||||
e := C.errno
|
||||
match e {
|
||||
C.EAGAIN {
|
||||
return false
|
||||
}
|
||||
else {
|
||||
cpanic_errno()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// timed_wait is similar to .wait(), but it also accepts a timeout duration,
|
||||
// thus it can return false early, if the timeout passed before the semaphore was posted.
|
||||
pub fn (mut sem Semaphore) timed_wait(timeout time.Duration) bool {
|
||||
$if macos {
|
||||
time.sleep(timeout)
|
||||
return true
|
||||
}
|
||||
t_spec := timeout.timespec()
|
||||
for {
|
||||
$if !macos {
|
||||
if C.sem_timedwait(&sem.sem, &t_spec) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
e := C.errno
|
||||
match e {
|
||||
C.EINTR {
|
||||
continue // interrupted by signal
|
||||
}
|
||||
C.ETIMEDOUT {
|
||||
break
|
||||
}
|
||||
else {
|
||||
cpanic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// destroy frees the resources associated with the Semaphore instance.
|
||||
// Note: the semaphore instance itself is not freed.
|
||||
pub fn (mut sem Semaphore) destroy() {
|
||||
should_be_zero(C.sem_destroy(&sem.sem))
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module sync
|
||||
|
||||
import time
|
||||
|
||||
#include <synchapi.h>
|
||||
#include <time.h>
|
||||
|
||||
fn C.GetSystemTimeAsFileTime(lpSystemTimeAsFileTime &C._FILETIME)
|
||||
fn C.InitializeConditionVariable(voidptr)
|
||||
fn C.WakeConditionVariable(voidptr)
|
||||
fn C.SleepConditionVariableSRW(voidptr, voidptr, u32, u32) i32
|
||||
|
||||
fn C.TryAcquireSRWLockExclusive(h voidptr) i32
|
||||
fn C.TryAcquireSRWLockShared(h voidptr) i32
|
||||
|
||||
// TODO: The suggestion of using CriticalSection instead of mutex
|
||||
// was discussed. Needs consideration.
|
||||
|
||||
// Mutex HANDLE
|
||||
type MHANDLE = voidptr
|
||||
|
||||
// Semaphore HANDLE
|
||||
type SHANDLE = voidptr
|
||||
|
||||
@[typedef]
|
||||
pub struct C.CONDITION_VARIABLE {}
|
||||
|
||||
//[init_with=new_mutex] // TODO: implement support for this struct attribute, and disallow Mutex{} from outside the sync.new_mutex() function.
|
||||
|
||||
// `SRWLOCK` is much more performant that `Mutex` on Windows, so use that in both cases since we don't
|
||||
// want to share with other processes
|
||||
@[heap]
|
||||
pub struct Mutex {
|
||||
mut:
|
||||
mx C.SRWLOCK // mutex handle
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct RwMutex {
|
||||
mut:
|
||||
mx C.SRWLOCK // mutex handle
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct Semaphore {
|
||||
mtx C.SRWLOCK
|
||||
cond C.CONDITION_VARIABLE
|
||||
mut:
|
||||
count u32
|
||||
}
|
||||
|
||||
pub fn new_mutex() &Mutex {
|
||||
mut m := &Mutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
pub fn new_rwmutex() &RwMutex {
|
||||
mut m := &RwMutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
pub fn (mut m Mutex) init() {
|
||||
C.InitializeSRWLock(&m.mx)
|
||||
}
|
||||
|
||||
pub fn (mut m RwMutex) init() {
|
||||
C.InitializeSRWLock(&m.mx)
|
||||
}
|
||||
|
||||
pub fn (mut m Mutex) lock() {
|
||||
C.AcquireSRWLockExclusive(&m.mx)
|
||||
}
|
||||
|
||||
// try_lock try to lock the mutex instance and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
// NOTE: try_lock require Windows 7 or later. Before Windows 7, it will always return false.
|
||||
// NOTE: To enable try_lock , you should compile your project with `-d windows_7`, like `v . -d windows_7`
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-tryacquiresrwlockexclusive
|
||||
pub fn (mut m Mutex) try_lock() bool {
|
||||
$if windows_7 ? {
|
||||
return C.TryAcquireSRWLockExclusive(&m.mx) != 0
|
||||
} $else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut m Mutex) unlock() {
|
||||
C.ReleaseSRWLockExclusive(&m.mx)
|
||||
}
|
||||
|
||||
// RwMutex has separate read- and write locks
|
||||
pub fn (mut m RwMutex) rlock() {
|
||||
C.AcquireSRWLockShared(&m.mx)
|
||||
}
|
||||
|
||||
pub fn (mut m RwMutex) lock() {
|
||||
C.AcquireSRWLockExclusive(&m.mx)
|
||||
}
|
||||
|
||||
// try_rlock try to lock the given RwMutex instance for reading and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
// NOTE: try_rlock require Windows 7 or later. Before Windows 7, it will always return false.
|
||||
// NOTE: To enable try_rlock , you should compile your project with `-d windows_7`, like `v . -d windows_7`
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-tryacquiresrwlockshared
|
||||
pub fn (mut m RwMutex) try_rlock() bool {
|
||||
$if windows_7 ? {
|
||||
return C.TryAcquireSRWLockShared(&m.mx) != 0
|
||||
} $else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// try_wlock try to lock the given RwMutex instance for writing and return immediately.
|
||||
// If the mutex was already locked, it will return false.
|
||||
// NOTE: try_wlock require Windows 7 or later. Before Windows 7, it will always return false.
|
||||
// NOTE: To enable try_wlock , you should compile your project with `-d windows_7`, like `v . -d windows_7`
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-tryacquiresrwlockexclusive
|
||||
pub fn (mut m RwMutex) try_wlock() bool {
|
||||
$if windows_7 ? {
|
||||
return C.TryAcquireSRWLockExclusive(&m.mx) != 0
|
||||
} $else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Windows SRWLocks have different function to unlock
|
||||
// So provide two functions here, too, to have a common interface
|
||||
pub fn (mut m RwMutex) runlock() {
|
||||
C.ReleaseSRWLockShared(&m.mx)
|
||||
}
|
||||
|
||||
pub fn (mut m RwMutex) unlock() {
|
||||
C.ReleaseSRWLockExclusive(&m.mx)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
pub fn new_semaphore() &Semaphore {
|
||||
return new_semaphore_init(0)
|
||||
}
|
||||
|
||||
pub fn new_semaphore_init(n u32) &Semaphore {
|
||||
mut sem := &Semaphore{}
|
||||
sem.init(n)
|
||||
return sem
|
||||
}
|
||||
|
||||
pub fn (mut sem Semaphore) init(n u32) {
|
||||
C.atomic_store_u32(&sem.count, n)
|
||||
C.InitializeSRWLock(&sem.mtx)
|
||||
C.InitializeConditionVariable(&sem.cond)
|
||||
}
|
||||
|
||||
pub fn (mut sem Semaphore) post() {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 1 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c + 1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
C.AcquireSRWLockExclusive(&sem.mtx)
|
||||
c = C.atomic_fetch_add_u32(voidptr(&sem.count), 1)
|
||||
if c == 0 {
|
||||
C.WakeConditionVariable(&sem.cond)
|
||||
}
|
||||
C.ReleaseSRWLockExclusive(&sem.mtx)
|
||||
}
|
||||
|
||||
pub fn (mut sem Semaphore) wait() {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
C.AcquireSRWLockExclusive(&sem.mtx)
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
|
||||
outer: for {
|
||||
if c == 0 {
|
||||
C.SleepConditionVariableSRW(&sem.cond, &sem.mtx, C.INFINITE, 0)
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
}
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
if c > 1 {
|
||||
C.WakeConditionVariable(&sem.cond)
|
||||
}
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
C.ReleaseSRWLockExclusive(&sem.mtx)
|
||||
}
|
||||
|
||||
pub fn (mut sem Semaphore) try_wait() bool {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
pub fn (mut sem Semaphore) timed_wait(timeout time.Duration) bool {
|
||||
mut c := C.atomic_load_u32(&sem.count)
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
mut ft_start := C._FILETIME{}
|
||||
C.GetSystemTimeAsFileTime(&ft_start)
|
||||
time_end := ((u64(ft_start.dwHighDateTime) << 32) | ft_start.dwLowDateTime) +
|
||||
u64(timeout / (100 * time.nanosecond))
|
||||
mut t_ms := u32(timeout.sys_milliseconds())
|
||||
C.AcquireSRWLockExclusive(&sem.mtx)
|
||||
mut res := 0
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
|
||||
outer: for {
|
||||
if c == 0 {
|
||||
res = C.SleepConditionVariableSRW(&sem.cond, &sem.mtx, t_ms, 0)
|
||||
if res == 0 {
|
||||
break outer
|
||||
}
|
||||
c = C.atomic_load_u32(&sem.count)
|
||||
}
|
||||
for c > 0 {
|
||||
if C.atomic_compare_exchange_weak_u32(&sem.count, &c, c - 1) {
|
||||
if c > 1 {
|
||||
C.WakeConditionVariable(&sem.cond)
|
||||
}
|
||||
break outer
|
||||
}
|
||||
}
|
||||
C.GetSystemTimeAsFileTime(&ft_start)
|
||||
time_now := ((u64(ft_start.dwHighDateTime) << 32) | ft_start.dwLowDateTime) // in 100ns
|
||||
if time_now > time_end {
|
||||
break outer // timeout exceeded
|
||||
}
|
||||
t_ms = u32((time_end - time_now) / 10000)
|
||||
}
|
||||
C.ReleaseSRWLockExclusive(&sem.mtx)
|
||||
return res != 0
|
||||
}
|
||||
|
||||
pub fn (mut m RwMutex) destroy() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
pub fn (mut m Mutex) destroy() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
pub fn (s Semaphore) destroy() {
|
||||
// nothing to do
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
module sync
|
||||
|
||||
fn C.pthread_self() usize
|
||||
|
||||
// thread_id returns a unique identifier for the caller thread.
|
||||
// All *currently* running threads in the same process, will have *different* thread identifiers.
|
||||
// Note: if a thread finishes, and another starts, the identifier of the old thread may be
|
||||
// reused for the newly started thread.
|
||||
// In other words, thread IDs are guaranteed to be unique only within a process.
|
||||
// A thread ID may be reused after a terminated thread has been joined (with `t.wait()`),
|
||||
// or when the thread has terminated.
|
||||
|
||||
pub fn thread_id() u64 {
|
||||
return u64(C.pthread_self())
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import sync
|
||||
|
||||
fn simple_thread() u64 {
|
||||
tid := sync.thread_id()
|
||||
eprintln('simple_thread thread_id: ${tid.hex()}')
|
||||
return tid
|
||||
}
|
||||
|
||||
fn test_sync_thread_id() {
|
||||
mtid := sync.thread_id()
|
||||
eprintln('main thread_id: ${sync.thread_id().hex()}')
|
||||
x := spawn simple_thread()
|
||||
y := spawn simple_thread()
|
||||
xtid := x.wait()
|
||||
ytid := y.wait()
|
||||
eprintln('main thread_id: ${sync.thread_id().hex()}')
|
||||
dump(xtid.hex())
|
||||
dump(ytid.hex())
|
||||
assert mtid != xtid
|
||||
assert mtid != ytid
|
||||
assert xtid != ytid
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module sync
|
||||
|
||||
// Get current thread ID from Windows API (via C interface)
|
||||
fn C.GetCurrentThreadId() u32
|
||||
|
||||
// thread_id returns a unique identifier for the caller thread.
|
||||
pub fn thread_id() u64 {
|
||||
return u64(C.GetCurrentThreadId())
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module threads
|
||||
|
||||
// This module adds the necessary compiler flags for using threads.
|
||||
// It is automatically imported by code that does `go func()` .
|
||||
// See vlib/v/parser/pratt.v, search for ast.GoExpr .
|
||||
// The goal is that programs, that do not use threads at all will not need
|
||||
// to link to -lpthread etc.
|
||||
// Note: on some platforms like Android, linking -lpthread is not needed too.
|
||||
// See https://stackoverflow.com/a/31277163/1904615
|
||||
|
||||
$if !windows && !android {
|
||||
#flag -lpthread
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
module threads
|
||||
|
||||
// This file is just a placeholder.
|
||||
// The actual implementation is backend/platform specific, so see threads.c.v, threads.js.v etc.
|
||||
@@ -0,0 +1,111 @@
|
||||
module sync
|
||||
|
||||
// ThreadLocalStorage provides thread-local storage for values of type T
|
||||
@[noinit]
|
||||
struct ThreadLocalStorage[T] {
|
||||
mut:
|
||||
key u64 // TLS key identifier. Note: While Linux uses unsigned int, Darwin requires unsigned long. u64 accommodates both.
|
||||
in_use bool // Allocation status flag
|
||||
}
|
||||
|
||||
// DataConversion convert voidptr to/from type T
|
||||
union DataConversion {
|
||||
mut:
|
||||
f_u8 u8
|
||||
f_u16 u16
|
||||
f_u32 u32
|
||||
f_u64 u64
|
||||
|
||||
f_i8 i8
|
||||
f_i16 i16
|
||||
f_i32 i32
|
||||
f_i64 i64
|
||||
|
||||
f_voidptr voidptr
|
||||
f_isize isize
|
||||
f_usize usize
|
||||
f_int int
|
||||
f_f32 f32
|
||||
f_f64 f64
|
||||
f_rune rune
|
||||
}
|
||||
|
||||
// convert_t_to_voidptr convert value from type T to voidptr
|
||||
@[inline]
|
||||
fn convert_t_to_voidptr[T](value T) !voidptr {
|
||||
mut f := DataConversion{}
|
||||
$if T is i8 {
|
||||
f.f_i8 = value
|
||||
} $else $if T is i16 {
|
||||
f.f_i16 = value
|
||||
} $else $if T is i32 {
|
||||
f.f_i32 = value
|
||||
} $else $if T is i64 {
|
||||
f.f_i64 = value
|
||||
} $else $if T is u8 {
|
||||
f.f_u8 = value
|
||||
} $else $if T is u16 {
|
||||
f.f_u16 = value
|
||||
} $else $if T is u32 {
|
||||
f.f_u32 = value
|
||||
} $else $if T is u64 {
|
||||
f.f_u64 = value
|
||||
} $else $if T is $pointer {
|
||||
f.f_voidptr = voidptr(value)
|
||||
} $else $if T is isize {
|
||||
f.f_isize = value
|
||||
} $else $if T is usize {
|
||||
f.f_usize = value
|
||||
} $else $if T is int {
|
||||
f.f_int = value
|
||||
} $else $if T is f32 {
|
||||
f.f_f32 = value
|
||||
} $else $if T is f64 {
|
||||
f.f_f64 = value
|
||||
} $else $if T is rune {
|
||||
f.f_rune = value
|
||||
} $else {
|
||||
return error('Unsupported data type `${T.name}` to voidptr')
|
||||
}
|
||||
return unsafe { f.f_voidptr }
|
||||
}
|
||||
|
||||
// convert_voidptr_to_t convert value from voidptr to type T
|
||||
@[inline]
|
||||
fn convert_voidptr_to_t[T](value voidptr) !T {
|
||||
f := DataConversion{
|
||||
f_voidptr: value
|
||||
}
|
||||
$if T is i8 {
|
||||
return unsafe { f.f_i8 }
|
||||
} $else $if T is i16 {
|
||||
return unsafe { f.f_i16 }
|
||||
} $else $if T is i32 {
|
||||
return unsafe { f.f_i32 }
|
||||
} $else $if T is i64 {
|
||||
return unsafe { f.f_i64 }
|
||||
} $else $if T is u8 {
|
||||
return unsafe { f.f_u8 }
|
||||
} $else $if T is u16 {
|
||||
return unsafe { f.f_u16 }
|
||||
} $else $if T is u32 {
|
||||
return unsafe { f.f_u32 }
|
||||
} $else $if T is u64 {
|
||||
return unsafe { f.f_u64 }
|
||||
} $else $if T is $pointer {
|
||||
return unsafe { f.f_voidptr }
|
||||
} $else $if T is isize {
|
||||
return unsafe { f.f_isize }
|
||||
} $else $if T is usize {
|
||||
return unsafe { f.f_usize }
|
||||
} $else $if T is int {
|
||||
return unsafe { f.f_int }
|
||||
} $else $if T is f32 {
|
||||
return unsafe { f.f_f32 }
|
||||
} $else $if T is f64 {
|
||||
return unsafe { f.f_f64 }
|
||||
} $else $if T is rune {
|
||||
return unsafe { f.f_rune }
|
||||
}
|
||||
return error('Unsupported data type `${T.name}` from voidptr')
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
module sync
|
||||
|
||||
// Pthread TLS API functions (via C interface)
|
||||
fn C.pthread_key_create(key voidptr, voidptr) i32
|
||||
fn C.pthread_key_delete(key u64) i32
|
||||
fn C.pthread_setspecific(key u64, const_ptr voidptr) i32
|
||||
fn C.pthread_getspecific(key u64) voidptr
|
||||
|
||||
// new_tls creates new TLS storage initialized with the given `value`
|
||||
pub fn new_tls[T](value T) !ThreadLocalStorage[T] {
|
||||
$if T !in [i8, i16, i32, i64, u8, u16, u32, u64, isize, usize, f32, f64, rune, int, voidptr,
|
||||
$pointer] {
|
||||
return error('new_tls: invalid type ${T.name}')
|
||||
}
|
||||
mut key := u64(0)
|
||||
// Validate key allocation
|
||||
if C.pthread_key_create(voidptr(&key), 0) == 0 {
|
||||
// Initialize storage and verify success
|
||||
if C.pthread_setspecific(key, convert_t_to_voidptr(value)!) == 0 {
|
||||
return ThreadLocalStorage[T]{
|
||||
key: key
|
||||
in_use: true
|
||||
}
|
||||
} else {
|
||||
return error('new_tls: Failed to initialize TLS value: ${value}')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle allocation failure
|
||||
return error('new_tls: Failed to allocate TLS index')
|
||||
}
|
||||
|
||||
// set updates the `value` in TLS storage.
|
||||
@[inline]
|
||||
pub fn (mut t ThreadLocalStorage[T]) set(value T) ! {
|
||||
if t.in_use {
|
||||
if C.pthread_setspecific(t.key, convert_t_to_voidptr(value)!) != 0 {
|
||||
return error('set: Failed to update TLS value: ${value}')
|
||||
}
|
||||
} else {
|
||||
return error('set: TLS storage is already destroyed')
|
||||
}
|
||||
}
|
||||
|
||||
// get retrieves the current value from TLS storage.
|
||||
@[inline]
|
||||
pub fn (mut t ThreadLocalStorage[T]) get() !T {
|
||||
if t.in_use {
|
||||
return convert_voidptr_to_t[T](C.pthread_getspecific(t.key))
|
||||
}
|
||||
return error('get: TLS storage is already destroyed')
|
||||
}
|
||||
|
||||
// destroy releases TLS resources (must be called manually).
|
||||
@[inline]
|
||||
pub fn (mut t ThreadLocalStorage[T]) destroy() ! {
|
||||
if C.pthread_key_delete(t.key) == 0 {
|
||||
t.in_use = false
|
||||
} else {
|
||||
return error('destroy: Failed to release TLS resources')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import sync
|
||||
import time
|
||||
|
||||
// Testing basic data types
|
||||
fn test_basic_tls() {
|
||||
println('--- Testing basic datatypes TLS ---')
|
||||
|
||||
// invalid type
|
||||
sync.new_tls[string]('hello world') or { assert err.msg().contains('invalid type') }
|
||||
|
||||
mut tls_i8 := sync.new_tls[i8](-3)!
|
||||
assert tls_i8.get()! == -3
|
||||
tls_i8.set(-4)!
|
||||
assert tls_i8.get()! == -4
|
||||
tls_i8.destroy()!
|
||||
if v := tls_i8.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_i8.set(-5) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_i16 := sync.new_tls[i16](-57)!
|
||||
assert tls_i16.get()! == -57
|
||||
tls_i16.set(-58)!
|
||||
assert tls_i16.get()! == -58
|
||||
tls_i16.destroy()!
|
||||
if v := tls_i16.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_i16.set(-59) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_i32 := sync.new_tls[i32](-570)!
|
||||
assert tls_i32.get()! == -570
|
||||
tls_i32.set(-580)!
|
||||
assert tls_i32.get()! == -580
|
||||
tls_i32.destroy()!
|
||||
if v := tls_i32.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_i32.set(-59) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_i64 := sync.new_tls[i64](-5700)!
|
||||
assert tls_i64.get()! == -5700
|
||||
tls_i64.set(-5800)!
|
||||
assert tls_i64.get()! == -5800
|
||||
tls_i64.destroy()!
|
||||
if v := tls_i64.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_i64.set(-5900) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_u8 := sync.new_tls[u8](3)!
|
||||
assert tls_u8.get()! == 3
|
||||
tls_u8.set(4)!
|
||||
assert tls_u8.get()! == 4
|
||||
tls_u8.destroy()!
|
||||
if v := tls_u8.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_u8.set(5) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_u16 := sync.new_tls[i16](57)!
|
||||
assert tls_u16.get()! == 57
|
||||
tls_u16.set(58)!
|
||||
assert tls_u16.get()! == 58
|
||||
tls_u16.destroy()!
|
||||
if v := tls_u16.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_u16.set(59) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_u32 := sync.new_tls[u32](570)!
|
||||
assert tls_u32.get()! == 570
|
||||
tls_u32.set(580)!
|
||||
assert tls_u32.get()! == 580
|
||||
tls_u32.destroy()!
|
||||
if v := tls_u32.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_u32.set(590) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_u64 := sync.new_tls[u64](5700)!
|
||||
assert tls_u64.get()! == 5700
|
||||
tls_u64.set(5800)!
|
||||
assert tls_u64.get()! == 5800
|
||||
tls_u64.destroy()!
|
||||
if v := tls_u64.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_u64.set(5900) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_isize := sync.new_tls[isize](-57000)!
|
||||
assert tls_isize.get()! == -57000
|
||||
tls_isize.set(-58000)!
|
||||
assert tls_isize.get()! == -58000
|
||||
tls_isize.destroy()!
|
||||
if v := tls_isize.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_isize.set(-59000) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_usize := sync.new_tls[usize](57000)!
|
||||
assert tls_usize.get()! == 57000
|
||||
tls_usize.set(58000)!
|
||||
assert tls_usize.get()! == 58000
|
||||
tls_usize.destroy()!
|
||||
if v := tls_usize.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_usize.set(59000) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_int := sync.new_tls[int](-32767)!
|
||||
assert tls_int.get()! == -32767
|
||||
tls_int.set(-32768)!
|
||||
assert tls_int.get()! == -32768
|
||||
tls_int.destroy()!
|
||||
if v := tls_int.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_int.set(-32769) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_f32 := sync.new_tls[f32](1.5)!
|
||||
assert tls_f32.get()! == 1.5
|
||||
tls_f32.set(2.5)!
|
||||
assert tls_f32.get()! == 2.5
|
||||
tls_f32.destroy()!
|
||||
if v := tls_f32.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_f32.set(3.5) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
mut tls_f64 := sync.new_tls[f64](-1.5)!
|
||||
assert tls_f64.get()! == -1.5
|
||||
tls_f64.set(-2.5)!
|
||||
assert tls_f64.get()! == -2.5
|
||||
tls_f64.destroy()!
|
||||
if v := tls_f64.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
tls_f64.set(-3.5) or { assert err.msg().contains('already destroyed') }
|
||||
}
|
||||
|
||||
// Testing pointer type
|
||||
struct PtrData {
|
||||
id int
|
||||
val string
|
||||
}
|
||||
|
||||
fn test_pointer_tls() {
|
||||
println('--- Testing pointer TLS ---')
|
||||
|
||||
data := &PtrData{
|
||||
id: 100
|
||||
val: 'initial'
|
||||
}
|
||||
mut tls := sync.new_tls[&PtrData](data) or { panic('Failed to create TLS: ${err}') }
|
||||
|
||||
// Verify initial value
|
||||
assert tls.get()!.id == 100
|
||||
assert tls.get()!.val == 'initial'
|
||||
|
||||
// Update and verify new value
|
||||
new_data := &PtrData{
|
||||
id: 200
|
||||
val: 'updated'
|
||||
}
|
||||
tls.set(new_data)!
|
||||
assert tls.get()!.id == 200
|
||||
assert tls.get()!.val == 'updated'
|
||||
|
||||
tls.destroy()!
|
||||
// Attempt to set value after destruction
|
||||
tls.set(data) or { assert err.msg().contains('already destroyed') }
|
||||
|
||||
// Attempt to get value after destruction
|
||||
if res_get := tls.get() {
|
||||
assert false
|
||||
} else {
|
||||
assert err.msg().contains('already destroyed')
|
||||
}
|
||||
}
|
||||
|
||||
// Thread worker function
|
||||
fn thread_worker(tls &sync.ThreadLocalStorage[u64], thread_id u64) {
|
||||
mut mut_tls := unsafe { tls }
|
||||
// Each thread sets a unique value
|
||||
mut_tls.set(thread_id * 100) or { panic(err) }
|
||||
|
||||
// Simulate work with a delay
|
||||
time.sleep(10 * time.millisecond)
|
||||
|
||||
// Retrieve and verify thread-private value
|
||||
val := mut_tls.get() or { panic(err) }
|
||||
assert val == thread_id * 100
|
||||
println('Thread ${thread_id}: TLS value = ${val}')
|
||||
}
|
||||
|
||||
// Testing multi-thread isolation
|
||||
fn test_thread_isolation() {
|
||||
println('--- Testing multi-thread isolation ---')
|
||||
|
||||
// Create shared TLS instance
|
||||
mut tls := sync.new_tls[u64](3366) or { panic(err) }
|
||||
defer { tls.destroy() or { panic(err) } }
|
||||
|
||||
// Create multiple threads
|
||||
mut threads := []thread{}
|
||||
for i in 1 .. 5 {
|
||||
threads << spawn thread_worker(&tls, u64(i))
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
threads.wait()
|
||||
|
||||
// Verify main thread value remains unchanged
|
||||
println('Main thread value after operations: ${tls.get()!}')
|
||||
assert tls.get()! == 3366
|
||||
}
|
||||
|
||||
// Thread worker function
|
||||
fn worker(int_tls &sync.ThreadLocalStorage[int],
|
||||
f64_tls &sync.ThreadLocalStorage[f64]) {
|
||||
mut mut_int_tls := unsafe { int_tls }
|
||||
mut mut_f64_tls := unsafe { f64_tls }
|
||||
// Update integer value
|
||||
mut_int_tls.set(100) or { panic(err) }
|
||||
v := mut_int_tls.get() or { panic(err) }
|
||||
assert v == 100
|
||||
|
||||
// Update floating-point value
|
||||
mut_f64_tls.set(2.5) or { panic(err) }
|
||||
s := mut_f64_tls.get() or { panic(err) }
|
||||
assert s == 2.5
|
||||
}
|
||||
|
||||
// Testing cross-thread type safety
|
||||
fn test_cross_thread_type() {
|
||||
println('--- Testing cross-thread type safety ---')
|
||||
|
||||
mut int_tls := sync.new_tls[int](42) or { panic(err) }
|
||||
defer { int_tls.destroy() or { panic(err) } }
|
||||
|
||||
mut f64_tls := sync.new_tls[f64](1.5) or { panic(err) }
|
||||
defer { f64_tls.destroy() or { panic(err) } }
|
||||
|
||||
shared_int_tls := &int_tls
|
||||
shared_f64_tls := &f64_tls
|
||||
|
||||
// Create and wait for threads
|
||||
threads := [
|
||||
spawn worker(shared_int_tls, shared_f64_tls),
|
||||
spawn worker(shared_int_tls, shared_f64_tls),
|
||||
]
|
||||
threads.wait()
|
||||
|
||||
// Verify original values in main thread
|
||||
assert int_tls.get()! == 42
|
||||
assert f64_tls.get()! == 1.5
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
module sync
|
||||
|
||||
// Windows TLS API functions (via C interface)
|
||||
fn C.TlsAlloc() u32
|
||||
fn C.TlsSetValue(key u32, voidptr) bool
|
||||
fn C.TlsGetValue(key u32) voidptr
|
||||
fn C.TlsFree(key u32) bool
|
||||
|
||||
// new_tls creates new TLS storage initialized with the given `value`
|
||||
pub fn new_tls[T](value T) !ThreadLocalStorage[T] {
|
||||
$if T !in [i8, i16, i32, i64, u8, u16, u32, u64, isize, usize, f32, f64, rune, int, voidptr,
|
||||
$pointer] {
|
||||
return error('new_tls: invalid type ${T.name}')
|
||||
}
|
||||
|
||||
key := C.TlsAlloc()
|
||||
// Validate key allocation
|
||||
if key != C.TLS_OUT_OF_INDEXES {
|
||||
// Initialize storage and verify success
|
||||
if C.TlsSetValue(key, convert_t_to_voidptr(value)!) {
|
||||
return ThreadLocalStorage[T]{
|
||||
key: key
|
||||
in_use: true
|
||||
}
|
||||
} else {
|
||||
return error('new_tls: Failed to initialize TLS value: ${value}')
|
||||
}
|
||||
}
|
||||
// Handle allocation failure
|
||||
return error('new_tls: Failed to allocate TLS index')
|
||||
}
|
||||
|
||||
// set updates the `value` in TLS storage.
|
||||
@[inline]
|
||||
pub fn (mut t ThreadLocalStorage[T]) set(value T) ! {
|
||||
if t.in_use {
|
||||
if !C.TlsSetValue(t.key, convert_t_to_voidptr(value)!) {
|
||||
return error('set: Failed to update TLS value: ${value}')
|
||||
}
|
||||
} else {
|
||||
return error('set: TLS storage is already destroyed')
|
||||
}
|
||||
}
|
||||
|
||||
// get retrieves the current value from TLS storage.
|
||||
@[inline]
|
||||
pub fn (mut t ThreadLocalStorage[T]) get() !T {
|
||||
if t.in_use {
|
||||
return convert_voidptr_to_t[T](C.TlsGetValue(t.key))
|
||||
}
|
||||
return error('get: TLS storage is already destroyed')
|
||||
}
|
||||
|
||||
// destroy releases TLS resources (must be called manually).
|
||||
@[inline]
|
||||
pub fn (mut t ThreadLocalStorage[T]) destroy() ! {
|
||||
if C.TlsFree(t.key) {
|
||||
t.in_use = false
|
||||
} else {
|
||||
return error('destroy: Failed to release TLS resources')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2019 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module sync
|
||||
|
||||
@[trusted]
|
||||
fn C.atomic_load_u64(voidptr) u64
|
||||
|
||||
@[trusted]
|
||||
fn C.atomic_store_u64(voidptr, u64)
|
||||
|
||||
@[trusted]
|
||||
fn C.atomic_compare_exchange_weak_u64(voidptr, voidptr, u64) bool
|
||||
|
||||
@[trusted]
|
||||
fn C.atomic_fetch_add_u64(voidptr, u64) u64
|
||||
|
||||
// WaitGroup
|
||||
// Do not copy an instance of WaitGroup, use a ref instead.
|
||||
//
|
||||
// usage: in main thread:
|
||||
// `wg := sync.new_waitgroup()`
|
||||
// `wg.add(nr_jobs)` before starting jobs with `go ...`
|
||||
// `wg.wait()` to wait for all jobs to have finished
|
||||
//
|
||||
// in each parallel job:
|
||||
// `wg.done()` when finished
|
||||
//
|
||||
// [init_with=new_waitgroup] // TODO: implement support for init_with struct attribute, and disallow WaitGroup{} from outside the sync.new_waitgroup() function.
|
||||
@[heap]
|
||||
pub struct WaitGroup {
|
||||
mut:
|
||||
state u64 // high 32 bits: task count, low 32 bits: wait count
|
||||
sem Semaphore // Blocks wait() until add() drops the task count to zero
|
||||
}
|
||||
|
||||
// new_waitgroup creates a new WaitGroup.
|
||||
pub fn new_waitgroup() &WaitGroup {
|
||||
mut wg := WaitGroup{}
|
||||
wg.init()
|
||||
return &wg
|
||||
}
|
||||
|
||||
// init initializes a WaitGroup.
|
||||
pub fn (mut wg WaitGroup) init() {
|
||||
C.atomic_store_u64(voidptr(&wg.state), 0)
|
||||
wg.sem.init(0)
|
||||
}
|
||||
|
||||
// add increments (+ve delta) or decrements (-ve delta) task count by delta
|
||||
// and unblocks any wait() calls if task count becomes zero.
|
||||
// add panics if task count drops below zero.
|
||||
pub fn (mut wg WaitGroup) add(delta int) {
|
||||
state_delta := u64(u32(delta)) << 32
|
||||
old_state := C.atomic_fetch_add_u64(voidptr(&wg.state), state_delta)
|
||||
new_state := old_state + state_delta
|
||||
new_nrjobs := int(i32(new_state >> 32))
|
||||
mut num_waiters := u32(new_state)
|
||||
if new_nrjobs < 0 {
|
||||
panic('Negative number of jobs in waitgroup')
|
||||
}
|
||||
if new_nrjobs > 0 || num_waiters == 0 {
|
||||
return
|
||||
}
|
||||
if C.atomic_load_u64(voidptr(&wg.state)) != new_state {
|
||||
panic('WaitGroup misuse: add() called concurrently with wait()')
|
||||
}
|
||||
C.atomic_store_u64(voidptr(&wg.state), 0)
|
||||
for num_waiters > 0 {
|
||||
wg.sem.post()
|
||||
num_waiters--
|
||||
}
|
||||
}
|
||||
|
||||
// done is a convenience fn for add(-1).
|
||||
pub fn (mut wg WaitGroup) done() {
|
||||
wg.add(-1)
|
||||
}
|
||||
|
||||
// wait blocks until all tasks are done (task count becomes zero).
|
||||
pub fn (mut wg WaitGroup) wait() {
|
||||
for {
|
||||
mut state := C.atomic_load_u64(voidptr(&wg.state))
|
||||
nrjobs := u32(state >> 32)
|
||||
if nrjobs == 0 {
|
||||
return
|
||||
}
|
||||
if C.atomic_compare_exchange_weak_u64(voidptr(&wg.state), voidptr(&state), state + 1) {
|
||||
wg.sem.wait() // blocks until task_count becomes 0
|
||||
if C.atomic_load_u64(voidptr(&wg.state)) != 0 {
|
||||
panic('WaitGroup misuse: reused before previous wait() returned')
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// go starts `f` in a new thread, arranging to call wg.add(1) before that,
|
||||
// and wg.done() in the same thread. The function `f` should not panic.
|
||||
// Calls to wg.go() should happen before the call to wg.wait().
|
||||
pub fn (mut wg WaitGroup) go(f fn ()) {
|
||||
wg.add(1)
|
||||
spawn fn (mut wg WaitGroup, f fn ()) {
|
||||
f()
|
||||
wg.done()
|
||||
}(mut wg, f)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// vtest retry: 3
|
||||
// vtest flaky: true
|
||||
module sync
|
||||
|
||||
import time
|
||||
import sync.stdatomic
|
||||
|
||||
fn issue_6870_worker(mut wg WaitGroup, ready chan bool, release chan bool) {
|
||||
ready <- true
|
||||
_ = <-release
|
||||
wg.add(1)
|
||||
wg.done()
|
||||
wg.done()
|
||||
}
|
||||
|
||||
fn test_waitgroup_reuse() {
|
||||
mut wg := new_waitgroup()
|
||||
|
||||
wg.add(1)
|
||||
wg.done()
|
||||
|
||||
wg.add(1)
|
||||
mut executed := false
|
||||
spawn fn (mut wg WaitGroup, executed voidptr) {
|
||||
defer {
|
||||
wg.done()
|
||||
}
|
||||
unsafe {
|
||||
*(&bool(executed)) = true
|
||||
}
|
||||
time.sleep(100 * time.millisecond)
|
||||
}(mut wg, voidptr(&executed))
|
||||
|
||||
wg.wait()
|
||||
assert executed
|
||||
}
|
||||
|
||||
fn test_waitgroup_no_use() {
|
||||
done := chan bool{cap: 1}
|
||||
watchdog := spawn fn (done chan bool) {
|
||||
select {
|
||||
_ := <-done {}
|
||||
10 * time.second {
|
||||
panic('test_waitgroup_no_use did not complete in time')
|
||||
}
|
||||
}
|
||||
}(done)
|
||||
|
||||
mut wg := new_waitgroup()
|
||||
wg.wait()
|
||||
done <- true
|
||||
watchdog.wait()
|
||||
}
|
||||
|
||||
fn test_waitgroup_go() {
|
||||
mut counter := stdatomic.new_atomic(0)
|
||||
mut wg := new_waitgroup()
|
||||
for i in 0 .. 10 {
|
||||
wg.go(fn [mut counter] () {
|
||||
counter.add(1)
|
||||
})
|
||||
}
|
||||
wg.wait()
|
||||
assert counter.load() == 10
|
||||
}
|
||||
|
||||
fn test_waitgroup_add_while_waiting() {
|
||||
for _ in 0 .. 50 {
|
||||
mut wg := new_waitgroup()
|
||||
ready := chan bool{cap: 1}
|
||||
release := chan bool{cap: 1}
|
||||
wait_done := chan bool{cap: 8}
|
||||
wg.add(1)
|
||||
|
||||
spawn issue_6870_worker(mut wg, ready, release)
|
||||
_ = <-ready
|
||||
|
||||
for _ in 0 .. 8 {
|
||||
spawn fn [mut wg, wait_done] () {
|
||||
wg.wait()
|
||||
wait_done <- true
|
||||
}()
|
||||
}
|
||||
release <- true
|
||||
|
||||
for _ in 0 .. 8 {
|
||||
select {
|
||||
_ := <-wait_done {}
|
||||
2 * time.second {
|
||||
assert false, 'wait() missed a wakeup while work added more tasks'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user