Go基础并发编程

Posted justry_deng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go基础并发编程相关的知识,希望对你有一定的参考价值。

并发编程


Go并发的设计

  Go语言最大的特色是并发,而且Go的并发并不像线程或进程那样,受CPU核心数的限制,只要你愿意,你可以启动成千上万个Goroutine协程

相关概念

  • 进程:最小的系统资源申请单位。
  • 线程:最小的执行单位,一个进程内可以启动多个线程。
  • 协程(Goroutine):协程是比线程还要小的执行单位,准确地说,协程是通过线程来执行的。

  在操作系统层面,线程是最小的执行单位。Go语言调度算法会为每个线程提供一个Goroutine协程执行列表,CPU在不同的线程间切换时需要记录上下文信息,如图所示:

  当线程调度执行某个Goroutine协程的时,该协程阻塞了,调度该协程的线程会被挂起,此时,该线程也就没法执行其他Goroutine协程了。此时,Go的调度算法会将该线程的Goroutine协程队列转移到其他线程的Goroutine协程队列中去,如图所示:

启动协程

使用关键字go + 函数调用,即可启动一个协程

示例一:

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("start")

	// 开启协程
	go func() {
		fmt.Println("协程执行了")
	}()

	// 睡眠1s,给足够的时间,观察协程输出
	time.Sleep(time.Second * 1)
	fmt.Println("end")
}

输出:

start
协程执行了
end

示例二:

import (
	"fmt"
	"time"
)

func main() {
	// 开启协程,每隔200毫秒打印一下符号,提升用户体验
	go spinner(time.Millisecond * 200)

	// 计算斐波那契数列
	fmt.Printf("\\n%d\\n", fib(45))
}

// 计算斐波那契数列
func fib(x int) int {
	if x < 2 {
		return x
	}
	return fib(x-2) + fib(x-1)
}

// 打印符号
func spinner(delay time.Duration) {
	for {
		for _, r := range `-\\|/` {
			fmt.Printf("\\r%c", r)
			time.Sleep(delay)
		}
	}
}

输出:

# 动态效果,循环输出 - \\ | /
\\

同步

在Go中,常用的同步机制有

  • WaitGroup

    注:类似于Java的倒计时锁CountDownLatch

  • 互斥锁(Mutex)

  • 读写锁(RWMutex)

  • 条件变量(Cond)

示例:使用同步机制前

import (
	"fmt"
)

func main() {
	for i := 0; i < 10; i++ {
		go func(num int) {
			fmt.Println(num)
		}(i)
	}
	fmt.Println("main线程执行了")
}

输出:

0
main线程执行了

示例:使用同步机制后

import (
	"fmt"
	"sync"
)

var w sync.WaitGroup

func main() {
	for i := 0; i < 10; i++ {
		// 计数+1
		w.Add(1)
		go func(num int) {
			fmt.Println(num)
			// 计数-1
			w.Done()
		}(i)
	}
	// 阻塞等待,直到w值为0
	w.Wait()
	fmt.Println("main线程执行了")
}

输出:

0
2
6
7
4
5
9
3
8
1
main线程执行了

通道channel

  • Linux的管到机制:Linux的管道机制是借助内核开辟的缓冲区实现一个像是"水管"的通道,"水管"两端的进程可以通过这个"水管"进行传递,以达到进程间通信的目标。

  • Go语言的channel:Go语言的channel类似于Linux管道,channel通过阻塞读和阻塞写的方式,可以精准控制Goroutine协程的运行、通信,即:实现了Goroutine协程间的同步。

    注:channel传递数据的方式有点"不见不散"的意思,读和写的双方同时操作的时候才会解除彼此的阻塞,否者一方会死等另一方的到来。

创建channel

创建channel

make(chan chantype)// 或者make(chan chantype, d uint)
  • chan:channel的关键字

    注:chantype代表了通道内可以传递的数据类型,可以是原生类型,也可以是自定义结构。

  • d:代表通道的缓冲区大小;通道本身是同步机制,使用缓冲区可以做到异步操作

channel的读写

channel的读写行为

  • 写行为
    • 通道缓冲区已满(无缓冲区):写阻塞直到缓冲区有空间(或读端有读行为)
    • 通道缓冲区未满:顺利写入,结束
  • 读行为
    • 缓冲区无数据(无数数据时写端未写数据):读阻塞直到写端有数据写入
    • 缓冲区有数据:顺利读数据,结束

channel的读写语法

msg := <- c // 读,将channel c中的数据读取至msgc <- msg // 写,将msg中的数据写入channel c

提示:通过观察channel和箭头的位置即可轻松区分读和写。

示例一:

