Go学习笔记(15)Go并发

x33g5p2x  于2022-03-06 转载在 其他  
字(4.0k)|赞(0)|评价(0)|浏览(508)

写在前面

提及Go,大家都会人云亦云一句“Go支持高并发,适合使用高并发的场景”,事实也确实如此,Go学习笔记系列也终于到了该介绍下最著名的Go并发的时候了,没有介绍并发的Go文章是没有灵魂的哈哈^^

关于并发

提及并发,很容易联想到另外一个概念:并行。它们两个的区别是:

  • 并发主要由切换时间片来实现多个任务“同时”运行
  • 并行是直接通过多核实现多个任务同时运行

一个并发程序可以只在一个处理器或内核上运行多个任务,但是某一时间点只有一个任务在运行;如果运行在多处理器或多核上,就可以在同一时间点有多个任务在运行,才能实现真正的并行,因此,并发程序可以是运行在单处理器(单核)上也可以是运行在多处理器(多核)上。而在Go中,可以设置核数,让并发程序在多核心上真正并行运行,充分发挥多核计算机的能力

Go协程与通道

在其它编程语言中,实现并发程序往往是使用多线程的技术。在一个进程中有多个线程,它们共享同一个内存地址空间。然而使用多线程难以做到准确,尤其是内存中数据共享的问题,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果。多线程解决这个问题的方式是同步不同的线程,对数据加锁
    但是,这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程
    在Go中,使用协程(goroutines)来实现并发程序:

  • 协程是轻量级的,比线程要更轻,只需要使用少量的内存和资源(每个实例4-5k左右的内存栈),因此在必要时可以创建大量的协程,实现高并发
  • 协程与操作系统线程之间没有一对一的关系,协程是根据一个或多个线程的可用性,映射(多路复用)在它们之上的
  • Go多个协程之间使用通道(channel)来同步(它不通过共享内存来通信,而是通过通信来共享内存),当然Go中也提供了sync包可以进行传统的加锁同步操作,但并不推荐使用

在Go中使用协程

在Go中使用协程是通过关键字go调用一个函数或者方法来实现的:

import (
	"fmt"
	"time"
)
func main(){
	go Goroutine()
	time.Sleep(3 * time.Second)
}
func Goroutine(){
	fmt.Println("start a goroutine")
}

根据上面的代码,我们定义了一个Goroutine的函数,并在主程序中使用go关键字去调用该函数,从而启动一个协程去执行Goroutine这个函数。在程序中使用time.Sleep(3*time.Second)是为了让主程序延时3s再结束,否则主程序启动完一个协程后立即退出,我们将没法看到协程函数中打印的信息
    新版本的Go(应该是1.8之后)当我们启动多个协程时,Go将会自动启动多个核心来并行运行,而在老版本的Go里需要我们手动设置,手动设置多核心的操作如下:

import (
	"fmt"
	"runtime"
	"time"
)
func main(){
	num := runtime.NumCPU()
	runtime.GOMAXPROCS(num)    //新版本会自动设置
	for i := 0; i < 10; i++ {
		go Goroutine(i)
	}
	time.Sleep(3 * time.Second)
}
func Goroutine(){
	fmt.Println("start a goroutine")
}

使用channel在协程间通信

通道(channel)是一种特殊的类型,可以理解为发送某种其它类型数据的管道,用于在协程之间通信。数据利用通道进行传递,在任何时间,通道中的数据只能被一个协程进行访问,因此不会发生数据竞争

  • channel通过make进行创建,使用close来关闭
var ch1 chan string
ch1 = make(chan string)   //创建一个用于传递string类型的通道

如果只是声明channel,未初始化,它的值为nil
上面两行代码也可以简写为ch1 := make(chan string)

  • 通道的操作符<-

  • 往通道发送数据:ch <- int1 表示把变量int1发送到通道ch

  • 从通道接收数据:int2 <- ch 表示变量int2从通道ch中接收数据(如果int2没有事先声明过,则要用int2 := <- ch)。直接使用<-ch也可以,也表示从通道中取值,然后该值会被丢弃

