Go语言入门-并发

2 篇文章 0 订阅
1 篇文章 0 订阅

Go语言入门-并发

什么是并发

1. 并发:在同一个时间段能够执行多个任务,把时间段分解为时间片,在不同的时间片通过状态切换来执行不同任务。(单个cpu)
提到并发,就得提出另一个相关联的概念并行
2. 并行:在同一个时间点执行的任务有多个。(多个cpu的情况)
多核情况下并发和并行是可以组合的。
而Go语言的并发goroutine也就是协程机制,像java中并发使用的是线程池也就是线程。至于线程和协程的区别。可以从以下进行区分:
协程:独立的、更小的栈空间、共享堆、调度可以在用户态控制、没有优先级、没有编号、没有局部存储等
线程:独立的的栈空间、共享堆、调用由操作系统进行控制(存在线程切换的开销)、有优先级、有编号、存在局部存储等。
可以把go语言中的协程称为用户态的线程,但是比线程更加轻量,实际上goroutine不仅仅是协程,因为goroutinue不仅仅能使用用户态的协程控制,而且在运行时也会创建线程,实现 多线程+协程的运行模式,能够极大的提高执行效率。

go语言提倡:以通讯来共享内存

go语言通过关键字go来创建goroutine

  • 示例 1
func main() {
    //创建一个goroutine
    //该goroutine执行匿名函数,匿名函数中打印“hello gouroutine”后退出
    go func() {
        fmt.Println("hello goroutine")
    }()
    //主goroutine打印“hello main”
    fmt.Println("hello main")
    //休眠1秒 保证子goroutine能够执行
    time.Sleep(1)
}
/**
output:
hello main
hello goroutine
 */

协程-goroutine

前面也提到过协程,在go中goroutine机制实际上是多线程+协程的机制。由Go在运行时调度和管理的。
作为开发人员在函数调用语句签名增加go关键字即可。同时入口函数main也是goroutine的一种。经常成为主goroutine。

创建使用

调度器不保证多个goroutine执行顺序,并且进程退出的时候不会等待goroutine结束,因此在示例1中通过sleep的方式,来保证goroutine能够执行到。

  1. 多个goroutine的运行
  • 示例2 多个goroutine的执行
func print()  {
    fmt.Println("print AAA")
}
func print2() {
    fmt.Println("print BBB")
}
func main() {
    go print()
    go print2()
    go func() {
        fmt.Println("print CCCC")
    }()
    
    fmt.Println("print main")
    time.Sleep(1)
}
/**
output:
print AAA
print main
print CCCC
print BBB
 */

由于goroutine调度不会保证goroutine执行的顺序,因此每次输出的结果可能会有所不同。
既然goroutine不保证执行顺序,那么在非要保证执行顺序的场景只能用户自己实现同步策略,在最后会对同步机制做一些实验来了解下同步机制。

GOMAXPROCS

  1. 执行goroutine时默认goroutine个数参数-GOMAXPROCS
    go语言默认goroutine数可以通过runtime.GOMAXPROCS(n)来进行设置。Go1.5版本之后默认为可用的核数。
  • 示例3 使用默认GOMAXPROCS来执行一下代码
func print()  {
    for i := 0; i < 10; i++ {
        fmt.Println("AAAAAAAAAAAAAAAAAAAA")
    }
}
func print2() {
    for i := 0; i < 10; i++ {
        fmt.Println("BBBBBBBBBBBBBBBBBBBB")
    }
}
func main(){
    //runtime.GOMAXPROCS(1)
    go print()
    go print2()
    time.Sleep(2)
}
/**
output:
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
 */

以上代码输出可以发现先执行的print2后执行的print,而在main函数的调用的过程中却是先执行print在执行print2。说明了默认是启动了多核的模式。

  • 示例4 设置GOMAXPROCS为1
func print()  {
    for i := 0; i < 10; i++ {
        fmt.Println("AAAAAAAAAAAAAAAAAAAA")
    }
}
func print2() {
    for i := 0; i < 10; i++ {
        fmt.Println("BBBBBBBBBBBBBBBBBBBB")
    }
}
func main() {
    //设置GOMAXPROCS数为1
    runtime.GOMAXPROCS(1)
    go print()
    go print2()
    time.Sleep(2)
}
/*
output:
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
 */

以上执行结果也可以发现,实际的OS线程显示执行了print 在执行print2。与调用顺序相同。

  • 示例5 设置GOMAXPROCS数为8
