New Upstream Release - golang-github-arceliar-phony
Ready changes
Summary
Merged new upstream version: 0.0~git20220903.530938a (was: 0.0~git20210209.dde1a8d).
Resulting package
Built on 2022-11-23T23:38 (took 2m38s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases golang-github-arceliar-phony-dev
Lintian Result
- golang-github-arceliar-phony-dev_0.0~git20220903.530938a-1~jan+nur1_all.deb
- golang-github-arceliar-phony_0.0~git20220903.530938a-1~jan+nur1.dsc
- golang-github-arceliar-phony_0.0~git20220903.530938a-1~jan+nur1_amd64.buildinfo
- golang-github-arceliar-phony_0.0~git20220903.530938a-1~jan+nur1_amd64.changes
Diff
diff --git a/README.md b/README.md
index d6f4645..e8f0df0 100644
--- a/README.md
+++ b/README.md
@@ -11,15 +11,16 @@ Phony is a [Pony](https://ponylang.io/)-inspired proof-of-concept implementation
goos: linux
goarch: amd64
pkg: github.com/Arceliar/phony
-BenchmarkLoopActor-4 15617646 71.1 ns/op 16 B/op 1 allocs/op
-BenchmarkLoopChannel-4 14870767 73.0 ns/op 0 B/op 0 allocs/op
-BenchmarkSendActor-4 3268095 377 ns/op 32 B/op 2 allocs/op
-BenchmarkSendChannel-4 2598151 442 ns/op 0 B/op 0 allocs/op
-BenchmarkRequestResponseActor-4 2256913 527 ns/op 48 B/op 3 allocs/op
-BenchmarkRequestResponseChannel-4 1257068 869 ns/op 0 B/op 0 allocs/op
-BenchmarkBlock-4 780747 1586 ns/op 144 B/op 3 allocs/op
+cpu: Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz
+BenchmarkLoopActor-8 38022962 29.83 ns/op 0 B/op 0 allocs/op
+BenchmarkLoopChannel-8 29876192 38.50 ns/op 0 B/op 0 allocs/op
+BenchmarkSendActor-8 14235270 82.94 ns/op 0 B/op 0 allocs/op
+BenchmarkSendChannel-8 8372472 143.9 ns/op 0 B/op 0 allocs/op
+BenchmarkRequestResponseActor-8 10360731 116.6 ns/op 0 B/op 0 allocs/op
+BenchmarkRequestResponseChannel-8 4226506 285.8 ns/op 0 B/op 0 allocs/op
+BenchmarkBlock-8 2662929 450.9 ns/op 32 B/op 2 allocs/op
PASS
-ok github.com/Arceliar/phony 12.677s
+ok github.com/Arceliar/phony 9.463s
```
These are microbenchmarks, but they seem to indicate that `Actor` messaging and goroutine+channel operations have comparable cost. I suspect that the difference is negligible in most applications.
@@ -29,9 +30,9 @@ These are microbenchmarks, but they seem to indicate that `Actor` messaging and
The code base is short, under 100 source lines of code as of writing, so reading the code is probably the best way to see *what* it does, but that doesn't necessarily explain *why* certain design decisions were made. To elaborate on a few things:
- Phony only depends on packages from the standard library:
- - `runtime` for some scheduler manipulation (through `Goexit()` and `Gosched()`).
+ - `runtime` for some scheduler manipulation (`Gosched()`).
+ - `sync` for `sync.Pool`, to minimize allocations.
- `sync/atomic` to implement the `Inbox`'s message queues.
- - `unsafe` to use `atomic`'s `unsafe.Pointer` operations, which the paranoid should audit themselves for correctness.
- Attempts were make to make embedding and composition work:
- `Actor` is an `interface` satisfied by the `Inbox` `struct`.
@@ -48,6 +49,5 @@ The code base is short, under 100 source lines of code as of writing, so reading
- The implementation aims to be as lightweight as reasonably possible:
- On `x86_64`, an empty `Inbox` is 24 bytes, and messages overhead is 16 bytes, or half that on `x86`.
- An `Actor` with an empty `Inbox` has no goroutine.
- - An `Actor` that has stopped due to backpressure also has no goroutine.
- This means that idle `Actor`s can be collected as garbage when they're no longer reachable, just like any other `struct`.
diff --git a/actor.go b/actor.go
index 9537514..2869e75 100644
--- a/actor.go
+++ b/actor.go
@@ -2,14 +2,17 @@ package phony
import (
"runtime"
+ "sync"
"sync/atomic"
- "unsafe"
)
+var stops = sync.Pool{New: func() interface{} { return make(chan struct{}, 1) }}
+var elems = sync.Pool{New: func() interface{} { return new(queueElem) }}
+
// A message in the queue
type queueElem struct {
msg func()
- next unsafe.Pointer // *queueElem, accessed atomically
+ next atomic.Pointer[queueElem] // *queueElem, accessed atomically
}
// Inbox is an ordered queue of messages which an Actor will process sequentially.
@@ -19,9 +22,9 @@ type queueElem struct {
// An Inbox must not be copied after first use.
type Inbox struct {
noCopy noCopy
- head *queueElem // Used carefully to avoid needing atomics
- tail unsafe.Pointer // *queueElem, accessed atomically
- busy uintptr // accessed atomically, 1 if sends should apply backpressure
+ head *queueElem // Used carefully to avoid needing atomics
+ tail atomic.Pointer[queueElem] // *queueElem, accessed atomically
+ busy atomic.Bool // accessed atomically, 1 if sends should apply backpressure
}
// Actor is the interface for Actors, based on their ability to receive a message from another Actor.
@@ -36,11 +39,12 @@ type Actor interface {
// enqueue puts a message into the Inbox and returns true if backpressure should be applied.
// If the inbox was empty, then the actor was not already running, so enqueue starts it.
func (a *Inbox) enqueue(msg func()) {
- q := &queueElem{msg: msg}
- tail := (*queueElem)(atomic.SwapPointer(&a.tail, unsafe.Pointer(q)))
+ q := elems.Get().(*queueElem)
+ *q = queueElem{msg: msg}
+ tail := a.tail.Swap(q)
if tail != nil {
//An old tail exists, so update its next pointer to reference q
- atomic.StorePointer(&tail.next, unsafe.Pointer(q))
+ tail.next.Store(q)
} else {
// No old tail existed, so no worker is currently running
// Update the head to point to q, then start the worker
@@ -59,10 +63,13 @@ func (a *Inbox) Act(from Actor, action func()) {
panic("tried to send nil action")
}
a.enqueue(action)
- if from != nil && atomic.LoadUintptr(&a.busy) != 0 {
- s := stop{from: from}
- a.enqueue(s.signal)
- from.enqueue(s.wait)
+ if from != nil && a.busy.Load() {
+ done := stops.Get().(chan struct{})
+ a.enqueue(func() { done <- struct{}{} })
+ from.enqueue(func() {
+ <-done
+ stops.Put(done)
+ })
}
}
@@ -76,15 +83,17 @@ func Block(actor Actor, action func()) {
} else if action == nil {
panic("tried to send nil action")
}
- done := make(chan struct{})
- actor.enqueue(func() { action(); close(done) })
+ done := stops.Get().(chan struct{})
+ actor.enqueue(action)
+ actor.enqueue(func() { done <- struct{}{} })
<-done
+ stops.Put(done)
}
// run is executed when a message is placed in an empty Inbox, and launches a worker goroutine.
// The worker goroutine processes messages from the Inbox until empty, and then exits.
func (a *Inbox) run() {
- atomic.StoreUintptr(&a.busy, 1)
+ a.busy.Store(true)
for running := true; running; running = a.advance() {
a.head.msg()
}
@@ -93,27 +102,29 @@ func (a *Inbox) run() {
// returns true if we still have more work to do
func (a *Inbox) advance() (more bool) {
head := a.head
- a.head = (*queueElem)(atomic.LoadPointer(&head.next))
+ a.head = head.next.Load()
if a.head == nil {
// We loaded the last message
// Unset busy and CAS the tail to nil to shut down
- atomic.StoreUintptr(&a.busy, 0)
- if !atomic.CompareAndSwapPointer(&a.tail, unsafe.Pointer(head), nil) {
+ a.busy.Store(false)
+ if !a.tail.CompareAndSwap(head, nil) {
// Someone pushed to the list before we could CAS the tail to shut down
// This means we're effectively restarting at this point
// Set busy and load the next message
- atomic.StoreUintptr(&a.busy, 1)
+ a.busy.Store(true)
for a.head == nil {
// Busy loop until the message is successfully loaded
// Gosched to avoid blocking the thread in the mean time
runtime.Gosched()
- a.head = (*queueElem)(atomic.LoadPointer(&head.next))
+ a.head = head.next.Load()
}
more = true
}
} else {
more = true
}
+ *head = queueElem{}
+ elems.Put(head)
return
}
@@ -121,24 +132,7 @@ func (a *Inbox) restart() {
go a.run()
}
-type stop struct {
- flag uintptr
- from Actor
-}
-
-func (s *stop) signal() {
- if atomic.SwapUintptr((*uintptr)(&s.flag), 1) != 0 && s.from.advance() {
- s.from.restart()
- }
-}
-
-func (s *stop) wait() {
- if atomic.SwapUintptr((*uintptr)(&s.flag), 1) == 0 {
- runtime.Goexit()
- }
-}
-
-// noCopy implements the sync.Locker interface so go vet can catch unsafe copying
+// noCopy implements the sync.Locker interface, so go vet can catch unsafe copying
type noCopy struct{}
func (n *noCopy) Lock() {}
diff --git a/debian/changelog b/debian/changelog
index 62f7c65..104b540 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-arceliar-phony (0.0~git20220903.530938a-1) UNRELEASED; urgency=low
+
+ * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk> Wed, 23 Nov 2022 23:36:17 -0000
+
golang-github-arceliar-phony (0.0~git20210209.dde1a8d-2) unstable; urgency=medium
* Don't build the example
diff --git a/go.mod b/go.mod
index afdc471..7047899 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
module github.com/Arceliar/phony
-go 1.12
+go 1.19
Debdiff
File lists identical (after any substitutions)
No differences were encountered in the control files