多路复用

selectselectchannelcase

select 语法如下:

   select {
    case <-chan1:
        // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
        // 如果成功向chan2写入数据,则进行该case处理语句
    default:
        // 如果上面都没有成功,则进入default处理流程
    }

在一个select语句中,会按顺序从头至尾评估每一个发送和接收的语句;如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:⑴ 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。⑵ 如果没有default语句,那么select语句将被阻塞,直到至少有一个channel可以进行下去。

defaultdefaultdefault

阻塞与非阻塞使用场景

  • 阻塞: 如:在监听超时退出时,如果100秒内无操作,择退出,此时添加了default会形成忙轮训,超时监听变成了无效。
  • 非阻塞: 如,在一个只有一个业务逻辑处理时,主进程控制进程的退出。此时可以使用default。

定时器

Go语言中定时器的使用有三个方法

time.Sleep()time.NewTimer()time.After(5 * time.Second)time.C

示例

select {
    case <-time.After(time.Second * 10):
}

锁和条件变量

syncsync/atomic

互斥锁

互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。

package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

var mutex sync.Mutex

func print(str string) {
	mutex.Lock()         // 添加互斥锁
	defer mutex.Unlock() // 使用结束时解锁

	for _, data := range str { // 迭代器
		fmt.Printf("%c", data)
		time.Sleep(time.Second) // 放大协程竞争效果
	}
	fmt.Println()
}

func main() {
	go print("hello") // main 中传参
	go print("world")
	for {
		runtime.GC()
	}
}

读写锁

读写锁的使用场景一般为读多写少,可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine不能进行读写操作;当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var count int           // 全局变量count
var rwlock sync.RWMutex // 全局读写锁 rwlock

func read(n int) {
	for {
		rwlock.RLock()
		fmt.Printf("reading goroutine %d ...\n", n)
		num := count
		fmt.Printf("read goroutine %d finished,get number %d\n", n, num)
		rwlock.RUnlock()
	}

}
func write(n int) {
	for {
		rwlock.Lock()
		fmt.Printf("writing goroutine %d ...\n", n)
		num := rand.Intn(1000)
		count = num
		fmt.Printf("write goroutine %d finished,write number %d\n", n, num)
		rwlock.Unlock()
	}
}

func main() {
	for i := 0; i < 5; i++ {
		go read(i + 1)
		time.Sleep(time.Microsecond * 100)
	}
	for i := 0; i < 5; i++ {
		go write(i + 1)
		time.Sleep(time.Microsecond * 100)
	}
	for {

	}
}

可以看出,读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间不存在互斥关系。

Go语言中的死锁

deadlock

单gorutine同时读写,写死锁

在一个gorutine中,当channel无缓冲,写阻塞,等待读取导致死锁

解决,应该至少在2个gorutine进行channle通讯,或者使用缓冲区。

package main

func main() {
	channel := make(chan int)
	channel <- 1
	<-channel
}

多gorutine使用一个channel通信,写先于读

代码顺序执行时,写操作阻塞,导致后面协程无法启动进行读操作,导致死锁

package main

func main() {
	channel := make(chan int)
	channel <- 1
	go func() {
		<-channel
	}()
}

多channel交叉死锁

在goroutine中,多个goroutine使用多个channel互相等待对方写入,导致死锁

package main

func main() {

	channel1 := make(chan int)
	channel2 := make(chan int)

	go func() {

		select {
		case <-channel1:
			channel2 <- 1
		}
	}()

	select {
	case <-channel2:
		channel1 <- 1
	}
}

隐性死锁