func print()  {
    for i := 0; i < 10; i++ {
        fmt.Println("AAAAAAAAAAAAAAAAAAAA")
    }
}
func print2() {
    for i := 0; i < 10; i++ {
        fmt.Println("BBBBBBBBBBBBBBBBBBBB")
    }
}
func main() {
    //设置GOMAXPROCS数为1
    runtime.GOMAXPROCS(8)
    //runtime.GOMAXPROCS(1)
    go print()
    go print2()
    time.Sleep(2)
}
/*
output:
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA

重复执行多次出现以上情况。
 */

可以看出,在设置GOMAXPROCS数为8以后,打印的结果可能会出现乱序的情况。也就是说明程序启动多核并行的机制。goroutine实际映射到了操作系统的OS线程,这样我们就可以引出Go语言的GMP调度模型。

GMP模型

GPM调度模型是go语言协程调度模型。
G:goroutine协程,是Goroutine的缩写,在GMP模型中主要标识是Goroutine的控制结构,
P:processor处理器,主要维护一组goroutine队列,也会对队里中的goroutine进行调度。每一个P都会维护一组队列。P决定了同事可以并发的任务数量。通过GOMAXPROCS来控制P的个数。P也会把队列里的goroutine和M进行绑定执行。当绑定的线程阻塞 P就会去创建或者切换另一个M。意味着即使P的默认数量是1,也有可能会创建很多个M出来
M:操作系统线程,每一个goroutine最终还是由M来执行。
具体详细介绍可以参考 [典藏版]Golang调度器GMP原理与调度全分析

通道-channel

Don’t communicate by sharing memory; share memory by communicating
go语言提倡 不要通过共享内存来通信,而是通过通信来共享内存。

既然是提倡使用通信来共享内容,Go语言中局引入了通道的特殊类型-chan,chan就像一个队列,遵循FIFO的规则,其内部实现了同步,确保了并发安全。当多个goroutine访问通道时,不需要加锁。(内部具备锁的机制)。

通道的声明

语法

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
  • 示例6 通道的声明
func main() {
    
    //声明通道类型为int的发送和接收的通道
    var ch1 chan int
    //声明通道类型为string的只能接收的通道 读
    var ch2 <-chan string
    //声明通道类型为float64的只能发送的通道 写
    var ch3 chan<- float64
    fmt.Println(ch1)
    fmt.Println(ch2)
    fmt.Println(ch3)
}
/**
output:
<nil>
<nil>
<nil>
 */

可选的<-操作符指定通道方向、发送或接收。如果没有给出方向,则信道是双向的。通过分配或显式转换,可以将信道限制为仅发送或仅接收。

可以得出:

  1. 通道是有类型的,在声明的的时候指定。
  2. 通道声明的时候可以指定通道的读写(发送、收发)属性,通过<-的位置区分 chan<- 表示可以接收,可以写。<-chan表示可以发送,可以读。chan不带<-表示通道可以接受和发送。
  3. 未初始化的通道的值为nil

通道初始化

通道是引用类型,声明通道后需要内建make函数进行初始化才能使用,该函数将通道类型和可选容量作为参数。
语法

make(chan Type, capacity)
  • 示例7 通道的定义
func main() {
    
    //声明通道类型为int的发送和接收的通道
    var ch1 chan int
    //声明通道类型为string的只能接收的通道 读
    var ch2 <-chan string
    //var ch3 chan<- float64
    //初始化通道ch1,通道类型为int,没有容量缓冲区----》可以理解为同步,
    ch1 = make(chan int)
    //初始化通道ch2,通道类位string,有大小缓冲区-大小为10
    ch2 = make(chan string, 10)
    //初始化通道ch3,通道类位float64,有大小缓冲区-大小为10
    ch3 := make(chan float64, 10)
    fmt.Println(ch1)
    fmt.Println(ch2)
    fmt.Println(ch3)
}
/**
output:
0xc000014120
0xc00001c0c0
0xc000090000
*/
  1. 没有指定容量的通道为一般称为无缓冲的通道,只有发送方和接收方都准备就绪的时候才能成功。
  2. 指定容量的通道一般称为有缓冲的通道,通道只要有空的位置发送方就可以写,同时通道中有数据时,接收方就可以读,当条件不满足的时候就会阻塞。

通道使用

写通道

发送数据msg给通道ch
ch <- msg

读通道

