Skip to content

Commit

Permalink
feat: add async.Once implementation (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
reugn authored Aug 11, 2023
1 parent 6b0c2f3 commit 22d2d3b
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Async is a synchronization and asynchronous computation package for Go.
* **Future** - A placeholder object for a value that may not yet exist.
* **Promise** - While futures are defined as a type of read-only placeholder object created for a result which doesn’t yet exist, a promise can be thought of as a writable, single-assignment container, which completes a future.
* **Task** - A data type for controlling possibly lazy and asynchronous computations.
* **Once** - An object similar to sync.Once having the Do method taking `f func() (T, error)` and returning `(T, error)`.
* **WaitGroupContext** - A WaitGroup with the `context.Context` support for graceful unblocking.
* **Reentrant Lock** - Mutex that allows goroutines to enter into the lock on a resource more than once.
* **Optimistic Lock** - Mutex that allows optimistic reading. Could be retried or switched to RLock in case of failure. Significantly improves performance in case of frequent reads and short writes. See [benchmarks](./benchmarks/README.md).
Expand Down
40 changes: 40 additions & 0 deletions once.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package async

import (
"fmt"
"sync"
)

// Once is an object that will execute the given function exactly once.
// Any subsequent call will return the previous result.
type Once[T any] struct {
runOnce sync.Once
result T
err error
}

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
//
// var once Once
//
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// The return values for each subsequent call will be the result of the
// first execution.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f
func (o *Once[T]) Do(f func() (T, error)) (T, error) {
o.runOnce.Do(func() {
defer func() {
if err := recover(); err != nil {
o.err = fmt.Errorf("recovered %v", err)
}
}()
o.result, o.err = f()
})
return o.result, o.err
}
57 changes: 57 additions & 0 deletions once_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package async

import (
"sync"
"sync/atomic"
"testing"

"github.com/reugn/async/internal"
)

func TestOnce(t *testing.T) {
var once Once[int32]
var count int32

for i := 0; i < 10; i++ {
count, _ = once.Do(func() (int32, error) {
count++
return count, nil
})
}
internal.AssertEqual(t, count, 1)
}

func TestOnceConcurrent(t *testing.T) {
var once Once[int32]
var count int32
var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result, _ := once.Do(func() (int32, error) {
newCount := atomic.AddInt32(&count, 1)
return newCount, nil
})
atomic.StoreInt32(&count, result)
}()
}
wg.Wait()
internal.AssertEqual(t, count, 1)
}

func TestOncePanic(t *testing.T) {
var once Once[int32]
var count int32
var err error

for i := 0; i < 10; i++ {
count, err = once.Do(func() (int32, error) {
count /= count
return count, nil
})
}
internal.AssertEqual(t, err.Error(), "recovered runtime error: integer divide by zero")
internal.AssertEqual(t, count, 0)
}

0 comments on commit 22d2d3b

Please sign in to comment.