Now that we know how to create Goroutines and channels, we'll put all our knowledge in a single package. Think back to the first few chapter, when we explained the singleton pattern-it was some structure or variable that could only exist once in our code. All access to this structure should be done using the pattern described, but, in fact, it wasn't concurrent safe.
Now we will write with concurrency in mind. We will write a concurrent counter, like the one we wrote in the mutexes section, but this time we will solve it with channels.
To restrict concurrent access to the singleton
instance, just one Goroutine will be able to access it. We'll access it using channels--the first one to add one, the second one to get the current count, and the third one to stop the Goroutine.
We will add one 10,000 times using 10,000 different Goroutines launched from two different singleton
instances. Then, we'll introduce a loop to check the count of the singleton
until it is 5,000, but we'll write how much the count is before starting the loop.
Once the count has reached 5,000, the loop will exit and quit the running Goroutine-the test code looks like this:
package channel_singleton import ( "testing" "time" "fmt" ) func TestStartInstance(t *testing.T) { singleton := GetInstance() singleton2 := GetInstance() n := 5000 for i := 0; i < n; i++ { go singleton.AddOne() go singleton2.AddOne() } fmt.Printf("Before loop, current count is %d ", singleton.GetCount()) var val int for val != n*2 { val = singleton.GetCount() time.Sleep(10 * time.Millisecond) } singleton.Stop() }
Here, we can see the full test we'll use. After creating two instances of the singleton
, we have created a for
loop that launches the AddOne
method 5,000 times from each instance. This is not happening yet; they are being scheduled and will be executed eventually. We are printing the count of the singleton
instance to clearly see this eventuality; depending on the computer, it will print some number greater than 0 and lower than 10,000.
The last step before stopping the Goroutine that is holding the count is to enter a loop that checks the value of the count and waits 10 milliseconds if the value is not the expected value (10,000). Once it reaches this value, the loop will exit and we can stop the singleton
instance.
We'll jump directly to the implementation as the requirement is quite simple.
First of all, we'll create the Goroutine that will hold the count:
var addCh chan bool = make(chan bool) var getCountCh chan chan int = make(chan chan int) var quitCh chan bool = make(chan bool) func init() { var count int go func(addCh <-chan bool, getCountCh <-chan chan int, quitCh <-chan bool) { for { select { case <-addCh: count++ case ch := <-getCountCh: ch <- count case <-quitCh: return } } }(addCh, getCountCh, quitCh) }
We created three channels, as we mentioned earlier:
addCh
channel is used to communicate with the action of adding one to the count, and receives a bool
type just to signal "add one" (we don't need to send the number, although we could).getCountCh
channel will return a channel that will receive the current value of the count. Take a moment to reason about the getCountCh
channel-it's a channel that receives a channel that receives integer types. It sounds a bit complicated, but it will make more sense when we finish the example, don't worry.quitCh
channel will communicate to the Goroutine that it should end its infinite loop and finish itself too.Now we have the channels that we need to perform the actions we want. Next, we launch the Goroutine passing the channels as arguments. As you can see, we are restricting the direction of the channels to provide more type safety. Inside this Goroutine, we create an infinite for
loop. This loop won't stop until a break is executed within it.
Finally, the select
statement, if you remember, was a way to receive data from different channels at the same time. We have three cases, so we listen to the three incoming channels that entered as arguments:
addCh
case will add one to the count. Remember that only one case can be executed on each iteration so that no Goroutine could be accessing the current count until we finish adding one.getCountCh
channel receives a channel that receives an integer, so we capture this new channel and send the current value through it to the other end.quitCh
channel breaks the for
loop, so the Goroutine ends.One last thing. The init()
function in any package will get executed on program execution, so we don't need to worry about executing this function specifically from our code.
Now, we'll create the type that the tests are expecting. We will see that all the magic and logic is hidden from the end user in this type (as we have seen in the code of the test):
type singleton struct {} var instance singleton func GetInstance() *singleton { return &instance }
The singleton
type works similar to the way it worked in
Chapter 2
, Creational Patterns - Singleton, Builder, Factory, Prototype, and Abstract Factory, but this time it won't hold the count value. We created a local value for it called instance
, and we return the pointer to this instance when we call the GetInstance()
method. It is not strictly necessary to do it this way, but we don't need to allocate a new instance of the singleton
type every time we want to access the count variable.
First, the AddOne()
method will have to add one to the current count. How? By sending true
to the addCh
channel. That's simple:
func (s *singleton) AddOne() { addCh <- true }
This small snippet will trigger the addCh
case in our Goroutine in turn. The addCh
case simply executes count++
and finishes, letting select
channel control flow that is executed on init
function above to execute the next instruction:
func (s *singleton) GetCount() int { resCh := make(chan int) defer close(resCh) getCountCh <- resCh return <-resCh }
The GetCount
method creates a channel every time it's called and defers the action of closing it at the end of the function. This channel is unbuffered as we have seen previously in this chapter. An unbuffered channel blocks the execution until it receives some data. So we send this channel to getCountCh
which is a channel too and, effectively, expects a chan int
type to send the current count value back through it. The GetCount()
method will not return until the value of count
variable arrives to the resCh
channel.
You might be thinking, why aren't we using the same channel in both directions to receive the value of the count? This way we will avoid an allocation. Well, if we use the same channel inside the GetCount()
method, we will have two listeners in this channel--one in select
statement, at the beginning of the file on the init
function, and one there, so it could resolve to any of them when sending the value back:
func (s *singleton) Stop() { quitCh <- true close(addCh) close(getCountCh) close(quitCh) }
Finally, we have to stop the Goroutine at some moment. The Stop
method sends the value to the singleton
type Goroutine so that the quitCh
case is triggered and the for
loop is broken. The next step is to close all channels so that no more data can be sent through them. This is very convenient when you know that you won't be using some of your channels anymore.
Time to execute the tests and take a look:
$ go test -v . === RUN TestStartInstance Before loop, current count is 4911 --- PASS: TestStartInstance (0.03s) PASS ok
Very little code output, but everything has worked as expected. In the test, we printed the value of the count before entering the loop that iterates until it reaches the value 10,000. As we saw previously, the Go scheduler will try to run the content of the Goroutines using as many OS threads as you configured by using the GOMAXPROCS
configuration. In my computer, it is set to 4
because my computer has four cores. But the point is that we can see that a lot of things can happen after launching a Goroutine (or 10,000) and the next execution line.
But what about its use of mutexes?
type singleton struct { count int sync.RWMutex } var instance singleton func GetInstance() *singleton { return &instance } func (s *singleton) AddOne() { s.Lock() defer s.Unlock() s.count++ } func (s *singleton) GetCount()int { s.RLock() defer s.RUnlock() return s.count }
In this case, the code is much leaner. As we saw previously, we can embed the mutex within the singleton
structure. The count is also held in the count
field and the AddOne()
and GetCount()
methods lock and unlock the value to be concurrently safe.
One more thing. In this singleton
instance, we are using the RWMutex
type instead of the already known sync.Mutex
type. The main difference here is that the RWMutex
type has two types of locks--a read lock and a write lock. The read lock, executed by calling the RLock
method, only waits if a write lock is currently active. At the same time, it only blocks a write lock, so that many read actions can be done in parallel. It makes a lot of sense; we don't want to block a Goroutine that wants to read a value just because another Goroutine is also reading the value-it won't change. The sync.RWMutex
type helps us to achieve this logic in our code.
3.144.15.154