从通道ch读取的数据放置msg中,或者丢弃。
msg<-ch
或者
<-ch
<- 箭头指向通道标识写数据、否则读数据。

通道关闭

关闭通道需要是使用内置函数close(ch)关闭通道。
官方close的介绍:

For a channel c, the built-in function close© records that no more values will be sent on the channel. It is an error if c is a receive-only channel. Sending to or closing a closed channel causes a run-time panic. Closing the nil channel also causes a run-time panic. After calling close, and after any previously sent values have been received, receive operations will return the zero value for the channel’s type without blocking. The multi-valued receive operation returns a received value along with an indication of whether the channel is closed.

需要注意的是:
1. close掉的chan无法再写消息,但是可以读消息。当写消息时会panic。
2. close通道不代表通道占用的资源被释放,通道的资源释放由垃圾回收机制控制。
3. 关闭通道不是必须的
4. 对已经关闭的通道且没有值,获取消息会拿到对应类型的零值。
5. 不能重复关闭通道,否则会panic
6. close一个没有初始化的通道(nil)也会panic

7. 使用_, ok := <-ch(ok-idom方式)或者forrange判断通道是否被关闭。

  • 关闭一个关闭的通道:
    ch := make(chan int)
    close(ch)
    //panic: close of closed channel
    close(ch)
  • 关闭没有初始化的通道
    var ch chan int
    //panic: close of nil channel
    close(ch)
  • 关闭通道后继续写数据到通道
    ch := make(chan int, 10)
    ch <- 10
    //panic: send on closed channel
    close(ch)
    ch <- 10
  • 关闭通道后继续读取数据
    ch := make(chan int, 10)
    ch <- 10
    close(ch)
    fmt.Println(<-ch) // 10
    //指定类型的控制 0
    fmt.Println(<-ch) // 0

  • 判断需要读取的通道有没有关闭
func main() {
    ch := make(chan int, 10)
    close(ch)
    _, ok := <-ch
    if !ok {
        fmt.Println("通道被关闭")
    }
}
/**
output:
通道被关闭
 */

以上例子是否可以省略close毕竟通道里本来没有数据,那么意味着ok也是false可以执行到fmt.Println("通道被关闭")?

func main() {
    ch := make(chan int, 10)
    _, ok := <-ch
    if !ok {
        fmt.Println("通道被关闭")
    }
}
/**
output:
凉凉~~~~~~~~~~
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
 */

当执行通道读操作时,如果通道为空就会阻塞。一直阻塞。当通道返回时有两种情况,一种是获取到数据,一种是通道被关闭了。

通道是否关闭的判断
最后再说一下通道关闭,作为写端要判断通道是否被关闭,go没有提供一个内置函数去判断, 只能采用需求救国的机制,可以通过panic和recover的方式来判断;也可以增加标志位,人肉的去维护通道的状态。
下面是关于如何关闭通道一遍博客,感兴趣的可以了解下《How to Gracefully Close Channels

经过思考补充:我感觉合理的方式应该是生产者不负责关闭通道,增加一个监听的goroutine来专门负责关闭通道。符合One general principle of using Go channels is don’t close a channel from the receiver side and don’t close a channel if the channel has multiple concurrent senders. 这个原则。

  • 循环遍历通道
    for range的使用
var N = 10
func main() {
    ch := make(chan int, 10)
    go func() {
        for i := 0; i < N; i++ {
            ch <- i
        }
        close(ch)
    }()
    //for i的方式变量遍历通道
    go func() {
        for i:= 0; i < N; i++ {
            msg := <-ch
            fmt.Printf("for i: %d\n", msg * msg)
        }
    }()
    //for range的方式遍历通道
    go func() {
        for msg := range ch {
            fmt.Printf("for range: %d\n", msg * msg)
        }
    }()
    time.Sleep(time.Second)
}
/**
output:
for i: 0
for i: 4
for i: 9
for i: 16
for i: 25
for i: 36
for i: 49
for i: 64
for i: 81
for i: 0
for range: 1
 */

以上打印的结果还是比较奇怪的:

  1. fori 竟然打印两次for i: 0
  2. for range 只执行了1,for没有执行i

原因
fori打印了两次0的原因应该是通道关闭以后 ch <- i拿到了空值,所以多打印了N(当前N为1)次;其次for range没有出现这种情况。意味着for range会判断通道是否关闭;
最后forifor range会发生竞争。

fori 可以修改ch <- i为ok idom的方式避免通道关闭以后拿到空值的情况。

