The different behavior for receiving channel when channel is closing? (Invalid question)

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.

Oops, i check my question again, because a long time since I wrote golang code, i totally misunderstood the go code means.

Following is the correct translate code, it works, no issue.

class A
  @start = Channel(Nil).new

  def sender
    @start.receive?
    puts "sender"
  end

  def receiver
    @start.receive?
    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
2 Likes