|
| 1 | + |
| 2 | + |
1 | 3 | # `conc` - Structured concurrency for Go
|
2 | 4 |
|
| 5 | +[](https://pkg.go.dev/github.com/negrel/conc) |
| 6 | +[](https://goreportcard.com/report/github.com/negrel/conc) |
| 7 | +[](./LICENSE) |
| 8 | +[](https://github.com/negrel/conc/pulls) |
| 9 | + |
| 10 | + |
| 11 | +`conc` is a **structured concurrency** library for Go that provides a safer, |
| 12 | +more intuitive approach to concurrent programming. |
| 13 | + |
| 14 | +By emphasizing proper resource management, error handling, and execution flow, |
| 15 | +`conc` helps developers write concurrent code that is less error-prone, easier |
| 16 | +to reason about, and aligned with established best practices. |
| 17 | + |
| 18 | +```sh |
| 19 | +go get github.com/negrel/conc |
| 20 | +``` |
| 21 | + |
| 22 | +## Predictable code flow |
| 23 | + |
| 24 | +`conc` is based on `nursery` as described in [this blog post](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/). |
| 25 | + |
| 26 | + |
| 27 | +Idea behind nursery is that routines are scoped to a block that returns when |
| 28 | +all goroutines are done. This way, code flow remains sequential outside before |
| 29 | +and after the block. Here is an example: |
| 30 | + |
| 31 | +```go |
| 32 | +func main() { |
| 33 | + conc.Block(func(n conc.Nursery) error { |
| 34 | + // Spawn a goroutine. |
| 35 | + n.Go(func() error { |
| 36 | + return nil |
| 37 | + }) |
| 38 | + |
| 39 | + // Spawn another goroutine. |
| 40 | + n.Go(func() error { |
| 41 | + time.Sleep(2 * time.Second) |
| 42 | + return nil |
| 43 | + }) |
| 44 | + |
| 45 | + // Sleep before returning. |
| 46 | + time.Sleep(time.Second) |
| 47 | + return nil |
| 48 | + }) |
| 49 | + // Once block returns (here after 2 seconds), you're guaranteed that there is no |
| 50 | + // goroutine leak. |
| 51 | + // ... |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +Here is the definition of the `Nursery` interface: |
| 56 | + |
| 57 | +```go |
| 58 | +type Nursery interface { |
| 59 | + context.Context |
| 60 | + Go(func() error) |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +It is a simple extension to [`context.Context`](https://pkg.go.dev/context#Context) that allows spawning routines. |
| 65 | + |
| 66 | +[`Block`](https://pkg.go.dev/github.com/negrel/conc#Block) and |
| 67 | +[`Nursery`](https://pkg.go.dev/github.com/negrel/conc#Nursery) are the core of |
| 68 | +the entire library. |
| 69 | + |
| 70 | +## Explicit goroutines "leak" |
| 71 | + |
| 72 | +Now, let's say you want to write a function spawning routines that outlives it. |
| 73 | +You can pass `Nursery` as a parameter making the leak explicit: |
| 74 | + |
| 75 | +```go |
| 76 | +func workHard(n conc.Nursery) { |
| 77 | + n.Go(func() error { |
| 78 | + return longWork(n) |
| 79 | + }) |
| 80 | + |
| 81 | + n.Go(func() error { |
| 82 | + return longerWork(n) |
| 83 | + }) |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +## And more... |
| 88 | + |
| 89 | +* Graceful panics and errors handling |
| 90 | +* Context integration for deadlines and cancellation |
| 91 | +* Limit maximum number of goroutine used by a block |
| 92 | +* Goroutines are pool allocated to improve efficiency |
| 93 | +* Dependency free |
| 94 | + |
| 95 | +## Performance |
| 96 | + |
| 97 | +Here, an operation means spawning 100 goroutines: |
| 98 | + |
| 99 | +```sh |
| 100 | +$ cd bench/ |
| 101 | +$ go test -v -bench=./... -benchmem ./... |
| 102 | +goos: linux |
| 103 | +goarch: amd64 |
| 104 | +pkg: github.com/negrel/conc/bench |
| 105 | +cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics |
| 106 | +BenchmarkNursery |
| 107 | +BenchmarkNursery/EmptyBlock |
| 108 | +BenchmarkNursery/EmptyBlock-16 1367210 914.3 ns/op 625 B/op 11 allocs/op |
| 109 | +BenchmarkNursery/WithRoutines/NoWork |
| 110 | +BenchmarkNursery/WithRoutines/NoWork-16 26823 43909 ns/op 1514 B/op 65 allocs/op |
| 111 | +BenchmarkNursery/WithRoutines/Nested/NoWork |
| 112 | +BenchmarkNursery/WithRoutines/Nested/NoWork-16 18189 69416 ns/op 3623 B/op 147 allocs/op |
| 113 | +BenchmarkNursery/WithRoutines/1msWork |
| 114 | +BenchmarkNursery/WithRoutines/1msWork-16 940 1300306 ns/op 11941 B/op 211 allocs/op |
| 115 | +BenchmarkNursery/WithRoutines/1-10msWork |
| 116 | +BenchmarkNursery/WithRoutines/1-10msWork-16 123 9681105 ns/op 12427 B/op 292 allocs/op |
| 117 | +BenchmarkNursery/WithRoutines/Error |
| 118 | +BenchmarkNursery/WithRoutines/Error-16 26767 44257 ns/op 1485 B/op 65 allocs/op |
| 119 | +BenchmarkSourceGraphConc |
| 120 | +BenchmarkSourceGraphConc/EmptyPool |
| 121 | +BenchmarkSourceGraphConc/EmptyPool-16 13450243 85.45 ns/op 176 B/op 2 allocs/op |
| 122 | +BenchmarkSourceGraphConc/WithRoutines/NoWork |
| 123 | +BenchmarkSourceGraphConc/WithRoutines/NoWork-16 48522 23928 ns/op 1835 B/op 84 allocs/op |
| 124 | +BenchmarkSourceGraphConc/WithRoutines/1msWork |
| 125 | +BenchmarkSourceGraphConc/WithRoutines/1msWork-16 966 1239140 ns/op 13776 B/op 302 allocs/op |
| 126 | +BenchmarkSourceGraphConc/WithRoutines/1-10msWork |
| 127 | +BenchmarkSourceGraphConc/WithRoutines/1-10msWork-16 123 9625635 ns/op 14030 B/op 372 allocs/op |
| 128 | +BenchmarkGo |
| 129 | +BenchmarkGo/WithRoutines/NoWork |
| 130 | +BenchmarkGo/WithRoutines/NoWork-16 84024 14240 ns/op 1600 B/op 100 allocs/op |
| 131 | +BenchmarkGo/WithRoutines/Nested/NoWork |
| 132 | +BenchmarkGo/WithRoutines/Nested/NoWork-16 81360 14497 ns/op 3199 B/op 199 allocs/op |
| 133 | +BenchmarkGo/WithRoutines/1msWork |
| 134 | +BenchmarkGo/WithRoutines/1msWork-16 59904 18651 ns/op 11215 B/op 200 allocs/op |
| 135 | +BenchmarkGo/WithRoutines/1-10msWork |
| 136 | +BenchmarkGo/WithRoutines/1-10msWork-16 51703 21088 ns/op 11069 B/op 190 allocs/op |
| 137 | +PASS |
| 138 | +ok github.com/negrel/conc/bench 22.347s |
| 139 | +``` |
| 140 | + |
3 | 141 | ## Contributing
|
4 | 142 |
|
5 | 143 | If you want to contribute to `conc` to add a feature or improve the code contact
|
|
0 commit comments