无缓冲的通道
  • 示例8 通道的使用-无缓冲通道
// read方法入参为msgChan,是一个<-chan int的通道限定只能从通道里取数据。不能写数据。
func read(msgChan <-chan int)  {
    //从通道中获取数据msgChan
    msg := <-msgChan
    //打印接受的数据
    fmt.Println("recv:", msg)
}
// write方法如蝉为msgChan,是一个chan<- int的通道,限制通道只能写数据,不能读数据。
func write(msgChan chan<- int, msg int)  {
    //写入参数msg的值到通道msgChan中
    msgChan <- msg
    //打印发送的数据
    fmt.Println("send:", msg)
}

func main() {
    //类型推导变量msgChan并出示为一个无缓冲的通道
    msgChan := make(chan int)
    //一个协程去读数据
    go read(msgChan)
    //一个协程去写数据
    go write(msgChan, 10)
    //休眠2s却表子协程都正常退出。
    time.Sleep(2)
}
/**
output:
send: 10
recv: 10
 */

可以看出可以使用chan<-<-chan来限制通道的读写权限。防止通道数据被破坏。

  • 示例9 使用通道的机制来通知主goroutine子goroutine退出。(不用sleep了,sleep有点傻)
var exitChan = make(chan struct{})
// read方法入参为msgChan,是一个<-chan int的通道限定只能从通道里取数据。不能写数据。
func read(msgChan <-chan int)  {
    //从通道中获取数据msgChan
    msg := <-msgChan
    //打印接受的数据
    fmt.Println("recv:", msg)
    exitChan <- struct{}{}
}
// write方法如蝉为msgChan,是一个chan<- int的通道,限制通道只能写数据,不能读数据。
func write(msgChan chan<- int, msg int)  {
    //写入参数msg的值到通道msgChan中
    msgChan <- msg
    //打印发送的数据
    fmt.Println("send:", msg)
    //注释掉 exitChan <- struct{}{} 会导致死锁。因为主goroutine一直读exitChan但是读取不到数据。
    exitChan <- struct{}{}
}
//定义一个全局的通道
func main() {
    //类型推导变量msgChan并出示为一个无缓冲的通道
    msgChan := make(chan int)
    //一个协程去读数据
    go read(msgChan)
    //一个协程去写数据
    go write(msgChan, 10)
    //等待接收子线程退出的消息
    <-exitChan
    <-exitChan
}
/**
output:
send: 10
recv: 10
*/

可以使用通道来进行同步控制

有缓冲的通道
  • 示例10 修改示例9的案例演示带缓冲的通道
var exitChan = make(chan struct{})
// read方法入参为msgChan,是一个<-chan int的通道限定只能从通道里取数据。不能写数据。
func read(msgChan <-chan int)  {
    //从通道中获取数据msgChan
    msg := <-msgChan
    //打印接受的数据
    fmt.Println("recv:", msg)
    exitChan <- struct{}{}
}
// write方法如蝉为msgChan,是一个chan<- int的通道,限制通道只能写数据,不能读数据。
func write(msgChan chan<- int, msg int)  {
    //写入参数msg的值到通道msgChan中
    msgChan <- msg
    //打印发送的数据
    fmt.Println("send:", msg)
    //注释掉 exitChan <- struct{}{} 会导致死锁。因为主goroutine一直读exitChan但是读取不到数据。
    exitChan <- struct{}{}
}
//定义一个全局的通道
func main() {
    n := 5
    //类型推导变量msgChan并出示为一个无缓冲的通道
    msgChan := make(chan int, 10)
    //一个协程去读数据
    for i:= 0 ; i < n; i++ {
        go read(msgChan)
        //一个协程去写数据
        go write(msgChan, 10)
        //等待接收子线程退出的消息
    }
    for i:= 0 ; i < n; i++ {
        <-exitChan
        <-exitChan
    }
}
/**
output:
recv: 10
send: 10
recv: 10
send: 10
send: 10
recv: 10
send: 10
recv: 10
send: 10
recv: 10
 */

有缓冲的通道的作用就是减少排队阻塞。效率更快一点。
内置函数caplen用来返回缓冲区大小和当前已缓冲的数量。在同步通道中两个方法返回都是0.

  • 示例11演示len和cap方法。
