Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
## Description
`sync` provides cross platform handling of concurrency primitives.
+38
View File
@@ -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)
}
+66
View File
@@ -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}')
}
}
}
+48
View File
@@ -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 |
+89
View File
@@ -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')!
}
+10
View File
@@ -0,0 +1,10 @@
module sync
struct ChanStruct {
workers []chan bool
}
fn test_chan_mod_sync() {
_ := chan bool{cap: 1}
assert true
}
+23
View File
@@ -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)
}
+17
View File
@@ -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
}
+32
View File
@@ -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
}
+32
View File
@@ -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
}
+104
View File
@@ -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()
}
+20
View File
@@ -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
}
+54
View File
@@ -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)
}
+62
View File
@@ -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
}
+181
View File
@@ -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
}
+43
View File
@@ -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
}
+61
View File
@@ -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
}
+77
View File
@@ -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'
}
+91
View File
@@ -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
}
+17
View File
@@ -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
}
+699
View File
@@ -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
}
+27
View File
@@ -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)
}
+11
View File
@@ -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)})'
}
+87
View File
@@ -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()
}
+191
View File
@@ -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
}
+36
View File
@@ -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()
}
+50
View File
@@ -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
}
+42
View File
@@ -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()
}
+66
View File
@@ -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()
}
+34
View File
@@ -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
}
+38
View File
@@ -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
}
+36
View File
@@ -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.
+191
View File
@@ -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]
}
+100
View File
@@ -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]
}
+74
View File
@@ -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()
}
+94
View File
@@ -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
}
+42
View File
@@ -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>
}
+333
View File
@@ -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')
}
+278
View File
@@ -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()
}
+14
View File
@@ -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
}
+124
View File
@@ -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)
}
}
+351
View File
@@ -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))
}
+320
View File
@@ -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))
}
+339
View File
@@ -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))
}
+263
View File
@@ -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
}
+15
View File
@@ -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())
}
+22
View File
@@ -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
}
+9
View File
@@ -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())
}
+13
View File
@@ -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
}
+4
View File
@@ -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.
+111
View File
@@ -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')
}
+62
View File
@@ -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')
}
}
+284
View File
@@ -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
}
+62
View File
@@ -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')
}
}
+107
View File
@@ -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)
}
+95
View File
@@ -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'
}
}
}
}
}