Skip to content

Commit

Permalink
Merge pull request #4 from chrismwendt/generics
Browse files Browse the repository at this point in the history
Generics
  • Loading branch information
anacrolix authored Jun 13, 2022
2 parents 88b3fdf + e6ac933 commit 4f6e80b
Show file tree
Hide file tree
Showing 19 changed files with 286 additions and 285 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ jobs:
build:
machine: true
environment:
GO_BRANCH: release-branch.go1.15
GO_BRANCH: master
steps:
- run: echo $CIRCLE_WORKING_DIRECTORY
- run: echo $PWD
Expand Down
33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ composition will either deadlock or release the lock between functions (making
it non-atomic).

The `stm` API tries to mimic that of Haskell's [`Control.Concurrent.STM`](https://hackage.haskell.org/package/stm-2.4.4.1/docs/Control-Concurrent-STM.html), but
this is not entirely possible due to Go's type system; we are forced to use
`interface{}` and type assertions. Furthermore, Haskell can enforce at compile
time that STM variables are not modified outside the STM monad. This is not
possible in Go, so be especially careful when using pointers in your STM code.
Haskell can enforce at compile time that STM variables are not modified outside
the STM monad. This is not possible in Go, so be especially careful when using
pointers in your STM code. Remember: modifying a pointer is a side effect!

Unlike Haskell, data in Go is not immutable by default, which means you have
to be careful when using STM to manage pointers. If two goroutines have access
Expand All @@ -31,22 +30,22 @@ applications in Go. If you find this package useful, please tell us about it!

See the package examples in the Go package docs for examples of common operations.

See [example_santa_test.go](example_santa_test.go) for a more complex example.
See [cmd/santa-example/main.go](cmd/santa-example/main.go) for a more complex example.

## Pointers

Note that `Operation` now returns a value of type `interface{}`, which isn't included in the
Note that `Operation` now returns a value of type `any`, which isn't included in the
examples throughout the documentation yet. See the type signatures for `Atomically` and `Operation`.

Be very careful when managing pointers inside transactions! (This includes
slices, maps, channels, and captured variables.) Here's why:

```go
p := stm.NewVar([]byte{1,2,3})
p := stm.NewVar[[]byte]([]byte{1,2,3})
stm.Atomically(func(tx *stm.Tx) {
b := tx.Get(p).([]byte)
b := p.Get(tx)
b[0] = 7
tx.Set(p, b)
stm.p.Set(tx, b)
})
```

Expand All @@ -57,11 +56,11 @@ Following this advice, we can rewrite the transaction to perform a copy:

```go
stm.Atomically(func(tx *stm.Tx) {
b := tx.Get(p).([]byte)
b := p.Get(tx)
c := make([]byte, len(b))
copy(c, b)
c[0] = 7
tx.Set(p, c)
p.Set(tx, c)
})
```

Expand All @@ -73,11 +72,11 @@ In the same vein, it would be a mistake to do this:
type foo struct {
i int
}
p := stm.NewVar(&foo{i: 2})
p := stm.NewVar[*foo](&foo{i: 2})
stm.Atomically(func(tx *stm.Tx) {
f := tx.Get(p).(*foo)
f := p.Get(tx)
f.i = 7
tx.Set(p, f)
stm.p.Set(tx, f)
})
```

Expand All @@ -88,11 +87,11 @@ the correct approach is to move the `Var` inside the struct:
type foo struct {
i *stm.Var
}
f := foo{i: stm.NewVar(2)}
f := foo{i: stm.NewVar[int](2)}
stm.Atomically(func(tx *stm.Tx) {
i := tx.Get(f.i).(int)
i := f.i.Get(tx)
i = 7
tx.Set(f.i, i)
f.i.Set(tx, i)
})
```

Expand Down
6 changes: 3 additions & 3 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ func BenchmarkIncrementSTM(b *testing.B) {
x := NewVar(0)
for i := 0; i < 1000; i++ {
go Atomically(VoidOperation(func(tx *Tx) {
cur := tx.Get(x).(int)
tx.Set(x, cur+1)
cur := x.Get(tx)
x.Set(tx, cur+1)
}))
}
// wait for x to reach 1000
Atomically(VoidOperation(func(tx *Tx) {
tx.Assert(tx.Get(x).(int) == 1000)
tx.Assert(x.Get(tx) == 1000)
}))
}
}
Expand Down
34 changes: 17 additions & 17 deletions cmd/santa-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ import (

type gate struct {
capacity int
remaining *stm.Var
remaining *stm.Var[int]
}

func (g gate) pass() {
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
rem := tx.Get(g.remaining).(int)
rem := g.remaining.Get(tx)
// wait until gate can hold us
tx.Assert(rem > 0)
tx.Set(g.remaining, rem-1)
g.remaining.Set(tx, rem-1)
}))
}

Expand All @@ -56,7 +56,7 @@ func (g gate) operate() {
stm.AtomicSet(g.remaining, g.capacity)
// wait for gate to be full
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
rem := tx.Get(g.remaining).(int)
rem := g.remaining.Get(tx)
tx.Assert(rem == 0)
}))
}
Expand All @@ -70,8 +70,8 @@ func newGate(capacity int) gate {

type group struct {
capacity int
remaining *stm.Var
gate1, gate2 *stm.Var
remaining *stm.Var[int]
gate1, gate2 *stm.Var[gate]
}

func newGroup(capacity int) *group {
Expand All @@ -85,28 +85,28 @@ func newGroup(capacity int) *group {

func (g *group) join() (g1, g2 gate) {
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
rem := tx.Get(g.remaining).(int)
rem := g.remaining.Get(tx)
// wait until the group can hold us
tx.Assert(rem > 0)
tx.Set(g.remaining, rem-1)
g.remaining.Set(tx, rem-1)
// return the group's gates
g1 = tx.Get(g.gate1).(gate)
g2 = tx.Get(g.gate2).(gate)
g1 = g.gate1.Get(tx)
g2 = g.gate2.Get(tx)
}))
return
}