func main() {
    ch1 := make(chan int, 5)
    //初始情况
    fmt.Printf("len=[%d] cap=[%d]\n", len(ch1), cap(ch1))
    ch1 <- 10
    //放入一个消息后
    fmt.Printf("len=[%d] cap=[%d]\n", len(ch1), cap(ch1))
    //新建通道ch2
    ch2 := make(chan int)
    fmt.Printf("len=[%d] cap=[%d]\n", len(ch2), cap(ch2))
    go func() {
        //放入一个消息
        ch2 <- 10
        //不会打印, 因为这个goroutine阻塞了,只有读走以后才能打印,
        fmt.Printf("len=[%d] cap=[%d]\n", len(ch2), cap(ch2))
    }()
    //保证ch2被放入 消息
    time.Sleep(1)
    fmt.Printf("len=[%d] cap=[%d]\n", len(ch2), cap(ch2))
    time.Sleep(3)
}
/**
output:
len=[0] cap=[5]
len=[1] cap=[5]
len=[0] cap=[0]
len=[0] cap=[0]
 */
select语句和通道

“select”语句选择将继续执行一组可能的发送或接收操作中的哪一个。它看起来类似于“switch”语句,但是所有情况都指向通信操作。不支持fallthrough。用于判断多个io事件。
注意事项:
1. 当存在多个case满足,select会通过伪随机的方式选择一个case块。
2. select不能用fallthrough
3. select没有case满足时,会执行default,如果连default都不存在就会阻塞。
4. select{}会一直阻塞

官方示例:

var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
	print("received ", i1, " from c1\n")
case c2 <- i2:
	print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // same as: i3, ok := <-c3
	if ok {
		print("received ", i3, " from c3\n")
	} else {
		print("c3 is closed\n")
	}
case a[f()] = <-c4:
	// same as:
	// case t := <-c4
	//	a[f()] = t
default:
	print("no communication\n")
}

for {  // send random sequence of bits to c
	select {
	case c <- 0:  // note: no statement, no fallthrough, no folding of cases
	case c <- 1:
	}
}

select {}  // block forever
  • 示例11 select和chan的基本使用
func main1() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        select {
        //阻塞-主goroutine死锁
        case ch<-i:
            fmt.Printf("send : %d\n", i)
        case r := <-ch:
            fmt.Printf("recv: %d\n", r)
        }
    }
}
/**
output:
 deadlock..
 */

func main() {
    ch := make(chan int, 10)
    for i := 0; i < 10; i++ {
        select {
        //阻塞-主goroutine死锁
        case ch<-i:
            fmt.Printf("send : %d\n", i)
        case r := <-ch:
            fmt.Printf("recv: %d\n", r)
        }
    }
    //打印没有被select选择到的通道里的数据。
    fmt.Printf("cap=[%d] len=[%d]\n", cap(ch), len(ch))
}
/**
output:
send : 0
recv: 0
send : 2
send : 3
recv: 2
recv: 3
send : 6
send : 7
send : 8
recv: 6
cap=[10] len=[2]
 */

几个select联系的例子,巩固下。例子-《go语言学习笔记》

func main() {
    a := make(chan int, 3)
    b := make(chan int)

    go func() {
        v := 0
        ok := false
        s := ""
        for {
            select {
            case v, ok = <-a:
                s = "a"
            case v, ok = <-b:
                s = "b"
            }
            if ok {
                fmt.Println(s, v)
            } else {
                os.Exit(0)
            }
        }
    }()
    for i := 0; i < 5; i++ {
        select {
        case a <- i:
        case b <- i:
        }
    }
    close(a)
    select {}
}
/**
output:
a 0
b 4
a 1
a 2
a 3
 */
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    a, b := make(chan int), make(chan int)
    go func() {
        defer wg.Done()
        for  {
            var name string
            var x int
            var ok bool
            select {
            case x, ok = <- a:
                name = "a"
            case x, ok = <- b:
                name = "b"
            }
            if !ok {
                return
            }
            fmt.Println(name, x)
        }
    }()
    go func() {
        defer wg.Done()
        defer close(a)
        defer close(b)
        for i:= 0; i < 10; i++ {
            select {
            case a <- i:
            case b <- i * 10:
            }
        }
    }()
    wg.Wait()
}
func main() {
    done := make(chan struct{})
    c := make(chan int)
    //子goroutine使用select 和for循环遍历io事件,如果遍历不到休眠一秒继续
    go func() {
        defer close(done)
        for {
            select {
            case x, ok := <- c:
                if !ok {
                    return
                }
                fmt.Println("data :", x)
            default://避免阻塞
            }
            fmt.Println(time.Now())
            time.Sleep(time.Second)
        }
    }()
    //等待3秒写数据
    time.Sleep(time.Second * 3)
    c <- 100
    //关闭后, 控制select退出。
    close(c)
    <-done
}