func main(){
	c := make(chan bool)
	go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量
		fmt.Println("go go go")
		c <- true
	}()
	<-c         //阻塞,直到从通道取出数据
}

    上面的代码创建了一个布尔型的channel,主程序启动协程后就一直阻塞在<-c那里等待从通道中取出数据,协程中当打印完数据后,就往通道中发送true。主程序此时方从通道中取出数据,退出程序。从而不需要手动让主程序睡眠等待协程完成

  • 大多数情况下channel默认都是阻塞的:从channel取数据一端会阻塞等待channel有数据可以取出才往下执行(如上一段代码中所示);往channel发送数据一端需要一直阻塞等到数据从channel取出才往下执行。如果把上面那段代码中往channel中读取数据的位置调换一下,程序依旧会正常输出
func main(){
	c := make(chan bool)
	go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量
		fmt.Println("go go go")
		<-c 
	}()
	c <- true    //这里把数据传入通道后也会阻塞知道通道中数据被取出        
}
  • 根据需要,channel也可以被设置为有缓存的,有缓存的channel在通道被填满之前不会阻塞(异步)。上面的程序,如果设置为有缓存的channel,那么主程序往通道中发送数据之后就直接退出了
func main(){
	c := make(chan bool, 1)
	go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量
		fmt.Println("go go go")
		<-c 
	}()
	c <- true     //这里往通道发完数据就直接退出了
}

    make(chan type, buf)这里buf是通道可以同时容纳的元素的个数,如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收

  • 使用for-range来操作channel
    for-range可以用在通道上,以便从通道中获取值:
for v := range ch {
	fmt.Println(v)
}

    它从指定的通道中读取数据直到通道关闭才能执行下面的代码,因此程序必须在某个地方close该通道,否则程序将死锁

func main(){
	c:=make(chan bool)
	go func(){
    	fmt.Println("gogogo")
    	c <- true
    	close(c)
	}()
	for v := range c{
        fmt.Println(v)
    }
}

此外,关于channel还需要注意:

  • channel可以设置为单向(只读或只写)或双向通道(能读能写),默认是双向的
  • channel是引用类型
  • 一个channel只能传递一种类型的数据

使用select来切换协程操作

使用select可以从不同的并发执行的协程中获取值,它和switch语句很类似。select可以用来监听进入通道的数据,也可以向通道发送数据

select {
case u:= <- ch1:
        ...
case v:= <- ch2:
        ...
        ...
default: // no value ready to be received
        ...
}

select的功能其实就是处理列出的多个通信中的一个

  • default语句是可选的,fallthrough是不允许的,任何一个case中执行了break或者return语句,select就结束了
  • 如果所有case的通道都阻塞了,会等待直到其中一个可以处理
  • 如果有多个case的通道可以处理,会随机选择一个处理
  • 如果没有通道操作可以处理并且写了default语句,它就会执行default语句
  • select中使用发送操作并且有default可以确保发送不被阻塞!如果没有defaultselect就会一直阻塞
  • select也可以设置超时处理

下面的代码是一个类似生产者-消费者的模式,包括了两个通道和三个协程,其中协程goroutine1goroutine2分别往通道ch1ch2中写入数据,协程goroutine3则通过select分别从两个通道中读出数据并输出

func main() {
        ch1 := make(chan int)
        ch2 := make(chan int)

        go goroutine1(ch1)
        go goroutine2(ch2)
        go goroutine3(ch1, ch2)

        time.Sleep(1e9)
}

func goroutine1(ch chan int) {
        for i := 0; ; i++ {
                ch <- i * 2
        }
}

func goroutine2(ch chan int) {
        for i := 0; ; i++ {
                ch <- i + 5
        }
}

func goroutine3(ch1, ch2 chan int) {
        for {
                select {
                case v := <-ch1:
                        fmt.Printf("Received on channel 1: %d\n", v)
                case v := <-ch2:
                        fmt.Printf("Received on channel 2: %d\n", v)
                }
        }
}

相关文章