import (	"fmt"	"sync"	"time")var c chan stringvar w sync.WaitGroupfunc reader() {	msg := <-c // 从c读	fmt.Println("I am reader,", msg)}func main() {	c = make(chan string)	w.Add(1)	go reader()	fmt.Println("begin sleep") // 睡眠3秒是为了看执行效果,验证channel阻塞读	time.Sleep(time.Second * 3)	c <- "hello"                // 往c写	time.Sleep(time.Second * 1) // 睡眠1秒是为了避免main结束得太快而看不到执行效果}

输出:

begin sleepI am reader, hello

示例二:

提示:

  • channel使用完毕后,需要作出close关闭,以达到广播的目的,这样一来其它使用到该channel的"家伙"就能感知到

    注:试想一下,假设协程writterA 通过通道channelB向另一个协程readerC跨协程传输数据,传输完毕后,通道自动关闭(不可能再有人往里面写数据了,因为通道都没了)而没有通知协程readerC,那么由于channel的读写阻塞机制,协程readerC就会一直等下去,进而形成死锁。

  • 关闭channel的动作一定要由写端发起;如果读端关闭了,写端不知情,写端再往已经关闭了的channel写数据就会报错

  下述代码示例为:做一个数字传递的游戏使用3个协程,第一个协程负责将0-9传递给第二个协程,第二个协程将受收到的数据平方后传递给第三个协程,第三个协程负责将收到的数据打印到控制台。

import (	"fmt"	"time")func main() {	var c1 = make(chan int)	var c2 = make(chan int)	// 数数的协程	go func() {		for i := 0; i < 10; i++ {			c1 <- i                     // 向通道c1写入数据			time.Sleep(time.Second * 1) // 这里睡眠1秒,是为了方便观察执行效果,要不然程序一下子就全部输出来了		}		close(c1) // 写完后, 关闭c1	}()	// 计算平方的协程	go func() {		for {			num, ok := <-c1 // 读取c1数据			if ok {				c2 <- num * num // 将平方写入c2			} else {				break // 如果ok==false,即:c1关闭了,那么结束循环			}		}		close(c2) // 写完后, 关闭c2	}()    // main最后负责打印(msin本身也属于一个协程)	for {		for {			num, ok := <-c2 // 读取c2数据			if ok {				fmt.Println(num)			} else {				break // 如果ok==false,即:c2关闭了,那么结束循环			}		}	}}

输出:

0149162536496481

单方向channel

  单方向channelchannel用法其实是一样的,不同的是:单方向channel通过主动声明读通道(或写通道)的方式,从语法上强制约束了该通道只能读(或只能写),降低了程序员对代码的理解难度,让通道的使用更加明了,让channel更友好了。

单方法channel的声明:

chan_name chan <- chan_type // 只写通道chan_name <- chan chan_type // 只读通道

示例:

  下述代码示例为:做一个数字传递的游戏使用3个协程,第一个协程负责将0-9传递给第二个协程,第二个协程将受收到的数据平方后传递给第三个协程,第三个协程负责将收到的数据打印到控制台。

import (	"fmt"	"time")func main() {	var c1 = make(chan int)	var c2 = make(chan int)	// 数数的协程	go func(writeChannel chan<- int) {		for i := 0; i < 10; i++ {			writeChannel <- i           // 向通道writeChannel写入数据			time.Sleep(time.Second * 1) // 这里睡眠1秒,是为了方便观察执行效果,要不然程序一下子就全部输出来了		}		close(writeChannel) // 写完后, 关闭writeChannel	}(c1)	// 计算平方的协程	go func(readChannel <-chan int, writeChannel chan<- int) {		for {			num, ok := <-readChannel // 读取readChannel数据			if ok {				writeChannel <- num * num // 将平方写入writeChannel			} else {				break // 如果ok==false,即:readChannel关闭了,那么结束循环			}		}		close(writeChannel) // 写完后, 关闭writeChannel	}(c1, c2)	// main最后负责打印	for {		for {			num, ok := <-c2 // 读取c2数据			if ok {				fmt.Println(num)			} else {				break // 如果ok==false,即:c2关闭了,那么结束循环			}		}	}}

输出:

0149162536496481

定时器

定时器是Go语言对channel的一个非常典型的应用;当你声明运行一个定时器后,Go语言会每隔一段时间(这个时间在你声明定时器时由你指定)作为写的一方往channel写数据,应用程序作为读的一方,每当收到一次数据,那么即说明过去了指定的时间长度,即达到了定时的效果

示例:

import (	"fmt"	"time")func main() {	ticker := time.NewTicker(time.Second)	num := 5	for {		// 从channel C中读取数据(注:这里虽然读取数据了,但是没有使用变量接收这个数据,这是可以的)		<-ticker.C		fmt.Println(num)		num--		if num == 0 {			break		}	}	ticker.Stop()	launch() // 倒计时结束,发射}func launch() {	fmt.Println("发射!")}

输出:

54321发射!


^_^ 整理自《Go语言区块链应用开发从入门到精通》高野 编著

^_^ 本文已经被收录进《程序员成长笔记》 ,笔者JustryDeng

以上是关于Go基础并发编程的主要内容,如果未能解决你的问题,请参考以下文章

Go基础并发编程

云原生时代崛起的编程语言Go并发编程实战

GO并发编程基础-channel

1.5 Go微服务实战(Go语言基础) --- 并发编程

Go基础并发编程

Go基础并发编程