func (g *group) await(tx *stm.Tx) (gate, gate) {
// wait for group to be empty
rem := tx.Get(g.remaining).(int)
rem := g.remaining.Get(tx)
tx.Assert(rem == 0)
// get the group's gates
g1 := tx.Get(g.gate1).(gate)
g2 := tx.Get(g.gate2).(gate)
g1 := g.gate1.Get(tx)
g2 := g.gate2.Get(tx)
// reset group
tx.Set(g.remaining, g.capacity)
tx.Set(g.gate1, newGate(g.capacity))
tx.Set(g.gate2, newGate(g.capacity))
g.remaining.Set(tx, g.capacity)
g.gate1.Set(tx, newGate(g.capacity))
g.gate2.Set(tx, newGate(g.capacity))
return g1, g2
}

Expand Down Expand Up @@ -137,7 +137,7 @@ type selection struct {
gate1, gate2 gate
}

func chooseGroup(g *group, task string, s *selection) stm.Operation {
func chooseGroup(g *group, task string, s *selection) stm.Operation[struct{}] {
return stm.VoidOperation(func(tx *stm.Tx) {
s.gate1, s.gate2 = g.await(tx)
s.task = task
Expand Down
26 changes: 12 additions & 14 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ it non-atomic).
To begin, create an STM object that wraps the data you want to access
concurrently.
x := stm.NewVar(3)
x := stm.NewVar[int](3)
You can then use the Atomically method to atomically read and/or write the the
data. This code atomically decrements x:
stm.Atomically(func(tx *stm.Tx) {
cur := tx.Get(x).(int)
tx.Set(x, cur-1)
cur := x.Get(tx)
x.Set(tx, cur-1)
})
An important part of STM transactions is retrying. At any point during the
Expand All @@ -29,11 +29,11 @@ updated before the transaction will be rerun. As an example, this code will
try to decrement x, but will block as long as x is zero:
stm.Atomically(func(tx *stm.Tx) {
cur := tx.Get(x).(int)
cur := x.Get(tx)
if cur == 0 {
tx.Retry()
}
tx.Set(x, cur-1)
x.Set(tx, cur-1)
})
Internally, tx.Retry simply calls panic(stm.Retry). Panicking with any other
Expand All @@ -47,13 +47,13 @@ retried. For example, this code implements the "decrement-if-nonzero"
transaction above, but for two values. It will first try to decrement x, then
y, and block if both values are zero.
func dec(v *stm.Var) {
func dec(v *stm.Var[int]) {
return func(tx *stm.Tx) {
cur := tx.Get(v).(int)
cur := v.Get(tx)
if cur == 0 {
tx.Retry()
}
tx.Set(v, cur-1)
v.Set(tx, cur-1)
}
}
Expand All @@ -69,11 +69,9 @@ behavior. One common way to get around this is to build up a list of impure
operations inside the transaction, and then perform them after the transaction
completes.
The stm API tries to mimic that of Haskell's Control.Concurrent.STM, but this
is not entirely possible due to Go's type system; we are forced to use
interface{} and type assertions. Furthermore, Haskell can enforce at compile
time that STM variables are not modified outside the STM monad. This is not
possible in Go, so be especially careful when using pointers in your STM code.
Remember: modifying a pointer is a side effect!
The stm API tries to mimic that of Haskell's Control.Concurrent.STM, but
Haskell can enforce at compile time that STM variables are not modified outside
the STM monad. This is not possible in Go, so be especially careful when using
pointers in your STM code. Remember: modifying a pointer is a side effect!
*/
package stm
22 changes: 11 additions & 11 deletions doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,38 @@ func Example() {
// read a variable
var v int
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
v = tx.Get(n).(int)
v = n.Get(tx)
}))
// or:
v = stm.AtomicGet(n).(int)
v = stm.AtomicGet(n)
_ = v

// write to a variable
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
tx.Set(n, 12)
n.Set(tx, 12)
}))
// or:
stm.AtomicSet(n, 12)

// update a variable
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
cur := tx.Get(n).(int)
tx.Set(n, cur-1)
cur := n.Get(tx)
n.Set(tx, cur-1)
}))

// block until a condition is met
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
cur := tx.Get(n).(int)
cur := n.Get(tx)
if cur != 0 {
tx.Retry()
}
tx.Set(n, 10)
n.Set(tx, 10)
}))
// or:
stm.Atomically(stm.VoidOperation(func(tx *stm.Tx) {
cur := tx.Get(n).(int)
cur := n.Get(tx)
tx.Assert(cur == 0)
tx.Set(n, 10)
n.Set(tx, 10)
}))

// select among multiple (potentially blocking) transactions
Expand All @@ -51,11 +51,11 @@ func Example() {
stm.VoidOperation(func(tx *stm.Tx) { tx.Retry() }),

// this function will always succeed without blocking
stm.VoidOperation(func(tx *stm.Tx) { tx.Set(n, 10) }),
stm.VoidOperation(func(tx *stm.Tx) { n.Set(tx, 10) }),

// this function will never run, because the previous
// function succeeded
stm.VoidOperation(func(tx *stm.Tx) { tx.Set(n, 11) }),
stm.VoidOperation(func(tx *stm.Tx) { n.Set(tx, 11) }),
))

// since Select is a normal transaction, if the entire select retries
Expand Down
Loading

0 comments on commit 4f6e80b

Please sign in to comment.