本文内容包括以下Go并发/并行的内容:
- go
- chan
- 锁
先来看一道并发同步问题:
1114. 按序打印
我们提供了一个类:
public class Foo {
public void first() { print(“first”); }
public void second() { print(“second”); }
public void third() { print(“third”); }
}
三个不同的线程 A、B、C 将会共用一个 Foo 实例。
一个将会调用 first() 方法
一个将会调用 second() 方法
还有一个将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/pr…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
这个题目在力扣上并没有给出Go的提交选项!
解决这道题目设计到Go中的一些并发知识:
关键字go
Go使用名为goroutine的方式来实现并发/并行,routine
的IT专业翻译为”例程,例行程序”,goroutine应该是一个Go诞生后的衍生词汇(嗯,我猜的)。main函数也属于一个goroutine
Go实现并发的方式非常的简单,go + 并发语句,代码如下:
package main
import (
"fmt"
"time"
)
func main() {
go first()
go fmt.Println("second main")
time.Sleep(10) // 如果不添加此句,main协程完毕后其他协程会自动退出而不执行
}
func first() {
fmt.Println("first")
}
func second() {
fmt.Println("second")
}
func third() {
fmt.Println("third")
}
复制代码
多执行几次,会发现语句打印的顺序是随机的。另外,如果不喜欢使用time.Sleep(10)这种方式,也可以使用sync.WaitGroup来保证其他协程的执行。
go
大概有两种使用方式:
(1) go + 函数名/匿名函数
(3) go + 语句或语句块:
go {
/// 代码块
}
复制代码
chan
go设计者认为在并发模型中,消息机制优于共享内存机制,go中的这种机制被称为channel-通道。
chan
, 个人看法,其实并不能说是一种数据类型,因为在内建包(builtin)中并没有写 type chan(不要在意这些细节,我只是没找到官方文档)。chan作为goroutine之间的通信通道,遵循FIFO原则。一个chan只能传递一种类型,但声明为 chan interface{}
可以传递任意类型。
发送数据与接收数据使用 <-
,并且通道是阻塞的,且发送与接收都是阻塞的。即如果接收方从通道获取数据时,发现通道无数据,则会等待发送方发送数据后才会执行,如:
package main
/**
* 不能直接在函数体外使用 ch := make(chan int), “:=”
* 可以使用var ch = make(chan int)
* 或者函数体外定义,函数体内使用make分配空间
* var ch chan int
* ...
* func ...{
* ch = make(chan int)
* }
*/
var ch chan int
func main() {
ch = make(chan int) // 无缓冲的通道
go parafunc(14)
v := <- ch // 阻塞
// 仍然会延迟5秒打印下述语句
fmt.Println(" v = ", v)
}
func parafunc(i int) {
fmt.Println("test...")
time.Sleep(5 * time.Second) // 延迟5秒
ch <- i // 如果没有接收方,也会阻塞
close(ch) // 关闭通道
}
复制代码
chan创建时也可以分配长度,如
ch := make(chan int, 2) // 此时,ch为缓冲区为2的通道
复制代码
此时ch为空时,取值阻塞;当ch填满两个元素时,存储值阻塞。如以下代码:
var ch chan int
func main() {
ch = make(chan int, 1)
go parafunc(14)
time.Sleep(10 * time.Second)
fmt.Println("over")
}
func parafunc(i int) {
fmt.Println("test...")
// time.Sleep(5 * time.Second)
fmt.Println("before...")
ch <- i // 因为容量为1,所以并不会阻塞;但是如果ch = make(chan int)时,则会阻塞
fmt.Println("after....")
close(ch)
}
复制代码
锁
除消息机制外,Go也提供了共享资源加锁机制,包 sync
一些函数可对资源加锁操作。
包atomic
提供了一些原子函数:官方链接
import "sync/atomic"
var count int64
atomic.AddInt64(&count, 1) // 原子操作+1
value := atomic.LoadInt64(&count) // 原子操作:取值
atomic.StoreInt64(&count, 1) // 设置值
复制代码
sync 包提供了互斥锁:
import "sync"
var mutex sync.Mutex
mutex.Lock()
.... // 临界区
mutext.Unlock()
复制代码
解决同步问题
我们回到问题上,如何保持三个方法的同步呢,我们可以使用chan,代码如下。思路是使用两个通道,保持三个方法的同步。当然也可以用三个通道,告知外部程序,三个方法均执行完毕。
package main
import "time"
type Foo struct {
ch1 chan int
ch2 chan int
}
func (f Foo) first() {
print("first")
f.ch1 <- 1 // 存入任何值均可
}
func (f Foo) second() {
<- f.ch1
print("second")
f.ch2 <- 2 // 存入任何值均可
}
func (f Foo) third() {
<- f.ch2
print("third")
}
func main() {
f := Foo{make(chan int), make(chan int)}
go f.first()
go f.second()
go f.third()
time.Sleep(10 * time.Second)
}
复制代码
补充:
-
在默认情况下(不使用
sync.WaitGroup
, 不使用chan阻塞),main的goroutine在存在其他goroutine为执行的情况下也会自动退出,导致有的goroutine无法执行,即所有 goroutine 在 main() 函数结束时会一同结束。所以第一次执行的时候,不添加time.Sleep(10)会发生什么都打印不出来的情况。 -
:=
仅能在函数体内使用,函数外应用如下方式定义参数:var test = "testing" 复制代码
最后的最后,我有个疑问,根据我看到的代码,结构体好像使用指针居多,即我声明方法的习惯是:
func (f Foo) first() {
....
}
复制代码
但是看到很多人都下面这样用:
func (f *Foo) first() {
....
}
复制代码
所以这样有什么好处吗?