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

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

More details

Full run details