十一、并发编程

1.goroutine

  • Go 语言在语言级别支持轻量级线程,叫goroutine。
  • 在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。
  • 当被调用的函数返回时,这个goroutine也自动结束了。
  • 需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。
  • 创建于进程中。直接使用 go 关键,放置于函数调用前面,产生一个 go程(并发)。
  • goroutine的特性:主go程结束,子go程随之退出。
//子go程
func sing(i int) {
	fmt.Println(i, "--> 唱歌...")
	time.Sleep(100 * time.Millisecond) //睡一下,让出CPU
}

//子go程
func dance(i int) {
	fmt.Println(i, "--> 跳舞...")
	time.Sleep(100 * time.Millisecond) //睡一下,让出CPU
}

//主go程
func main() {
	for i := 0; i < 10; i++ {
		go sing(i)  //goroutine 并发
		go dance(i) //goroutine 并发
	}
	//主go程结束,子go程随之退出。所以这里要睡10秒等子go程执行完毕,不然看不见子go程打印输出。
	time.Sleep(10 * time.Second)
}
1.1runtime包
  • runtime.Gosched():出让当前go程所占用的 cpu时间片。当再次获得cpu时,从出让位置继续恢复执行。
  • runtime.Goexit():立即终止当前 goroutine执行,调度器确保所有已注册 defer延迟调用被执行。
  • runtime.GOMAXPROCS(n int):设置当前进程使用的最大cpu核数。返回上一次调用成功的设置值。首次调用返回默认值。
  • runtime.NumCPU():获取CPU核心数。

2.channel

  • 不要通过共享内存来通信,而应该通过通信来共享内存。
  • channel是Go语言在语言级别提供的goroutine间的通信方式。
  • 我们可以使用channel在两个或多个goroutine之间传递消息。
  • channel 是一种数据类型,主要用来解决go程的同步问题及协程之间数据共享的问题。
  • 我们在使用Go语言开发时,经常会遇到需要实现条件等待的场景,这也是channel可以发挥作用的地方。
  • 定义channel:make(chan <类型>, <容量>)。ch := make(chan string)
  • channel有两个端:
    • 写端:ch <- “hehe”。写端写数据,读端不在读。写端阻塞
    • 读端:str := <- ch。读端读数据,同时写端不在写,读端阻塞。
  • len(ch):channel 中剩余未读取数据个数。cap(ch):通道的容量。
  • 确定不再相对端发送、接收数据。关闭channel。使用:close(chs)
func main() {
	chs := make(chan int, 10)
	chs <- 1 //写入一个数据

	fmt.Println(len(chs)) //未读取数据个数
	fmt.Println(cap(chs)) //通道的容量

	//tmp := <-chs
	//fmt.Println(tmp) //读一个

	//close(chs) //关闭channel,channel中数据读完了才最终成功关闭
	//判断 channel 是否关闭
	if data, ok := <-chs; ok {
		fmt.Println("ok:", data)
	} else {
		fmt.Println("channel已经关闭...")
	}

	//循环写
	go func() {
		for i := 0; i < 10; i++ {
			chs <- i
		}
		close(chs) //关闭channel
	}()

	//循环读
	for ch := range chs {
		fmt.Print(ch, " ")
	}
}
  • 总结
  • 数据不发送完,不应该关闭。
  • 已经关闭的channel,不能再向其写数据。报错:panic: send on closed channel
  • 写端已经关闭channel,可以从中读取数据。
  • 无缓冲channel:ch := make(chan int) 或 make(chan int, 0) 同步通信(打电话)
  • 有缓冲channel:ch := make(chan int, 5) 异步通信(发短信)
2.1单向channel
  • channel本身必然是同时支持读写的,否则根本没法用。
  • 假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。
  • 同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。
  • 所谓的单向channel概念,其实只是对channel的一种使用限制。
  • 默认的channel 是双向的。var ch chan int; ch = make(chan int)
  • 单向写channel。var sendCh chan <- int; sendCh = make(chan <- int) 不能读操作
  • 单向读channel。var recvCh <- chan int; recvCh = make(<-chan int)
  • 转换:双向channel 可以 隐式转换为 任意一种单向channel;单向 channel 不能转换为 双向 channel
ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel
  • 下面我们来看一下单向channel的用法:
  • 这个函数不会因为各种原因而对ch进行写,避免在ch中出现非期望的数据,从而很好地实践最小权限原则。
func Parse(ch <-chan int) {
	for value := range ch {
		fmt.Println("Parsing value", value)
	}
}

