Skip to content

Commit 22e7e78

Browse files
committed
so: conc and sync
1 parent d926b2a commit 22e7e78

32 files changed

Lines changed: 3097 additions & 4 deletions

doc/changelog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ This document outlines the main changes in different So versions.
44

55
## v0.3 (in progress)
66

7+
`conc` package: basic primitives for concurrent programming, backed by pthreads.
8+
9+
- `Chan[T]` — a thread-safe FIFO channel (buffered) or rendezvous (unbuffered).
10+
- `Pool` — a bounded worker pool for fork-join parallelism.
11+
- `Thread` — an operating system thread.
12+
13+
`sync` package: basic synchronization primitives, backed by pthreads.
14+
15+
- `Cond` — a condition variable.
16+
- `Mutex` — a mutual exclusion lock.
17+
- `Once` — runs a function exactly once.
18+
719
You can now use anonymous functions as variable types and function parameters:
820

921
```go

doc/stdlib.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Solod provides a growing set of high-level packages similar to Go's stdlib, and
66
[bytes](#sobytes)
77
[c](#soc)
88
[cmp](#socmp)
9+
[conc](#soconc)
910
[crypto/crand](#socryptocrand)
1011
[encoding/binary](#soencodingbinary)
1112
[encoding/hex](#soencodinghex)
@@ -27,6 +28,7 @@ Solod provides a growing set of high-level packages similar to Go's stdlib, and
2728
[slices](#soslices)
2829
[strconv](#sostrconv)
2930
[strings](#sostrings)
31+
[sync](#sosync)
3032
[time](#sotime)
3133
[unicode](#sounicode)
3234
[unicode/utf8](#sounicodeutf8)
@@ -113,6 +115,34 @@ Types:
113115
- `Func` is a comparison function `func(a, b any) int`.
114116
- `FuncFor` returns the appropriate comparison function for type T.
115117

118+
## [so/conc](https://pkg.go.dev/solod.dev/so/conc)
119+
120+
Basic primitives for concurrent programming, backed by pthreads.
121+
Meant to be used in place of language-level concurrency features.
122+
123+
`Chan[T]` is a thread-safe FIFO channel, similar to Go's built-in `chan T`. It carries pointers (`*T`): a sender hands off ownership of an allocated value and a receiver takes it.
124+
125+
- `NewChan[T]` creates a new channel, either buffered or unbuffered (rendezvous).
126+
- `Chan.Send` sends a pointer to the channel (blocks until delivered).
127+
- `Chan.Recv` receives a pointer from the channel (blocks until a value or close).
128+
- `Chan.SendTimeout` sends with a deadline, returning a status; a zero duration makes it non-blocking.
129+
- `Chan.RecvTimeout` receives with a deadline, returning the pointer and a status; a zero duration makes it non-blocking.
130+
- `Chan.Close` closes the channel.
131+
- `Chan.Free` releases the channel's resources.
132+
133+
`Thread` is a handle to a single OS thread running a `func(any) any`:
134+
135+
- `Go` launches a thread and returns a handle to it.
136+
- `Thread.Wait` blocks until the thread terminates.
137+
- `Thread.Detach` hands the thread's resources to the runtime.
138+
139+
`Pool` is a bounded pool of worker threads for tasks of type `func(any)`:
140+
141+
- `NewPool` creates a pool of workers and starts them.
142+
- `Pool.Go` submits a task for execution.
143+
- `Pool.Wait` blocks until all submitted tasks finish; the pool stays usable afterward.
144+
- `Pool.Free` drains queued tasks, joins the workers, and releases the pool.
145+
116146
## [so/crypto/crand](https://pkg.go.dev/solod.dev/so/crypto/crand)
117147

118148
Cryptographically secure random number generation.
@@ -424,6 +454,31 @@ Types:
424454
- `Builder` efficiently builds a string, minimizing memory copying.
425455
- `Reader` reads data from a string.
426456

457+
## [so/sync](https://pkg.go.dev/solod.dev/so/sync)
458+
459+
Basic synchronization primitives, backed by pthreads.
460+
461+
`Mutex` is a mutual exclusion lock:
462+
463+
- `Mutex.Init` prepares the mutex for use, leaving it unlocked.
464+
- `Mutex.Lock` and `Mutex.Unlock` acquire and release the lock.
465+
- `Mutex.TryLock` tries to acquire the lock and reports whether it succeeded.
466+
- `Mutex.Free` releases the mutex's resources.
467+
468+
`Cond` is a condition variable tied to a `*Mutex`:
469+
470+
- `Cond.Init` prepares the condition variable, guarded by the given mutex.
471+
- `Cond.Wait` atomically unlocks the mutex and blocks until signaled, then re-locks.
472+
- `Cond.WaitFor` waits like `Cond.Wait` but gives up after a given duration.
473+
- `Cond.Signal` and `Cond.Broadcast` wake one or all waiting threads.
474+
- `Cond.Free` releases the condition variable's resources.
475+
476+
`Once` runs a function exactly once, even when called concurrently:
477+
478+
- `Once.Init` prepares the once for use.
479+
- `Once.Do` runs the given function on the first call only.
480+
- `Once.Free` releases the once's resources.
481+
427482
## [so/time](https://pkg.go.dev/solod.dev/so/time)
428483

429484
Measuring and displaying time. Offers an API similar to Go's `time` package, but handles locations, formatting, and parsing differently.

internal/compiler/builtin/builtin.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,8 @@ typedef struct { uint32_t val; uint32_t val2; } so_R_u32_u32;
553553
typedef struct { uint64_t val; bool val2; } so_R_u64_bool;
554554
typedef struct { uint64_t val; so_int val2; } so_R_u64_int;
555555
typedef struct { uint64_t val; uint64_t val2; } so_R_u64_u64;
556+
typedef struct { void* val; bool val2; } so_R_ptr_bool;
557+
typedef struct { void* val; so_int val2; } so_R_ptr_int;
556558

557559
// clang-format on
558560

so/conc/buffer.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package conc
2+
3+
import (
4+
"solod.dev/so/c"
5+
"solod.dev/so/mem"
6+
"solod.dev/so/sync"
7+
"solod.dev/so/time"
8+
)
9+
10+
// Status reports the outcome of a timed channel operation.
11+
type Status int
12+
13+
const (
14+
Ok Status = iota // the value was transferred
15+
Timeout // the deadline elapsed before a transfer
16+
Closed // the channel was closed
17+
)
18+
19+
// Buffer is the non-generic engine behind a buffered [Chan]:
20+
// a thread-safe FIFO of items stored in a ring buffer. In most cases,
21+
// using [Chan] is more convenient.
22+
type Buffer struct {
23+
alloc mem.Allocator
24+
25+
mu sync.Mutex
26+
notEmpty sync.Cond // signaled when an item becomes available
27+
notFull sync.Cond // signaled when a slot frees
28+
29+
// buf is a ring buffer of items in the channel.
30+
// bhead and btail are indices into buf,
31+
// and bcount is the number of items buffered.
32+
buf []any
33+
bhead int
34+
btail int
35+
bcount int
36+
37+
closed bool // true after Close
38+
}
39+
40+
// NewBuffer creates a buffered channel holding up to size items.
41+
func NewBuffer(alloc mem.Allocator, size int) *Buffer {
42+
c.Assert(size > 0, "conc: buffered chan size must be > 0")
43+
44+
ch := mem.Alloc[Buffer](alloc)
45+
ch.alloc = alloc
46+
ch.buf = mem.AllocSlice[any](alloc, size, size)
47+
ch.bhead, ch.btail, ch.bcount = 0, 0, 0
48+
ch.closed = false
49+
50+
ch.mu.Init()
51+
ch.notEmpty.Init(&ch.mu)
52+
ch.notFull.Init(&ch.mu)
53+
return ch
54+
}
55+
56+
// Send stores v in the channel, blocking while the channel is full
57+
// (back-pressure). Panics if the channel is closed.
58+
func (ch *Buffer) Send(v any) {
59+
ch.mu.Lock()
60+
for ch.bfull() && !ch.closed {
61+
ch.notFull.Wait()
62+
}
63+
if ch.closed {
64+
ch.mu.Unlock()
65+
panic("conc: send on closed channel")
66+
}
67+
ch.bpush(v)
68+
ch.notEmpty.Signal()
69+
ch.mu.Unlock()
70+
}
71+
72+
// SendTimeout stores v, waiting up to d for room if the buffer is full. A
73+
// zero or negative d makes it non-blocking.
74+
//
75+
// Returns Ok if the value was stored, Timeout if the deadline passed while
76+
// the buffer stayed full, or Closed if the channel is closed.
77+
func (ch *Buffer) SendTimeout(v any, d time.Duration) Status {
78+
deadline := time.Now().Add(d)
79+
ch.mu.Lock()
80+
timedOut := false
81+
for ch.bfull() && !ch.closed && !timedOut {
82+
dur := int64(time.Until(deadline))
83+
timedOut = ch.notFull.WaitFor(dur)
84+
}
85+
if ch.closed {
86+
ch.mu.Unlock()
87+
return Closed
88+
}
89+
if ch.bfull() {
90+
// Still full: the deadline passed before a slot freed.
91+
ch.mu.Unlock()
92+
return Timeout
93+
}
94+
// A slot may have freed right at the deadline; store anyway.
95+
ch.bpush(v)
96+
ch.notEmpty.Signal()
97+
ch.mu.Unlock()
98+
return Ok
99+
}
100+
101+
// Recv takes the next value from the channel. It reports whether a value was
102+
// received: false means the channel is closed and drained.
103+
func (ch *Buffer) Recv() (any, bool) {
104+
ch.mu.Lock()
105+
for ch.bempty() && !ch.closed {
106+
ch.notEmpty.Wait()
107+
}
108+
if ch.bempty() && ch.closed {
109+
ch.mu.Unlock()
110+
return nil, false
111+
}
112+
v := ch.bpop()
113+
ch.notFull.Signal()
114+
ch.mu.Unlock()
115+
return v, true
116+
}
117+
118+
// RecvTimeout takes the next value, waiting up to d for one if the buffer is
119+
// empty. A zero or negative d makes it non-blocking.
120+
//
121+
// Returns the value with Ok, or nil with Timeout if the deadline passed while
122+
// the buffer stayed empty, or nil with Closed if the channel is closed and drained.
123+
func (ch *Buffer) RecvTimeout(d time.Duration) (any, Status) {
124+
deadline := time.Now().Add(d)
125+
ch.mu.Lock()
126+
timedOut := false
127+
for ch.bempty() && !ch.closed && !timedOut {
128+
dur := int64(time.Until(deadline))
129+
timedOut = ch.notEmpty.WaitFor(dur)
130+
}
131+
if !ch.bempty() {
132+
// A value is available (possibly delivered right at the deadline), so
133+
// it wins over both close and timeout.
134+
v := ch.bpop()
135+
ch.notFull.Signal()
136+
ch.mu.Unlock()
137+
return v, Ok
138+
}
139+
if ch.closed {
140+
ch.mu.Unlock()
141+
return nil, Closed
142+
}
143+
ch.mu.Unlock()
144+
return nil, Timeout
145+
}
146+
147+
// Close marks the channel closed. Subsequent sends panic; receivers drain any
148+
// buffered items and then return false. Closing a closed channel panics.
149+
func (ch *Buffer) Close() {
150+
ch.mu.Lock()
151+
if ch.closed {
152+
ch.mu.Unlock()
153+
panic("conc: close of closed channel")
154+
}
155+
ch.closed = true
156+
ch.notEmpty.Broadcast()
157+
ch.notFull.Broadcast()
158+
ch.mu.Unlock()
159+
}
160+
161+
// bfull reports whether the ring buffer is at capacity.
162+
func (ch *Buffer) bfull() bool { return ch.bcount == len(ch.buf) }
163+
164+
// bempty reports whether the ring buffer is empty.
165+
func (ch *Buffer) bempty() bool { return ch.bcount == 0 }
166+
167+
// bpush appends v to the tail of the ring buffer.
168+
func (ch *Buffer) bpush(v any) {
169+
ch.buf[ch.btail] = v
170+
ch.btail = (ch.btail + 1) % len(ch.buf)
171+
ch.bcount++
172+
}
173+
174+
// bpop removes and returns the item at the head of the ring buffer.
175+
func (ch *Buffer) bpop() any {
176+
v := ch.buf[ch.bhead]
177+
ch.bhead = (ch.bhead + 1) % len(ch.buf)
178+
ch.bcount--
179+
return v
180+
}
181+
182+
// Free releases the channel's resources. The channel is unusable afterward.
183+
// Call it once fully done; a channel may be drained after Close.
184+
func (ch *Buffer) Free() {
185+
ch.mu.Free()
186+
ch.notEmpty.Free()
187+
ch.notFull.Free()
188+
mem.FreeSlice(ch.alloc, ch.buf)
189+
mem.Free(ch.alloc, ch)
190+
}

0 commit comments

Comments
 (0)