phony

package module
v0.0.0-...-530938a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2022 License: MPL-2.0 Imports: 3 Imported by: 24

README

Phony

Go Report Card GoDoc

Phony is a Pony-inspired proof-of-concept implementation of shared-memory actor-model concurrency in the Go programming language. Actors automatically manage goroutines and use asynchronous causal messaging (with backpressure) for communcation. This makes it easy to write programs that are free from deadlocks, goroutine leaks, and many of the for loops over select statements that show up in boilerplate code. The down side is that the code needs to be written in an asynchronous style, which is not idiomatic to Go, so it can take some getting used to.

Benchmarks

goos: linux
goarch: amd64
pkg: github.com/Arceliar/phony
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	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.

Implementation Details

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 (Gosched()).
    • sync for sync.Pool, to minimize allocations.
    • sync/atomic to implement the Inbox's message queues.
  • Attempts were make to make embedding and composition work:

    • Actor is an interface satisfied by the Inbox struct.
    • The zero value of an Inbox is a fully initialized and ready-to-use Actor
    • This means any struct that anonymously embeds an Inbox is an Actor
    • structs that don't want to export the Actor interface can embed it as a field instead.
  • Inbox was implemented with scalability in mind:

    • The Inbox is basically an atomically updated single-consumer multiple-producer linked list.
    • Pushing a message is wait-free -- no locks, spinlocks, or CompareAndSwap loops.
    • Popping messages is wait-free in the normal case, with a busy loop (LoadPointer) if popping the last message lost a race with a push.
    • When backpressure is required, it's implemented by sending two extra messages (one to the receiver of the original message, and one to the sender).
  • 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.
    • This means that idle Actors can be collected as garbage when they're no longer reachable, just like any other struct.

Documentation

Overview

Package phony is a small actor model library for Go, inspired by the causal messaging system in the Pony programming language. An Actor is an interface satisfied by a lightweight Inbox struct. Structs that embed an Inbox satisfy an interface that allows them to send messages to eachother. Messages are functions of 0 arguments, typically closures, and should not perform blocking operations. Message passing is asynchronous, causal, and fast. Actors implemented by the provided Inbox struct are scheduled to prevent messages queues from growing too large, by pausing at safe breakpoints when an Actor detects that it sent something to another Actor whose inbox is flooded.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Block

func Block(actor Actor, action func())

Block adds a message to an Actor's Inbox, which will be executed at some point in the future. It then blocks until the Actor has finished running the provided function. Block meant exclusively as a convenience function for non-Actor code to send messages and wait for responses. If an Actor calls Block, then it may cause a deadlock, so Act should always be used instead.

Types

type Actor

type Actor interface {
	Act(Actor, func())
	// contains filtered or unexported methods
}

Actor is the interface for Actors, based on their ability to receive a message from another Actor. It's meant so that structs which embed an Inbox can satisfy a mutually compatible interface for message passing.

type Inbox

type Inbox struct {
	// contains filtered or unexported fields
}

Inbox is an ordered queue of messages which an Actor will process sequentially. Messages are meant to be in the form of non-blocking functions of 0 arguments, often closures. The intent is for the Inbox struct to be embedded in other structs, causing them to satisfy the Actor interface, and then the Actor is used to access any protected fields of the struct. It is up to the user to ensure that memory is used safely, and that messages do not contain blocking operations. An Inbox must not be copied after first use.

func (*Inbox) Act

func (a *Inbox) Act(from Actor, action func())

Act adds a message to an Inbox, which will be executed by the inbox's Actor at some point in the future. When one Actor sends a message to another, the sender is meant to provide itself as the first argument to this function. If the sender argument is non-nil and the receiving Inbox has been flooded, then backpressure is applied to the sender. This backpressue cause the sender stop processing messages at some point in the future until the receiver has caught up with the sent message. A nil first argument is valid, but should only be used in cases where backpressure is known to be unnecessary, such as when an Actor sends a message to itself or sends a response to a request (where it's the request sender's fault if they're flooded by responses).

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL