Skip to content

Commit 469bc0e

Browse files
sudhirjSudhir Jonathan
andauthored
Switched to a generic function (#4)
* Made lib generic * Switched to 1.18 build * Removed deps step * Switched off modules * Added mod init * Adding GOPATH * Try path again * Used newer action * Set unstable * Fixed version number * Try numbering again * More version format nonsense * Added paths * Added go.mod * Refactoring * Generified README * Clarified and commented * Shifted to typed sync maps Co-authored-by: Sudhir Jonathan <[email protected]>
1 parent 7ca2c9e commit 469bc0e

File tree

6 files changed

+105
-71
lines changed

6 files changed

+105
-71
lines changed

.github/workflows/go.yml

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,18 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88

9-
- name: Set up Go 1.13
10-
uses: actions/setup-go@v1
9+
- name: Set up Go 1.18
10+
uses: actions/setup-go@v2
1111
with:
12-
go-version: 1.13
12+
go-version: '1.18.0-beta2'
13+
stable: 'false'
1314
id: go
1415

1516
- name: Check out code into the Go module directory
1617
uses: actions/checkout@v1
1718

18-
- name: Get dependencies
19-
run: |
20-
go get -v -t -d ./...
21-
if [ -f Gopkg.toml ]; then
22-
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
23-
dep ensure
24-
fi
25-
2619
- name: Build
27-
run: go build -v .
28-
20+
run: go build
2921

3022
- name: Run Tests
31-
run: go test . --race
23+
run: go test --race

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ A circular queue that processes jobs in parallel but returns results in FIFO.
44
```go
55
inputs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
66

7-
inputChannel, outputChannel := NewCirque(3, func(i interface{}) interface{} {
7+
inputChannel, outputChannel := NewCirque(3, func(i int) int {
88
time.Sleep(time.Duration(rand.Int63n(100)) * time.Millisecond)
9-
return i.(int) * 2
9+
return i * 2
1010
})
1111

1212
go func() {
@@ -18,7 +18,7 @@ go func() {
1818

1919
var output []int
2020
for i := range outputChannel {
21-
output = append(output, i.(int))
21+
output = append(output, i)
2222
}
2323
fmt.Println(output)
2424

cirque.go

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,77 +4,82 @@ import (
44
"sync"
55
)
66

7-
type indexedValue struct {
8-
value interface{}
9-
index int64
10-
}
11-
12-
// NewCirque creates a FIFO parallel queue that runs a given processor function on each job, similar to a parallel Map.
7+
// NewCirque creates a FIFO parallel queue that runs a given
8+
// processor function on each job, similar to a parallel Map.
139
//
14-
// The method accepts a parallelism number, which the maximum number of jobs that are processed simultaneously,
15-
// and a processor function that takes a job as input and returns a indexedValue as output. The processor function must be safe
10+
// The method accepts a parallelism number, which the maximum
11+
// number of jobs that are processed simultaneously,
12+
// and a processor function that takes an input and returns
13+
// an output. The processor function must be safe
1614
// to call from multiple goroutines.
1715
//
18-
// It returns two channels, one into which inputs can be passed, and one from which outputs can be read.
19-
// Closing the input channel will close the output channel after processing is complete. Do not close the output channel yourself.
20-
func NewCirque(parallelism int64, processor func(interface{}) interface{}) (chan<- interface{}, <-chan interface{}) {
21-
input := make(chan interface{})
22-
output := make(chan interface{})
16+
// It returns two channels, one into which inputs can be passed,
17+
// and one from which outputs can be read. Closing the input channel
18+
// will close the output channel after processing is complete. Do not
19+
// close the output channel yourself.
20+
func NewCirque[I any, O any](parallelism int64, processor func(I) O) (chan<- I, <-chan O) {
21+
inputChannel := make(chan I)
22+
outputChannel := make(chan O)
23+
24+
inputHolder := NewSyncMap[int64, I]()
25+
outputHolder := NewSyncMap[int64, O]()
26+
27+
// let the output goroutine know every time an input is processed, so it
28+
// can wake up and try to send outputs
29+
processCompletionSignal := make(chan struct{})
30+
31+
// apply backpressure to make sure we're processing inputs only when outputs are
32+
// actually being collected - otherwise we're going to fill up memory with processed
33+
// jobs that aren't being taken out.
34+
outputBackpressureSignal := make(chan struct{}, parallelism)
2335

24-
processedJobs := make(chan indexedValue)
25-
semaphore := make(chan struct{}, parallelism)
2636
go func() { // process inputs
27-
poolWaiter := sync.WaitGroup{}
28-
pool := make(chan indexedValue)
37+
inflightInputs := sync.WaitGroup{}
38+
inputPool := make(chan int64)
2939

3040
// Start worker pool of specified size
31-
for workerID := int64(0); workerID < parallelism; workerID++ {
32-
poolWaiter.Add(1)
41+
for n := int64(0); n < parallelism; n++ {
42+
inflightInputs.Add(1)
3343
go func() {
34-
for job := range pool {
35-
processedJobs <- indexedValue{
36-
value: processor(job.value),
37-
index: job.index,
38-
}
44+
for index := range inputPool {
45+
input, _ := inputHolder.Get(index)
46+
outputHolder.Set(index, processor(input))
47+
inputHolder.Delete(index)
48+
processCompletionSignal <- struct{}{}
3949
}
40-
poolWaiter.Done()
50+
inflightInputs.Done()
4151
}()
4252
}
4353

4454
index := int64(0)
45-
for job := range input {
46-
pool <- indexedValue{
47-
value: job,
48-
index: index,
49-
}
50-
index = index + 1
51-
semaphore <- struct{}{}
55+
for input := range inputChannel {
56+
inputHolder.Set(index, input)
57+
inputPool <- index
58+
index++
59+
outputBackpressureSignal <- struct{}{}
5260
}
53-
close(pool)
61+
close(inputPool)
5462

55-
poolWaiter.Wait()
56-
close(processedJobs)
63+
inflightInputs.Wait()
64+
close(processCompletionSignal)
5765
}()
5866

5967
go func() { // send outputs in order
6068
nextIndex := int64(0)
61-
storedResults := map[int64]indexedValue{}
62-
for res := range processedJobs {
63-
storedResults[res.index] = res
64-
canSend := true
65-
for canSend {
66-
if storedResult, ok := storedResults[nextIndex]; ok {
67-
output <- storedResult.value
68-
delete(storedResults, storedResult.index)
69-
nextIndex = nextIndex + 1
70-
<-semaphore
69+
for range processCompletionSignal {
70+
for true {
71+
if output, ok := outputHolder.Get(nextIndex); ok {
72+
outputChannel <- output
73+
outputHolder.Delete(nextIndex)
74+
nextIndex++
75+
<-outputBackpressureSignal
7176
} else {
72-
canSend = false
77+
break
7378
}
7479
}
7580
}
76-
close(output)
81+
close(outputChannel)
7782
}()
7883

79-
return input, output
84+
return inputChannel, outputChannel
8085
}

cirque_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ func TestCirque(t *testing.T) {
4040
var wipCount int64 = 0
4141

4242
var maxParallelism int64 = 3
43-
inputChannel, outputChannel := NewCirque(maxParallelism, func(i interface{}) interface{} {
43+
inputChannel, outputChannel := NewCirque(maxParallelism, func(i int) int {
4444
atomic.AddInt64(&measuredParallelism, 1)
4545
time.Sleep(time.Duration(rand.Int63n(10)) * time.Millisecond)
4646
atomic.AddInt64(&measuredParallelism, -1)
47-
return i.(int) * 2
47+
return i * 2
4848
})
4949

5050
go func() {
@@ -67,7 +67,7 @@ func TestCirque(t *testing.T) {
6767
var actualOutput []int
6868
for i := range outputChannel {
6969
atomic.AddInt64(&wipCount, -1)
70-
actualOutput = append(actualOutput, i.(int))
70+
actualOutput = append(actualOutput, i)
7171
}
7272
if len(cs.expectedOutput) > 0 && !reflect.DeepEqual(cs.expectedOutput, actualOutput) {
7373
t.Errorf("WRONG WRONG WRONG. Case: %s \n Expected: %v, Actual: %v,",
@@ -81,9 +81,9 @@ func TestCirque(t *testing.T) {
8181
func ExampleNewCirque() {
8282
inputs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
8383

84-
inputChannel, outputChannel := NewCirque(3, func(i interface{}) interface{} {
84+
inputChannel, outputChannel := NewCirque(3, func(i int) int {
8585
time.Sleep(time.Duration(rand.Int63n(100)) * time.Millisecond)
86-
return i.(int) * 2
86+
return i * 2
8787
})
8888

8989
go func() {
@@ -95,7 +95,7 @@ func ExampleNewCirque() {
9595

9696
var output []int
9797
for i := range outputChannel {
98-
output = append(output, i.(int))
98+
output = append(output, i)
9999
}
100100
fmt.Println(output)
101101

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/sudhirj/cirque
2+
3+
go 1.18

map.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cirque
2+
3+
import "sync"
4+
5+
type SyncMap[K comparable, V any] struct {
6+
holder map[K]V
7+
lock sync.RWMutex
8+
}
9+
10+
func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
11+
return &SyncMap[K, V]{
12+
holder: make(map[K]V),
13+
lock: sync.RWMutex{},
14+
}
15+
}
16+
17+
func (tm *SyncMap[K, V]) Set(key K, value V) {
18+
tm.lock.Lock()
19+
tm.holder[key] = value
20+
tm.lock.Unlock()
21+
}
22+
23+
func (tm *SyncMap[K, V]) Get(key K) (value V, ok bool) {
24+
tm.lock.RLock()
25+
defer tm.lock.RUnlock()
26+
value, ok = tm.holder[key]
27+
return
28+
}
29+
30+
func (tm *SyncMap[K, V]) Delete(key K) {
31+
tm.lock.Lock()
32+
delete(tm.holder, key)
33+
tm.lock.Unlock()
34+
}

0 commit comments

Comments
 (0)