go并发编程
创始人
2025-05-30 20:15:11
0

介绍之前,先澄清下并发这个概念,并发指的是多个任务被(一个)cpu 轮流切换执行,在 Go 语言里面主要用 goroutine (协程)来实现并发,类似于其他语言中的线程。

1.goroutine入门示例

go中使用goroutine语法较为简单

go function()

或者使用匿名函数

go function(){
...//
}()

那为什么要使用并发呢,这里通过一个示例给出,定义一个打印输出函数,其中暂停一段时间

import ("log""time"
)func main() {funcTestgoRoutine(1)funcTestgoRoutine(2)funcTestgoRoutine(3)
}func funcTestgoRoutine(index int) {log.Printf("before do something: %d\n", index)time.Sleep(4 * time.Second)log.Printf("after do something: %d\n", index)
}

输出结果如下所示:

2023/03/18 22:14:52 before do something: 1
2023/03/18 22:14:56 after do something: 1
2023/03/18 22:14:56 before do something: 2
2023/03/18 22:15:00 after do something: 2
2023/03/18 22:15:00 before do something: 3
2023/03/18 22:15:04 after do something: 3

执行完成结果展示总共函数12s,因为是串行执行的,每个任务是阻塞的,此时就想能否在当前任务执行sleep时,转而使用cpu片给其他函数执行呢,可以使用goroutine并发执行任务,从而整体加速度,如下所示

import ("log""time"
)func main() {go funcTestgoRoutine(1)go funcTestgoRoutine(2)go funcTestgoRoutine(3)
}func funcTestgoRoutine(index int) {log.Printf("before do something: %d\n", index)time.Sleep(4 * time.Second)log.Printf("after do something: %d\n", index)
}

发现控制台没有任何输出,这是因为main()也是在一个goroutine中执行,但是main执行完成后,这个函数goroutine还没有开始执行,所以无法展示输出到控制台上。为了查看输出结果,可以在main中使用time.Sleep(),以等待其他函数执行完成。

func main() {go funcTestgoRoutine(1)go funcTestgoRoutine(2)go funcTestgoRoutine(3)time.Sleep(6*time.Second)fmt.Println("等待main函数执行完毕")
}

输出如下所示

2023/03/18 22:21:57 before do something: 3
2023/03/18 22:21:57 before do something: 1
2023/03/18 22:21:57 before do something: 2
2023/03/18 22:22:01 after do something: 2
2023/03/18 22:22:01 after do something: 1
2023/03/18 22:22:01 after do something: 3
等待main函数执行完毕

可以看到输出时间从过去的12s降低为4s,执行效率明显提升。对于goroutine的使用场景:特别使用IO密集型,比如文件,网络读写任务

这里很容易忽视一个问题,对比下以下的case

package mainimport ("fmt"
)func hello() {fmt.Println("hello func")
}func main() {// hello   // 无并发场景go hello() // 有并发场景fmt.Println("hello main")
}

第一个输出:

hello func
hello main

第二个输出:

hello main

在hello前面加上go之后,就是启动一个goroutine区执行这个func函数。但是第二次并没有打印输出hello func,究其原因就是go在启动程序是,Go中程序会main函数创建一个默认的goroutine。在上述的main中使用go去创建了另一个goroutine去执行hello函数,此时main goroutine还在继续执行,整个程序中有两个并发执行的goroutine,当main函数结束时整个程序就结束了,同时main goroutine也结束了,所有由main goroutine创建的goroutine也会一起退出,就是main退出的太快了,另一个goroutine中的函数还没有执行完成就退出了,所以导致没有打印出hello func.解决方法上面的实例中已经给出了,就是在main中增加sleep.

func main() {go hello()fmt.Println("hello main")
}func hello()  {fmt.Println("hello func")time.Sleep(time.Second)
}

