Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions .gitattributes

This file was deleted.

26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Test Project

on:
push:
branches:
workflow_dispatch:

jobs:
test-project:
runs-on: ubuntu-latest
steps:
- run: uname -a
- run: lsb_release -a

- name: Setup
uses: actions/setup-go@v4
with:
go-version: "1.23"

- run: go version

- name: Checkout
uses: actions/checkout@v4

- name: Test
run: make test
5 changes: 5 additions & 0 deletions FUTURE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## ✒ 未来版本的新特性 (Features in future versions)

### v0.1.0

* [x] 增加支持退避策略的自旋锁
* [ ] 增加轻量协程库

### v0.0.1

* [x] 基础功能完善
18 changes: 8 additions & 10 deletions README.en.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# 📝 Goes
# 🦉 Goes

[![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/goes)
[![License](_icons/license.svg)](https://opensource.org/licenses/MIT)
[![License](_icons/build.svg)](_icons/build.svg)
[![License](_icons/coverage.svg)](_icons/coverage.svg)
[![Coverage](_icons/coverage.svg)](./_icons/coverage.svg)
![Test](https://github.com/FishGoddess/goes/actions/workflows/test.yml/badge.svg)

**Goes** is a lightweight lib for limiting goroutines.
**Goes** is a easy-to-use and lightweight lib for limiting goroutines.

[阅读中文版的文档](./README.md)

### 🥇 Features

* Limit goroutines, that's it.
* Spin lock with backoff strategy.
* Limiter only limits the number of simultaneous goroutines, not reuses goroutines.

_Check [HISTORY.md](./HISTORY.md) and [FUTURE.md](./FUTURE.md) to know about more information._

Expand Down Expand Up @@ -43,11 +44,8 @@ func main() {
}
```

_Check more examples in [_examples](./_examples)._

### 👥 Contributing

If you find that something is not working as expected please open an _**issue**_.

At last, I want to thank JetBrains for **free JetBrains Open Source license(s)**, because goes is developed with Idea /
GoLand under it.

<a href="https://www.jetbrains.com/?from=goes" target="_blank"><img src="./_icons/jetbrains.png" width="250"/></a>
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# 📝 Goes
# 🦉 Goes

[![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/goes)
[![License](_icons/license.svg)](https://opensource.org/licenses/MIT)
[![License](_icons/build.svg)](_icons/build.svg)
[![License](_icons/coverage.svg)](_icons/coverage.svg)
[![Coverage](_icons/coverage.svg)](./_icons/coverage.svg)
![Test](https://github.com/FishGoddess/goes/actions/workflows/test.yml/badge.svg)

**Goes** 是一个比协程池轻量的协程数限制库
**Goes** 是一个简单易用且轻量的协程数限制库

[Read me in English](./README.en.md)

### 🥇 功能特性

* 限制协程数,没了。
* 支持退避策略的自旋锁。
* Limiter 只限制同时执行的协程数,不复用协程。

_历史版本的特性请查看 [HISTORY.md](./HISTORY.md)。未来版本的新特性和计划请查看 [FUTURE.md](./FUTURE.md)。_

Expand Down Expand Up @@ -43,10 +44,8 @@ func main() {
}
```

_更多使用案例请查看 [_examples](./_examples) 目录。_

### 👥 贡献者

如果您觉得 goes 缺少您需要的功能,请不要犹豫,马上参与进来,发起一个 _**issue**_。

最后,我想感谢 JetBrains 公司的 **free JetBrains Open Source license(s)**,因为 goes 是用该计划下的 Idea / GoLand 完成开发的。

<a href="https://www.jetbrains.com/?from=goes" target="_blank"><img src="./_icons/jetbrains.png" width="250"/></a>
33 changes: 33 additions & 0 deletions _examples/spin_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025 FishGoddess. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package main

import (
"fmt"
"sync"

"github.com/FishGoddess/goes/pkg/spinlock"
)

func main() {
// It's an implementation of sync.Locker, so you can use it as a mutex.
spin := spinlock.New()

var total int
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()

spin.Lock()
total++
spin.Unlock()
}()
}

wg.Wait()
fmt.Println("total is %d", total)
}
16 changes: 0 additions & 16 deletions _icons/build.svg

This file was deleted.

Binary file removed _icons/jetbrains.png
Binary file not shown.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/FishGoddess/goes

go 1.17
go 1.23
24 changes: 0 additions & 24 deletions limit_test.go

This file was deleted.

6 changes: 3 additions & 3 deletions limit.go → limiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ const (

type token struct{}

// Limiter limits the max number of goroutines.
// Limiter limits the simultaneous number of goroutines.
type Limiter struct {
tokens chan token
wg sync.WaitGroup
}

// NewLimiter creates a limiter with limit.
// NewLimiter creates a new limiter with limit.
func NewLimiter(limit int) *Limiter {
if limit < minLimit {
limit = minLimit
Expand All @@ -46,7 +46,7 @@ func (l *Limiter) releaseToken() {
}
}

// Go starts a goroutine with token.
// Go starts a goroutine to run f().
func (l *Limiter) Go(f func()) {
l.acquireToken()
l.wg.Add(1)
Expand Down
48 changes: 48 additions & 0 deletions limiter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2023 FishGoddess. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package goes

import (
"sync"
"testing"
"time"
)

// go test -v -cover -run=^TestLimiter$
func TestLimiter(t *testing.T) {
limit := 16
limiter := NewLimiter(limit)

var countMap = make(map[int64]int, 16)
var lock sync.Mutex

totalCount := 100
for i := 0; i < totalCount; i++ {
limiter.Go(func() {
now := time.Now().UnixMilli()

lock.Lock()
countMap[now] = countMap[now] + 1
lock.Unlock()

time.Sleep(100 * time.Millisecond)
})
}

limiter.Wait()

gotTotalCount := 0
for now, count := range countMap {
gotTotalCount = gotTotalCount + count

if count != limit {
t.Logf("now %d: count %d != limit %d", now, count, limit)
}
}

if gotTotalCount != totalCount {
t.Fatalf("gotTotalCount %d != totalCount %d", gotTotalCount, totalCount)
}
}
36 changes: 36 additions & 0 deletions pkg/spinlock/spin_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package spinlock

import (
"runtime"
"sync"
"sync/atomic"
)

const maxBackoff = 16

type lock uint32

// New creates a new spin lock.
func New() sync.Locker {
return new(lock)
}

// Lock locks with the spin lock.
func (l *lock) Lock() {
backoff := 1

for !atomic.CompareAndSwapUint32((*uint32)(l), 0, 1) {
for i := 0; i < backoff; i++ {
runtime.Gosched()
}

if backoff < maxBackoff {
backoff <<= 1
}
}
}

// Unlock unlocks with the spin lock.
func (l *lock) Unlock() {
atomic.StoreUint32((*uint32)(l), 0)
}
31 changes: 31 additions & 0 deletions pkg/spinlock/spin_lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package spinlock

import (
"sync"
"testing"
"time"
)

func benchmarkLock(b *testing.B, lock sync.Locker) {
b.SetParallelism(1024)

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
lock.Lock()
time.Sleep(50 * time.Microsecond)
lock.Unlock()
}
})
}

// go test -v -run=none -bench=^BenchmarkMutex$ -benchmem -benchtime=1s
func BenchmarkMutex(b *testing.B) {
var mu sync.Mutex
benchmarkLock(b, &mu)
}

// go test -v -run=none -bench=^BenchmarkSpinLock$ -benchmem -benchtime=1s
func BenchmarkSpinLock(b *testing.B) {
spin := New()
benchmarkLock(b, spin)
}
Loading