I am reading concurrent in go
, and i saw a example code here, following is the copy.
// main_test.go
package main
import (
"sync"
"testing"
)
func BenchmarkContextSwitch(b *testing.B) {
var wg sync.WaitGroup
begin := make(chan struct{})
c := make(chan struct{})
var token struct{}
sender := func() {
defer wg.Done()
<-begin // <1>
for i := 0; i < b.N; i++ {
c <- token // <2>
}
}
receiver := func() {
defer wg.Done()
<-begin // <1>
for i := 0; i < b.N; i++ {
<-c // <3>
}
}
wg.Add(2)
go sender()
go receiver()
b.StartTimer() // <4>
close(begin) // <5>
wg.Wait()
}
Above code works on my laptop go 1.18.
╰─ $ go test -bench=. -cpu=1 ./main_test.go
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkContextSwitch 7570882 155.7 ns/op
PASS
ok command-line-arguments 1.344s
What i really care about is, the behavior of begin
channel for the above example.
func BenchmarkContextSwitch(b *testing.B) {
begin := make(chan struct{})
sender := func() {
<-begin // <1>
}
receiver := func() {
<-begin // <1>
}
go sender()
go receiver()
b.StartTimer()
close(begin) // <2>
}
The usage of begin is:
chan begin
is receving in sender<1> and receiver<1>, both of them get blocked, when begin is closing in <2>, the sender and receiver will resume and continue execute the measurement code in golang.
But this code not work for Crystal.
class A
@start = Channel(Nil).new
def sender
@start.send nil
puts "sender"
end
def receiver
@start.send nil
puts "receiver"
end
def meth
spawn sender
spawn receiver
puts "before start"
@start.close
puts "after start"
end
end
a = A.new
a.meth
Fiber.yield
before start
after start
Unhandled exception in spawn: Channel is closed (Channel::ClosedError)
from /home/zw963/Crystal/share/crystal/src/channel.cr:228:8 in 'send'
from 1.cr:6:5 in 'sender'
from 1.cr:15:5 in '->'
from /home/zw963/Crystal/share/crystal/src/fiber.cr:146:11 in 'run'
from /home/zw963/Crystal/share/crystal/src/fiber.cr:98:34 in '->'
from ???
Unhandled exception in spawn: Channel is closed (Channel::ClosedError)
from /home/zw963/Crystal/share/crystal/src/channel.cr:228:8 in 'send'
from 1.cr:11:5 in 'receiver'
from 1.cr:16:5 in '->'
from /home/zw963/Crystal/share/crystal/src/fiber.cr:146:11 in 'run'
from /home/zw963/Crystal/share/crystal/src/fiber.cr:98:34 in '->'
from ???
We have to rescue the Channel::ClosedError to get similar behavior.
class A
@start = Channel(Nil).new
def sender
@start.send nil
rescue Channel::ClosedError
puts "sender"
end
def receiver
@start.send nil
rescue Channel::ClosedError
puts "receiver"
end
def meth
spawn sender
spawn receiver
puts "before start"
@start.close
puts "after start"
end
end
a = A.new
a.meth
Fiber.yield
Now, we get expected result.
╰─ $ cr 1.cr
before start
after start
sender
receiver
Why this behavior not same as golang by default? and not given a non-raised version, e.g. Channel#close?
The concerns is, close a channel from the only send side, should not raised, right?
Following is explain in the book
I also noticed, calling #close
on a closed channel does not have any effect in Crystal, but it will panic in golang.
I guess all this design is intentional, may I know the reason why it is designed like this?
Thanks.