盘点Golang并发那些事儿之二(上)

简介: 盘点Golang并发那些事儿之二

并发控制得心应手


上一节提到,golang中直接使用关键字go创建goroutine,无法满足我们的需求。主要问题如下


  • 无法有效的并发执行完成


  • 无法有效的控制并发


首先我们再来看看下面这个栗子,代码如下


1// example-goroutine anonymous
 2
 3package main
 4
 5import (
 6    "fmt"
 7    "time"
 8)
 9
10func anonymous1() {
11    startTime := time.Now()
12    // 创建十个goroutine
13    for i := 0; i < 10; i++ {
14        go func() {
15            fmt.Println("HelloWord~, stamp is", i)
16        }()
17    }
18    fmt.Println("Main~")
19    spendTime := time.Since(startTime)
20    fmt.Println("Spend Time:", spendTime)
21    // 防止goroutine提前退出
22    // time.Sleep(time.Second)
23}
24
25// goroutine anonymous
26func main() {
27    anonymous2()
28}

image.gif

此时你会发现有些任务被多次执行了,但有些任务却又没有被执行。以上例子虽加速了运行,但带来的损失却也是巨大的。例如银行转账等,一旦出现以上情况多次付款也随之而来了。弊大于利


首先我们来分析以上代码,为什么会出现此种情况?虽然是个废品,但也是俺辛辛苦苦的写的不是,让俺做个明白鬼。


image.png


我们从里面往外分析anonymous1首先他是个匿名函数 + 立即执行函数,且变量i并不是传递的参数,而是外部循环带进来的。由上图,我们知道,执行流程为先创建

goroutine,执行逻辑,返回结果。


请思考:


goroutine,越多越好么?为什么


如何避免以上情景?如何避免提前退出?


信道-Channel


信道的英文是channel,在golang当中的关键字是chan。它的用途是用来在goroutine之间传输数据,这里你可能要问了,为什么一定得是goroutine之间传输数据呢,函数之间传递不行吗?


因为正常的传输数据直接以参数的形式传递就可以了,只有在并发场景当中,多个线程彼此隔离的情况下,才需要一个特殊的结构传输数据。


Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信


如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。


Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。


channel底层的实现为互斥锁


example


1var 变量 chan 元素类型
 2// example-var
 3// 只声明
 4var a chan int
 5var b chan string
 6var c chan byte
 7var d chan []string
 8var e chan []int
 9// 实例化
10a = make(chan []int)
11
12//example-2(推荐使用)
13管道名称 := make(chan 数据类型 [缓冲区size])


无缓冲channel


示例代码如下


1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func hello(intCh <-chan int) {
 9    fmt.Println("Hello, Gopher. I am stamp[Id]", <-intCh)
10    time.Sleep(time.Second * 2)
11}
12
13func main() {
14    startTime := time.Now()
15    const jobNumber = 100 * 100
16    // create chan
17    intCh := make(chan int)
18    for i := 0; i <= jobNumber; i++ {
19        // create goroutine same number for jobNumber
20        go hello(intCh)
21        intCh <- i
22    }
23    fmt.Println("Completed, Spend Time :", time.Since(startTime))
24}


image.png


这速度可谓是非常的快啊


带缓冲Channel


带缓冲的 channel(buffered channel) 是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:


无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证


来段代码压压惊


1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func hello(intCh <-chan int) {
 9    fmt.Println("Hello, Gopher. I am stamp[Id]", <-intCh)
10    time.Sleep(time.Second * 2)
11}
12
13func hello1(intCh <-chan int) {
14    fmt.Println("Hello, Gopher1. I am stamp[Id]", <-intCh)
15    time.Sleep(time.Second * 2)
16}
17
18func main() {
19    startTime := time.Now()
20    const jobNumber = 100 * 100
21    // create chan
22    intCh := make(chan int, 100)
23    for i := 0; i <= jobNumber; i++ {
24        // create goroutine same number for jobNumber
25        go hello(intCh)
26        go hello1(intCh)
27        intCh <- i
28    }
29    fmt.Println("Completed, Spend Time :", time.Since(startTime))
30}


运行效果如下

image.gif


这速度杠杠滴哈,别急,同时也让我和你说执行流程,老规矩,上图


image.png


首先开始执行把需要传递的数据打到channle里面,然后goroutine去取,执行。那么有留下几个问题


还可以加速么?


加速的方法?


可能带来什么新的问题?


如何解决?

单向 channel

有时候,我们有一些特殊的业务需求,比如限制一个 channel 只可以接收但是不能发送,或者限制一个 channel 只能发送但不能接收,这种 channel 称为单向 channel。

单向 channel 的声明也很简单,只需要在声明的时候带上 <- 操作符即可,如下面的代码所示:


1onlySend := make(chan<- int)
2onlyReceive:=make(<-chan int)


使用单向 channel 的较多场景一般在函数参数中使用较多,这样可以防止一些操作影响了 channel。


1//example channel
 2onlySend := make(chan<- int)
 3onlyReceive:=make(<-chan int)
 4
 5//example function
 6package main
 7
 8import "fmt"
 9