3.select

  • Go语言直接在语言级别支持select关键字,用于处理异步IO问题。
  • select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作。
  • 每个case语句都必须是一个面向channel的操作。
  • select的特点是只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况。
select {
	case <-chan1:
		// 如果chan1成功读到数据,则进行该case处理语句
	case chan2 <- 1:
		// 如果成功向chan2写入数据,则进行该case处理语句
	default:
		// 如果上面都没有成功,则进入default处理流程
}
  • 以下程序实现了一个随机向ch中写入一个0或者1的过程。
func main() {
	ch := make(chan int, 1)
	for {
		select {
			case ch <- 0:
			case ch <- 1:
		}
		i := <-ch
		fmt.Println("Value received:", i)
	}
}
3.1超时机制
  • 在并发编程的通信过程中,最需要处理的就是超时问题,即向channel写数据时发现channel已满,或者从channel试图读取数据时发现channel为空。
  • Go语言没有提供直接的超时处理机制,但我们可以利用select机制。
  • 因为select的特点是只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况。
func main() {
	// 首先,设定一个超时的定时器
	timeout := time.After(1 * time.Second)

	ch := make(chan string, 1)
	go func() {
		time.Sleep(2 * time.Second)
		ch <- "ok"
	}()

	// 然后我们把timeout这个channel利用起来
	select {
	case <-ch:
		fmt.Println("读取到数据了。。。") // 从ch中读取到数据
	case <-timeout:
		fmt.Println("超时了。。。") // 一直没有从ch中读取到数据,但从timeout中读取到了数据
	}
}

4.生产者消费者模型

//生产者:单向写channel
func producer(out chan<- int, idx int) {
	for {
		num := rand.Intn(800) //产生随机数
		out <- num
		fmt.Printf("生产者%dth,生产:%d\n", idx, num)
		time.Sleep(time.Millisecond * 200)
	}
}

//消费者:单向读channel
func consumer(in <-chan int, idx int) {
	for {
		num := <-in
		fmt.Printf("-----消费者%dth,消费:%d\n", idx, num)
		time.Sleep(time.Millisecond * 200)
	}
}

func main() {
	rand.Seed(time.Now().UnixNano()) //随机种子
	product := make(chan int, 5)     //数据缓冲池,容量5

	for i := 0; i < 1; i++ {
		go producer(product, i+1) //1个生产者
	}

	for i := 0; i < 1; i++ {
		go consumer(product, i+1) //1个消费者
	}

	quit := make(chan bool)
	<-quit //没有写,直接读,让主go程阻塞
}
  • 如果只有一个生产者、一个消费者,这个看不出什么问题。
  • 当有多个生产者、多个消费者,就会存在一个问题:
  • 消费者不是按照生产者生产的产品的先后顺序来消费产品的。
  • 解决办法,就是引入条件变量,让数据缓冲池中的产品,生产一点,消费一点。
  • 有点像分布式消息发布订阅系统 kafka一样。
4.1条件变量
var cond sync.Cond // 定义全局条件变量

func producer(out chan<- int, idx int) {
	for {
		cond.L.Lock() //先加锁
		// 判断缓冲区是否满
		for len(out) == 5 {
			cond.Wait() //挂起当前子go程,等待条件变量满足,被消费者唤醒
		}
		num := rand.Intn(800) //产生随机数
		out <- num
		fmt.Printf("生产者%dth,生产:%d\n", idx, num)
		cond.L.Unlock() // 访问公共区结束,并且打印结束,解锁
		// 唤醒阻塞在条件变量上的 消费者
		cond.Signal() //cond.Broadcast() 广播方式,唤醒全部生产者
		time.Sleep(time.Millisecond * 200)
	}
}

func consumer(in <-chan int, idx int) {
	for {
		cond.L.Lock() //先加锁
		// 判断 缓冲区是否为空
		for len(in) == 0 {
			cond.Wait() //挂起当前子go程,等待条件变量满足,被生产者唤醒
		}
		num := <-in
		fmt.Printf("-----消费者%dth,消费:%d\n", idx, num)
		cond.L.Unlock() // 访问公共区结束后,解锁
		// 唤醒 阻塞在条件变量上的 生产者
		cond.Signal() //cond.Broadcast() 广播方式,唤醒全部生产者
		time.Sleep(time.Millisecond * 200)
	}
}