同步-sync

sync.WaitGroup

A WaitGroup waits for a collection of goroutines to finish.
The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines
runs and calls Done when finished. At the same time,Wait can be used to block until all goroutines have finished.

sync.WaitGroup是一种协程同步的机制,主要用于主协程等待其他子协程执行结束后再退出。保证每个子协程能够正常执行结束。在之前的例子中我们使用sleep来保证所有的子协程结束,现在可以改造为sync.WaitGroup的方式。

常用的方法和类型如下 WaitGroup Add Done Wait

// A WaitGroup must not be copied after first use.
type WaitGroup struct {
	noCopy noCopy
	state1 [3]uint32
}
//Add adds delta, which may be negative, to the WaitGroup counter.
func (wg *WaitGroup) Add(delta int) 
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() 
// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait()
  • 示例11-演示 sync.WaitGroup
func main() {
    wg := sync.WaitGroup{}
    
    wg.Add(2) //计数器加2
    
    go func() {
        fmt.Println("testA")
        wg.Done() //计数器减一
    }()
    go func() {
        fmt.Println("testB")
        wg.Done() //计数器减一
    }()
    wg.Wait() //判断计时器是否为0,不为0则阻塞。
}
/**
output:
testB
testA
 */

sys.Mutex

互斥锁mutex不能值传递,只能传指针,负责加锁会失败,提供两个方法Lock() Unlock()

// A Mutex must not be copied after first use.
type Mutex struct {
	state int32
	sema  uint32
}
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
	Lock()
	Unlock()
}
  • 示例13 -演示互斥锁加锁控制计数器累加。
var wg sync.WaitGroup
var look sync.Mutex

func add(x *int64) {
    for i := 0; i < 500; i++ {
        look.Lock() // 互斥锁加锁
        *x = *x + 1
        look.Unlock() // 互斥锁解锁
    }
    wg.Done()
}

func main() {
    var x int64
    wg.Add(2)
    go add(&x) // 分别累加
    go add(&x) // 加锁累加
    wg.Wait()
    fmt.Println(x)
}
/**
output
1000
 */
  • 示例14 演示匿名成员值接收者 值传递锁失效
type data struct {
    sync.Mutex
}

// 值传递 锁会失效 
func (d data) print6(message string)  {
    d.Lock()
    defer d.Unlock()
    for i := 0; i < 6; i++ {
        fmt.Printf("%s------[%d]\n", message, i)
    }
}
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    var d data
    go func() {
        defer wg.Done()
        d.print6("ERROR DEMO   A ")
    }()
    go func() {
        defer wg.Done()
        d.print6("ERROR DEMO   B ")
    }()
    wg.Wait()
}
/**
ERROR DEMO   A ------[0]
ERROR DEMO   B ------[0]
ERROR DEMO   B ------[1]
ERROR DEMO   B ------[2]
ERROR DEMO   B ------[3]
ERROR DEMO   B ------[4]
ERROR DEMO   B ------[5]
ERROR DEMO   A ------[1]
ERROR DEMO   A ------[2]
ERROR DEMO   A ------[3]
ERROR DEMO   A ------[4]
ERROR DEMO   A ------[5]
 */

结构体成员包含 sync.Mutex成员时,必须是指针类型,能能值类型。

  • 示例15 演示匿名成员指针接收者
type data struct {
    sync.Mutex
}
/**
Mutex 作为匿名字段时,相关方法必须实现为pointer-receiver
否则会因为复制多个锁对象导致锁机制失败。
 */
func (d *data) print6(message string)  {
    d.Lock()
    defer d.Unlock()
    for i := 0; i < 6; i++ {
        fmt.Printf("%s------[%d]\n", message, i)
    }
}
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    var d data
    go func() {
        defer wg.Done()
        d.print6("ERROR DEMO   C ")
    }()
    go func() {
        defer wg.Done()
        d.print6("ERROR DEMO   D ")
    }()
    wg.Wait()
}
/**
output:
ERROR DEMO   D ------[0]
ERROR DEMO   D ------[1]
ERROR DEMO   D ------[2]
ERROR DEMO   D ------[3]
ERROR DEMO   D ------[4]
ERROR DEMO   D ------[5]
ERROR DEMO   C ------[0]
ERROR DEMO   C ------[1]
ERROR DEMO   C ------[2]
ERROR DEMO   C ------[3]
ERROR DEMO   C ------[4]
ERROR DEMO   C ------[5]
*/