10func exs(accept <-chan int, recipient chan<- int) {
11    for result := range accept {
12        fmt.Println("Received only sent channel a:", result)
13        recipient <- result + 2
14    }
15
16    //fmt.Println("Send Only", recipient)
17}
18
19func main() {
20    const processNumber = 100
21    sender := make(chan int, processNumber)
22    recipient := make(chan int, processNumber)
23    for e := 0; e < 10; e++ {
24        go exs(sender, recipient)
25    }
26
27    for s := 0; s < processNumber; s++ {
28        sender <- s
29    }
30    for r := 0; r < processNumber; r++ {
31        //<-recipient
32        fmt.Println("recipient", <-recipient)
33    }
34}


小技巧:箭头该谁指谁?这可把我整的不好了,别慌,我告诉你,到底该谁指谁。其实很简单


箭头一致向左指


1a <- chan <- b
2
3// 存入
4chan <- b
5
6// 取出
7a := <- chan


Chan其实就是起到一个中间人的作用,箭头指向chan,那就是放入,chan指出去 就是拿出来。


相信你应该记住了吧,反正我记住了


多路复用Channel


假设要从网上下载一个文件,启动了 5个 goroutine 进行下载,并把结果发送到 5 个 channel 中。其中,哪个先下载好,就会使用哪个 channel 的结果。


在这种情况下,如果我们尝试获取第一个 channel 的结果,程序就会被阻塞,无法获取剩下4个 channel 的结果,也无法判断哪个先下载好。这个时候就需要用到多路复用操作了,在 Go 语言中,通过 select 语句可以实现多路复用,其语句格式如下:


1select{
 2    case <-ch1:
 3        ...
 4    case data := <-ch2:
 5        ...
 6    case ch3<-data:
 7        ...
 8    default:
 9        默认操作
10}


整体结构和 switch 非常像,都有 case 和 default,只不过 select 的 case 是一个个可以操作的 channel。


小提示:多路复用可以简单地理解为,N 个 channel 中,任意一个 channel 有数据产生,select 都可以监听到,然后执行相应的分支,接收数据并处理。


使用select语句能提高代码的可读性。


  • 可处理一个或多个channel的发送/接收操作。


  • 如果多个case同时满足,select会随机选择一个。


  • 对于没有caseselect{}会一直等待,可用于阻塞main函数。


1// example select
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9func main() {
10
11    c1 := make(chan string)
12    c2 := make(chan string)
13
14    go func() {
15        time.Sleep(1 * time.Second)
16        c1 <- "one"
17    }()
18    go func() {
19        time.Sleep(2 * time.Second)
20        c2 <- "two"
21    }()
22
23    for i := 0; i < 2; i++ {
24        select {
25        case msg1 := <-c1:
26            fmt.Println("received", msg1)
27        case msg2 := <-c2:
28            fmt.Println("received", msg2)
29        }
30    }
31}


小结:关于数据流动、传递等情况的优先使用channle, 它是并发安全的,且性能优异


Sync深入并发控制


sync.waitGroup


在此之前我们先去,解决一个开启goroutine,提前退出的例子

示例代码如下


1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6    //"time"
 7)
 8
 9var wg sync.WaitGroup
10func main() {
11    for i := 0; i < 10 ; i++ {
12        go exampleOut(i)
13    }
14}
15
16func exampleOut(i int)  {
17    fmt.Println("Hello, Gopher, I am [Id]", i)
18}


640.jpg


仔细看,你会发现根本就没有输出,原因是它开启goroutine,也需要时间。main函数并会等待,当然我们也可以手动添加一个停止,但这个并不能有效的阻止(你我都知道需要多久才能把goroutine执行完成),那有没有办法。。。

答案当然是有滴,它就是sync.WaitGroup

目录
相关文章
|
6月前
|
Go
浅谈Golang并发控制WaitGroup
浅谈Golang并发控制WaitGroup
56 0
|
存储 安全 编译器
Golang 语言中 map 的键值类型选择,它是并发安全的吗?
Golang 语言中 map 的键值类型选择,它是并发安全的吗?
70 0
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
86 4
|
6月前
|
Go
深度探讨 Golang 中并发发送 HTTP 请求的最佳技术
深度探讨 Golang 中并发发送 HTTP 请求的最佳技术
122 4
|
6月前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
195 7
|
6月前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
167 7
|
6月前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
446 5
|
6月前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
153 2
|
6月前
|
设计模式 Go 调度
Golang深入浅出之-Go语言中的并发模式:Pipeline、Worker Pool等
【5月更文挑战第1天】Go语言并发模拟能力强大,Pipeline和Worker Pool是常用设计模式。Pipeline通过多阶段处理实现高效并行,常见问题包括数据竞争和死锁,可借助通道和`select`避免。Worker Pool控制并发数,防止资源消耗,需注意任务分配不均和goroutine泄露,使用缓冲通道和`sync.WaitGroup`解决。理解和实践这些模式是提升Go并发性能的关键。
79 2
|
Go
Golang 语言怎么控制并发 goroutine?
Golang 语言怎么控制并发 goroutine?
42 0