以本地机器为2.6 GHz 六核Intel Core i7为例,每次输出都是先输出hello main,这是为什么呢?
因为程序创建goroutine需要一定的开销,而与此同时main函数所在的goroutine是一直在继续向下执行的。
在这里插入图片描述![在这里插入图片描述](https://img-blog.csdnimg.cn/2dc78c456f134c8cb1d93d81f1447b6a.png

2.sync.WaitGroup实现同步

上面的示例中手动在main中暂停了,实际在函数调用时,并不知道某个函数实际会执行多久,所以可以使用sync.WaitGroup来等待所有的goroutine结束,从而实现并发同步,这个明显具有编程之美。

package mainimport ("log""sync""time"
)func main() {var wg sync.WaitGroupwg.Add(3)go funcTestgoRoutineAndSync(1, &wg)go funcTestgoRoutineAndSync(2, &wg)go funcTestgoRoutineAndSync(3, &wg)wg.Wait()log.Printf("i have finshed all jobs\n")
}func funcTestgoRoutineAndSync(index int, wg *sync.WaitGroup) {defer wg.Done()log.Printf("before do something: %d\n", index)time.Sleep(4 * time.Second)log.Printf("after do something: %d\n", index)}

输出结果如下所示:

2023/03/18 22:30:39 before do something: 1
2023/03/18 22:30:39 before do something: 3
2023/03/18 22:30:39 before do something: 2
2023/03/18 22:30:43 after do something: 2
2023/03/18 22:30:43 after do something: 1
2023/03/18 22:30:43 after do something: 3
2023/03/18 22:30:43 i have finshed all jobs

这里关于Add函数,wg.Add(delta int)Add将增量添加到计数器中(可能为负),如果计数器为零则释放所有的goroutine,如过计数器为负则需要添加panic。所有的Add操作必须在Wait之前。WaitGroup有三个对外开放的API,分别是Add(delta int),Done(),Wait()。在协程开始前将调用Add(3),表示等待3个协程的退出,使用defer wg.Done(),当WaitGroup中的计数器归零时,Wait()会取消阻塞。这里既然提到了锁,接着介绍一下。Go语言标准库sync提供了2种锁🔐,互斥锁sync.Mutex和读写读写锁sync.RWMutex.互斥锁即不可同时运行,使用了互斥锁的两个代码片互相排斥,只有其中一个代码片执行完成后,另一个才能执行。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock 加锁
  • Unlock 释放锁

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行,也可以用 defer 语句来保证互斥锁一定会被解锁。在一个 Go 协程调用 Lock 方法获得锁后,其他请求锁的协程都会阻塞在 Lock 方法,直到锁被释放。

关于读写锁。想象一下这种场景,当你在银行存钱或取钱时,对账户余额的修改是需要加锁的,因为这个时候,可能有人汇款到你的账户,如果对金额的修改不加锁,很可能导致最后的金额发生错误。读取账户余额也需要等待修改操作结束,才能读取到正确的余额。大部分情况下,读取余额的操作会更频繁,如果能保证读取余额的操作能并发执行,程序效率会得到很大地提高。保证读操作的安全,那只要保证并发读时没有写操作在进行就行。在这种场景下我们需要一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。

这种锁称之为 多读单写锁 (multiple readers, single writer lock),简称读写锁,读写锁分为读锁和写锁,读锁是允许同时执行的,但写锁是互斥的。一般来说,有如下几种情况:

读锁之间不互斥,没有写锁的情况下,读锁是无阻塞的,多个协程可以同时获得读锁。
写锁之间是互斥的,存在写锁,其他写锁阻塞。
写锁与读锁是互斥的,如果存在读锁,写锁阻塞,如果存在写锁,读锁阻塞。
Go 标准库中提供了 sync.RWMutex 互斥锁类型及其四个方法:

  • Lock 加写锁
  • Unlock 释放写锁
  • RLock 加读锁
  • RUnlock 释放读锁
    读写锁的存在是为了解决读多写少时的性能问题,读场景较多时,读写锁可有效地减少锁阻塞的时间。

3.channel

goroutine是Go中实现并发的重要机制,channel是goroutine之间通信的重要桥梁。使用内建函数make创建channel

ch := make(chan int)  
// 或者使用var声明channel
var ch chan int

上面的这些声明channel都是双工的,即channel可以收发数据,接收和发送是两个操作动作。

ch <- x // channel 接收数据 x
x <- ch // channel 发送数据并赋值给 x
<- ch // channel 发送数据,忽略接受者

如果使用make(chan int)创建channel,这类channel称为非缓冲通道,channel可以定义时可以指定容量大小

chInt := make(chan int)       // unbuffered channel  非缓冲通道
chBool := make(chan bool, 0)  // unbuffered channel  非缓冲通道
chStr := make(chan string, 2) // bufferd channel     缓冲通道

📢注意:如果是是非缓冲通道,则需要有不同的goroutine对非缓冲通道机型接收和发送操作动作,否则会造成通道阻塞。

package main
import "fmt"
func main() {ch := make(chan string)ch <- "ping"fmt.Println(<-ch)
}

输出结果如下所示:

fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()

因为 main 函数是一个 goroutine, 在这一个 goroutine 中发送了数据给非缓冲通道,但是却没有另外一个 goroutine 从非缓冲通道中里读取数据, 所以造成了阻塞或者称为死锁,做如下修改即可

func main() {ch := make(chan string)go func() {ch <- "ping"}()fmt.Println(<-ch)
}

那如果确实没有指定接收操作,可以使用缓冲通道,当然缓冲通道也只能接受容量范围内的数据,即没有另外的goroutine进行读取操作

func main() {ch := make(chan int, 2)ch <- 1ch <- 2
}

3.1 单向channel

单向channel可以粗暴的理解为udp通信一样,发送就是了,至于有无收到或者是否正确收到不用处理,单向通道限定了该channel只能接收或者发送数据,单向通道通常作为函数的参数进行使用。如下:

package mainfunc main() {chan1 := make(chan string, 1)chan2 := make(chan string, 2)receive(chan1, "pass message")send(chan1, chan2)
}func receive(receiver chan<- string, msg string) {receiver <- msg
}func send(sender <-chan string, receiver chan<- string) {msg := <-senderreceiver <- msg
}

3.2 channel遍历和关闭

close() 函数可以用于关闭 channel,关闭后的 channel 中如果有缓冲数据,依然可以读取,但是无法再发送数据给已经关闭的channel

func main() {ch := make(chan int, 10)for i := 0; i < 10; i++ {ch <- i}close(ch)res := 0for v := range ch {res += v}fmt.Println(res)
}

3.3 select

select专门用于通道发送和接收操作,看起来和switch有点相似,但是进行选择和判断的方式方法不同。在章节2中我们使用sync.WaitGroup实现goroutine执行完成后才退出main函数,这里使用select方式

package mainimport ("fmt""time"
)func main() {chStr := make(chan string)chInt := make(chan int)go strWorker(chStr)go intWorker(chInt)for  i := 0 ;i < 2;i ++{select {case <- chStr:fmt.Println("get value from strWorker")case <- chInt:fmt.Println("get value from intWorker")}}
}func strWorker(ch chan string)  {time.Sleep(1 * time.Second)fmt.Println("to do something with strWorker...")ch <- "str"
}func intWorker(ch chan int)  {time.Sleep(2 * time.Second)fmt.Println("to do something with intWokrer...")ch <- 1
}

当然channel也可以实现同步机制,上午说过main是一个goroutine,通过非缓冲队列的使用,能够保障goroutine执行结束之前main函数不会提前退出

package mainimport ("fmt"
)func worker(done chan bool) {fmt.Println("start working...")done <- truefmt.Println("end working...")
}func main() {done := make(chan bool, 1)go worker(done)<- done
}

4.锁的使用

前面的章节中已经提到了go中的sync锁,在章节3中已经介绍了如何使用channel在多个goroutine之间进行通信,其实对于并发还有一种较为常见的通信方式,那就是共享内存。

package mainimport ("log""time"
)var name stringfunc main() {name = "hello world"go printName()go printName()time.Sleep(time.Second)name = "welcome"go printName()go printName()time.Sleep(time.Second)
}func printName() {log.Println("the name is ", name)
}

输出结果如下所示:

2023/03/19 20:29:56 the name is  hello world
2023/03/19 20:29:56 the name is  hello world
2023/03/19 20:29:57 the name is  welcome
2023/03/19 20:29:57 the name is  welcome

在两个goroutine中可以访问Name变量,当其修改后,在不同的goroutine中都可以同时获取到最新的值,这就是最简单的通过共享内存(变量)的方式在多个goroutine中进行通信的方式。有了上面的示例,接着再看另一个例子

package mainimport ("fmt""sync"
)func main() {var (wg      sync.WaitGroupnumbers []int)for i := 0; i < 10; i++ {wg.Add(1)go func(i int) {numbers = append(numbers, i)wg.Done()}(i)}wg.Wait()fmt.Println("The numbers is ", numbers)
}

这里连续输出两次,查阅输出结果

The numbers is  [9 0 1 2 3 4 5 6 7 8]
The numbers is  [0 2 1 4 3 5 6 7 9]

发现输出结果不一致,并发对同一个切片进行写操作的时候,会出现数据不一致的问题,这就是一个典型的共享变量的问题,针对这个问题使用Lock🔐来修复,从而保障数据的一致性。

package mainimport ("fmt""sync"
)func main() {var (wg sync.WaitGroupnumbers []intmux sync.Mutex)for  i := 0;i < 10;i++{wg.Add(1)go func(i int) {mux.Lock()numbers = append(numbers,i)mux.Unlock()wg.Done()}(i)}wg.Wait()fmt.Println("The numbers it ",numbers)
}

优化之后,再次运行,正确输出0-9的numbers。sync.Mutex是互斥锁,只有一个信号量,对于sync.RWMutex,共享的对象如果可以分离出读、写两个互斥信号量,可以使用它提供读写的并发性能。参考sync.RWMutex

参考文章go并发 go高性能并发编程

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...