Channel is an important and unique concurrent programming primitive in Go. With its thread safety and blocking characteristics, it can realize scenarios such as information transmission, signal notification, mutex lock, task scheduling and so on.
I signal transmission
There are 4 goroutines numbered 1, 2, 3 and 4. Every second, a goroutine will print out its own number and ask you to write a program so that the output number is always printed in the order of 1, 2, 3, 4, 1, 2, 3, 4.
In order to realize sequential data transmission, we can define a token variable. Whoever gets the token can print his own number once, and pass the token to the next goroutine at the same time.
type Token struct{} func newWorker(id int, ch chan Token, nextCh chan Token) { for { token := <-ch // Get token fmt.Println((id + 1)) // id starts with 1 time.Sleep(time.Second) nextCh <- token } } func main() { chs := []chan Token{make(chan Token), make(chan Token), make(chan Token), make(chan Token)} // Create 4 worker s for i := 0; i < 4; i++ { go newWorker(i, chs[i], chs[(i+1)%4]) } //First, give the token to the first worker chs[0] <- struct{}{} select {} }
First define a Token type, and then define a method to create a worker, which will read the Token from its own chan. When a goroutine obtains a Token, it can print its own number. Because it needs to print data every second, we let it sleep for 1 second and then give the Token to its next home. A feature of this kind of scenario is that goroutine currently holding data has a mailbox, which is implemented by chan. Goroutine only needs to pay attention to the data in its mailbox and send the results to the mailbox of the next company after processing.
II Signal notification
chan type has such a feature: if chan is empty, the receiver will block and wait when receiving data until chan is closed or new data arrives. Using this mechanism, we can implement the design pattern of wait/notify. The traditional concurrency primitive Cond can also achieve this function. However, Cond is complex and error prone to use, and it is much more convenient to use chan to implement the wait/notify mode.
For example, use chan to implement the graceful shutdown of the program, and perform some actions such as connection closing, file closing, cache disk dropping, etc. before exiting.
func main() { go func() { ...... // Execute business processing }() // Process interrupt signals such as CTRL+C termChan := make(chan os.Signal) signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM) <-termChan // Perform the cleanup action before exiting doCleanup() fmt.Println("Graceful exit") }
Sometimes, doCleanup may be a very time-consuming operation. For example, it takes more than ten minutes to complete. If the program needs to wait for such a long time to exit, the user cannot accept it. Therefore, in practice, we need to set a maximum waiting time. As long as this time is exceeded, the program will no longer wait and can exit directly. Therefore, the exit is divided into two stages: closing, which represents the exit of the program, but the cleaning work has not been done; closed means that the cleaning work has been completed.
func main() { var closing = make(chan struct{}) var closed = make(chan struct{}) go func() { // Simulated service processing for { select { case <-closing: return default: // ....... Business calculation time.Sleep(100 * time.Millisecond) } } }() // Process interrupt signals such as CTRL+C termChan := make(chan os.Signal) signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM) <-termChan close(closing) // Perform the cleanup action before exiting go doCleanup(closed) select { case <-closed: case <-time.After(time.Second): fmt.Println("Cleaning timeout, wait") } fmt.Println("Graceful exit") } func doCleanup(closed chan struct{}) { time.Sleep((time.Minute)) close(closed) }
III mutex
Mutexes can also be implemented using chan. In the internal implementation of chan, there is a mutex to protect all its fields. Externally, there is also a happens before relationship between the sending and receiving of chan. It is ensured that the receiver can read the elements only after they are put in. First initialize a Channel with capacity equal to 1, and then put in an element. This element represents the lock. Whoever obtains this element is equivalent to obtaining the lock.
// Using chan to implement mutex type Mutex struct { ch chan struct{} } // Using locks requires initialization func NewMutex() *Mutex { mu := &Mutex{make(chan struct{}, 1)} mu.ch <- struct{}{} return mu } // Request a lock until it is acquired func (m *Mutex) Lock() { <-m.ch } // Unlock func (m *Mutex) Unlock() { select { case m.ch <- struct{}{}: default: panic("unlock of unlocked mutex") } } // Attempt to acquire lock func (m *Mutex) TryLock() bool { select { case <-m.ch: return true default: } return false } // Add a timeout setting func (m *Mutex) LockTimeout(timeout time.Duration) bool { timer := time.NewTimer(timeout) select { case <-m.ch: timer.Stop() return true case <-timer.C: } return false } // Is the lock held func (m *Mutex) IsLocked() bool { return len(m.ch) == 0 } func main() { m := NewMutex() ok := m.TryLock() fmt.Printf("locked v %v\n", ok) ok = m.TryLock() fmt.Printf("locked %v\n", ok) }
Use the Channel with buffer equal to 1 to implement the mutex lock. When initializing the lock, first insert an element into the Channel. Whoever takes the element away will obtain the lock. Putting the element back will release the lock. Before the element is put back into chan, no goroutine can take the element out of chan, which ensures mutual exclusion.
Using the select+chan method, it is easy to realize the functions of TryLock and Timeout. Specifically, in the select statement, we can use default to implement TryLock and a Timer to implement the Timeout function.
//Asynchronous processing of subsequent tasks to be supplemented