func main() {
	rand.Seed(time.Now().UnixNano()) //随机种子
	cond.L = new(sync.Mutex)         // 指定条件变量 使用的锁
	product := make(chan int, 5)     //数据缓冲池,容量5

	for i := 0; i < 5; i++ {
		go producer(product, i+1) //5个生产者
	}

	for i := 0; i < 5; i++ {
		go consumer(product, i+1) //5个消费者
	}

	quit := make(chan bool)
	<-quit //没有写,直接读,让主go程阻塞
}

5.定时器

  • 创建定时器,指定定时时长,定时到达后。系统会自动向定时器的里写系统当前时间。(对 chan 的写操作)
//第一种定时:Sleep
func timer1() {
	fmt.Println("系统时间:", time.Now())
	time.Sleep(2 * time.Second)
	fmt.Println("系统时间:", time.Now())
}

//第二种定时:NewTimer
func timer2() {
	fmt.Println("系统时间:", time.Now())
	//创建一个定时器
	myTimer := time.NewTimer(2 * time.Second)

	nowTime := <-myTimer.C //系统会自动向定时器的成员 C 写 系统当前时间。
	fmt.Println("当前时间", nowTime)
	fmt.Println("系统时间:", time.Now())
}

//第三种定时:After
func timer3() {
	fmt.Println("系统时间:", time.Now())
	nowTime := time.After(2 * time.Second)
	fmt.Println("当前时间", <-nowTime)
	fmt.Println("系统时间:", time.Now())
}

func main() {
	timer1()
	fmt.Println("------------------")
	timer2()
	fmt.Println("------------------")
	timer3()
}
5.1定时器重置、停止
func main() {
	//创建一个定时器
	myTimer := time.NewTimer(2 * time.Second)
	//myTimer.Reset(5 * time.Second) //重置
	go func() {
		<-myTimer.C
		fmt.Println("子go程,定时结束。。。")
	}()
	myTimer.Stop() //停止,将定时器归零。 <-myTimer.C 会阻塞
	time.Sleep(10 * time.Second)
	fmt.Println("主go程醒了。。。")
}
5.2周期定时器
func main() {
	//创建周期定时器
	//定时时长到达后,系统会自动向 Ticker 的 C 中写入 系统当前时间。
	//并且,每隔一个定时时长后,循环写入 系统当前时间。
	myTicker := time.NewTicker(1 * time.Second)
	go func() {
		for {
			fmt.Println("当前时间:", <-myTicker.C)
		}
	}()
	time.Sleep(10 * time.Second)
	fmt.Println("周期定时器。。。")
}

6.同步

6.1channel同步
func print(str string) {
	for _, data := range str {
		fmt.Printf("%c ", data)
		time.Sleep(100 * time.Millisecond)
	}
}

func print1(ch chan int) {
	print("Hello")
	ch <- 1 //打印完了再写入
}

func print2(ch chan int) {
	<-ch //读取到数据再打印
	print("World")
}

func main() {
	ch := make(chan int, 1)
	//顺序打印HelloWorld
	go print1()
	go print2()
	time.Sleep(3 * time.Second)
}
6.2同步锁
  • Go语言包中的sync包提供了两种锁类型:
  • sync.Mutex,当一个goroutine获得了Mutex后,其他goroutine就只能乖乖等到这个goroutine释放该Mutex。
  • sync.RWMutex,经典的单写多读模型。读时共享,写时独占。写锁优先级比读锁高。
    • lock.Lock() 加写锁
    • lock.Unlock() 解写锁
    • lock.RLock() 加读锁
    • lock.RUnlock() 解读锁
var lock sync.Mutex

func print(str string) {
	lock.Lock()
	for _, data := range str {
		fmt.Printf("%c ", data)
		time.Sleep(100 * time.Millisecond)
	}
	lock.Unlock()
}

func print1() {
	print("Hello")
}

func print2() {
	print("World")
}

func main() {
	//顺序打印HelloWorld
	go print1()
	go print2()
	time.Sleep(3 * time.Second)
}
6.3全局唯一性
  • 对于从全局的角度只需要运行一次的代码,比如全局初始化操作,Go语言提供了一个Once类型来保证全局的唯一性操作
var once sync.Once

func setup() {
	fmt.Println("初始化完成。。。")
}

func doprint() {
	//setup()
	//once的Do()方法可以保证在全局范围内只调用指定的函数一次
	once.Do(setup)
	fmt.Println("开始搞事情。。。")
}

func main() {
	go func() {
		for {
			doprint()
			time.Sleep(2 * time.Second)
		}
	}()
	time.Sleep(10 * time.Second)
}
  • 程序输出:
初始化完成。。。
开始搞事情。。。
开始搞事情。。。
开始搞事情。。。
开始搞事情。。。
开始搞事情。。。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值