sync.Mutex作为结构体成员变量,相关的方法如果包含了锁机制,则该方法必须实现pointer-receiver(指针接收者),不然就会锁失效。

  • 示例16 互斥锁支持锁重入吗?
func main() {
    var lock sync.Mutex
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer lock.Unlock()
        lock.Lock()
        //不支持锁重入
        //fatal error: all goroutines are asleep - deadlock!
        //lock.Lock()
        fmt.Printf("go func ")
        wg.Done()
    }()
    wg.Wait()
}
/**
output:
go func
*/

打开注释的lock.Lock() 就会报错,因为不支持锁重入,所以报deadlock.
go语言的程序中没事实现锁重入即没事实现多次锁定的计数。

  • 示例17 演示锁重入,以及使用锁实现一个简单的并发安全的队列
// go语言锁重入验证
type queue struct {
    sync.Mutex
    data[] int
}

func (q *queue) len() int {
    q.Lock()
    n := len(q.data)
    q.Unlock()
    return n
}

func (q *queue) pop() int {
    defer q.Unlock()
    q.Lock()
    n := q.data[0]
    q.data=q.data[1:]
    return n
}

func (q *queue) print() {
    //锁重入
    //fatal error: all goroutines are asleep - deadlock!
    //defer q.Unlock()
    //q.Lock()
    for i := 0; i < q.len(); i++ {
        //len解锁结束以后可以继续加锁,加锁后必须取消
        q.Lock()
        tmp := q.data[i]
        q.Unlock()
        fmt.Printf("%d ", tmp)
    }
    fmt.Println()
}

func main() {
    q := queue{
        data:  []int{100, 4, 3, 54, 100},
    }
    q.print()
    q.pop()
    q.print()
}

/**
output:
100 4 3 54 100
4 3 54 100
*/

sync.RWMutex

  • 示例18 读写锁的使用
type Counter struct {
    int
    sync.RWMutex
}

func (c *Counter) Get() int {
    
    //增加读锁
    c.RLock()
    ret := c.int
    //解开读锁`
    c.RUnlock()
    fmt.Println("Get : ", ret)
    time.Sleep(time.Microsecond * 20)
    return ret
}

func (c *Counter) Add() {
    //增加写锁
    c.Lock()
    c.int++
    //解开写锁
    c.Unlock()
    fmt.Println("Current Count : ", c.int)
    time.Sleep(time.Microsecond * 200)
}


func main() {
    var wg sync.WaitGroup
    var counter Counter
    wg.Add(2)
    go func() {
        for i := 0; i < 10; i++ {
            counter.Add()
        }
        wg.Done()
    }()
    go func() {
        for i := 0; i < 15; i++ {
            counter.Get()
        }
        wg.Done()
    }()
    wg.Wait()
}
/**
出现多次Get连续打印的情况,因为写操作比较慢,但是不会导致数据出现问题。
output:
Get :  0
Current Count :  1
Get :  1
Current Count :  2
Current Count :  3
Get :  3
Current Count :  4
Get :  4
Current Count :  5
Get :  5
Current Count :  6
Get :  6
Get :  6
Current Count :  7
Get :  7
Current Count :  8
Current Count :  9
Get :  9
Current Count :  10
Get :  10
Get :  10
Get :  10
Get :  10
Get :  10
Get :  10
 */

sync.Once

go语言中提供的一种只执行一次的机制。

// Once is an object that will perform exactly one action.

  • 示例19 使用sync.Once实现懒汉式单例模式
type Model struct {
}

var instance *Model
var once sync.Once
//懒汉式单例模式
func GetModel() *Model {
    once.Do(func() {
        instance = &Model{}
    })
    fmt.Printf("instance:[%p]", instance)
    return instance
}

func main() {
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
        fmt.Printf("%p\n", GetModel())
        wg.Done()
    }()
    go func() {
        fmt.Printf("%p\n", GetModel())
        wg.Done()
    }()
    wg.Wait()
}
/**
output:
instance:[0x596c38]0x596c38
instance:[0x596c38]0x596c38
 */

引用:

How to Gracefully Close Channels

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值