Distributed work without channels

In order to distribute the work equally among the cashiers, we need to know the amount of orders we will get beforehand and ensure that the work each cashier receives is within his/her limit. This is not the most practical solution, because it would fail in a real-world scenario where we would need to keep track of how many orders each cashier has processed and divert the remaining orders to the other cashiers. However, before we look at the correct way to solve it, let's take time to better understand the problem of uncontrolled parallelism and try to solve it. The following code attempts to solve it in a naïve manner, which should provide us with a good start:

// wochan.go 
 
package main 
 
import ( 
   "fmt" 
   "sync" 
) 
 
func createCashier(cashierID int, wg *sync.WaitGroup) func(int) { 
   ordersProcessed := 0 
   return func(orderNum int) { 
         if ordersProcessed < 10 { 
               // Cashier is ready to serve! 
               //fmt.Println("Cashier ", cashierID, "Processing order", orderNum, "Orders Processed", ordersProcessed) 
               fmt.Println(cashierID, "->", ordersProcessed) 
               ordersProcessed++ 
         } else { 
               // Cashier has reached the max capacity of processing orders. 
               fmt.Println("Cashier ", cashierID, "I am tired! I want to take rest!", orderNum) 
         } 
         wg.Done() 
   } 
} 
 
func main() { 
   cashierIndex := 0 
   var wg sync.WaitGroup 
 
   // cashier{1,2,3} 
   cashiers := []func(int){} 
   for i := 1; i <= 3; i++ { 
         cashiers = append(cashiers, createCashier(i, &wg)) 
   } 
 
   for i := 0; i < 30; i++ { 
         wg.Add(1) 
 
         cashierIndex = cashierIndex % 3 
 
         func(cashier func(int), i int) { 
               // Making an order 
               go cashier(i) 
         }(cashiers[cashierIndex], i) 
 
         cashierIndex++ 
   } 
   wg.Wait() 
} 

The following is one possible output:

Cashier  2 Processing order 7
Cashier  1 Processing order 6
Cashier  3 Processing order 8
Cashier  3 Processing order 29
Cashier  1 Processing order 9
Cashier  3 Processing order 2
Cashier  2 Processing order 10
Cashier  1 Processing order 3
...

We split the available 30 orders between cashiers 1, 2, and 3, and all of the orders were successfully processed without anyone complaining about being tired. However, note that the code to make this work required a lot of work on our end. We had to create a function generator to create cashiers, keep track of which cashier to use via cashierIndex, and so on. And the worst part is that the preceding code isn't correct! Logically, it might seem to be doing what we want; however, note that we are spawning multiple goroutines that are working on variables with a shared state, ordersProcessed! This is the race condition we discussed earlier. The good news is that we can detect it in wochan.go in two ways:

  • In createCashier function, replace fmt.Println("Cashier ", cashierID, "Processing order", orderNum) with fmt.Println(cashierID, "->", ordersProcessed). Here is one possible output:
      3 -> 0
      3 -> 1
      1 -> 0
      ...
      2 -> 3
      3 -> 1  # Cashier 3 sees ordersProcessed as 1 but three lines above, Cashier 3     
was at ordersProcessed == 4!
3 -> 5 1 -> 4 1 -> 4 # Cashier 1 sees ordersProcessed == 4 twice. 2 -> 4 2 -> 4 # Cashier 2 sees ordersProcessed == 4 twice. ...
  • The previous point proves that the code is not correct; however, we had to guess the possible issue in the code and then verify it. Go provides us with tools to detect data race so that we do not have to worry about such issues. All we have to do to detect data race is to test, run, build, or install the package (file in the case of run) with the -race flag . Let's run this on our program and look at the output:
      $ go run -race wochan.go 
      Cashier  1 Processing order 0
      Cashier  2 Processing order 1
      ==================
      WARNING: DATA RACE
      Cashier  3 Processing order 2
      Read at 0x00c4200721a0 by goroutine 10:
      main.createCashier.func1()
          wochan.go:11 +0x73
    
      Previous write at 0x00c4200721a0 by goroutine 7:
      main.createCashier.func1()
          wochan.go:14 +0x2a7
    
      Goroutine 10 (running) created at:
      main.main.func1()
          wochan.go:40 +0x4a
      main.main()
          wochan.go:41 +0x26e
    
      Goroutine 7 (finished) created at:
      main.main.func1()
          wochan.go:40 +0x4a
      main.main()
          wochan.go:41 +0x26e
      ==================
      Cashier  2 Processing order 4
      Cashier  3 Processing order 5
      ==================
      WARNING: DATA RACE
      Read at 0x00c420072168 by goroutine 9:
      main.createCashier.func1()
          wochan.go:11 +0x73
    
      Previous write at 0x00c420072168 by goroutine 6:
      main.createCashier.func1()
          wochan.go:14 +0x2a7
    
      Goroutine 9 (running) created at:
      main.main.func1()
          wochan.go:40 +0x4a
      main.main()
          wochan.go:41 +0x26e
    
      Goroutine 6 (finished) created at:
      main.main.func1()
          wochan.go:40 +0x4a
      main.main()
          wochan.go:41 +0x26e
      ==================
      Cashier  1 Processing order 3
      Cashier  1 Processing order 6
      Cashier  2 Processing order 7
      Cashier  3 Processing order 8
      ...
      Found 2 data race(s)
      exit status 66

As can be seen, the -race flag helped us to detect the data race.

Does this mean that we cannot distribute our tasks when we have shared state? Of course we can! But we need to use mechanisms provided by Go for this purpose:

  • Mutexes, semaphores, and locks
  • Channels

Mutex is a mutual exclusion lock that provides us with a synchronization mechanism to allow only one goroutine to access a particular piece of code or shared state at any given point in time. As already stated, for synchronization problems, we can use either mutex or channels, and Go recommends using the right construct for the right job. However, in practice, using channels provides us with a higher level of abstraction and greater versatility in terms of usage, though mutex has its uses. It is for this reason for that, throughout this chapter and the book, we will be making use of channels.

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

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