Using it all - concurrent singleton

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.

Unit test

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.

Implementation

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:

  • The 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).
  • The 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.
  • The 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:

  • The 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.
  • The 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.
  • The 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.45.137