【Golang】go语言之并发编程基础(goroutine、channel、SELECT)

并发编程

go中的串行和并行还有并发的概念

  • 串行(Sequential):
    • 串行是一种执行方式,串联行走,按照顺序逐一的执行任务或者操作,
    • 在串行执行中,每个任务必须等待前一个任务执行完了之后才能开始执行
    • 场景:串行执行通常用于单核或者单线程环境,其中一次只能执行一个任务,意味着任务要找现行顺序执行,一个接一个,直到所有的任务都完成
    • 串行执行通常具有可预测行,因为任务的执行顺序是确定的,
    • 缺点就是在多核系统中,无法充分的利用硬件资源
  • 并行(Parallel):
    • 并行也是一种执行方式
    • 多个任务或者操作可以同时的执行,不必等待前一个任务完成,
    • 充分利用多核处理器或者多线程环境的优势
  • 并发(Concurrency):
    • 并发是一个更广泛的概念,在同一时间段内处理多个任务,但不一定要求同时执行。在并发中,任务可以交替执行,每个任务都有自己的执行周期,并发通常涉及的是多个独立的执行线程、进程或者协程。
    • 并发不一定需要多核处理器,它可以在单核处理器上模拟通过快速切换执行线程来实现。
    • 并发通常用于提高系统的吞吐量、资源利用率和响应性,特别是IO密集型应用中。
    • 并发任务之间可能需要协调、同步和共享数据,因此需要小心处理并发问题,如竞争条件和死锁等
  • 总结:
    • 串行是指按顺序执行任务的方法,不涉及多个任务之间的交替执行
    • 并行是多个任务同时执行的方式,通常需要多核处理器或多线程环境
    • 并发是多个任务在同一时间段内处理的方式,可以是交替执行,通常涉及多线程或协程、并需要处理并发相关问题。

【拓展】并发模型

  • 主流并发模型无外乎三种
    • 1、多线程:每个线程一次处理一个请求,线程越多可并发处理的请求数就越多,
      • 但是在高并发下,多线程的开销会比较大
    • 2、协程:无需抢占式的调度,开销小,可以有效的提高线程的并发性,从而避免了线程的缺点那部分
    • 3、基于异步回调的IO模型:异步编程,当遇到密集IO的时候,等待,让其他程序继续跑

goroutine的基本概念

  • 概念:是一种轻量级的线程,用于执行程序并发任务,与传统线程相比,goroutines更加轻量且消耗更少,

routine:常规;例行程序;日常工作

与创建线程相比,创建成本和开销都很小,每个goroutine的堆栈只有几kb,并且堆栈可根据程序的需要增长和缩小(线程的堆栈是需要指明和固定的),所以go语言从语言层面就支持高并发。

  • 并发执行:多个goroutines可以同时运行,且不需要显式的线程管理,有助于充分利用多核处理器,提高程序的性能
  • 轻量级:goroutines比传统线程更轻量。这个轻量可以通俗理解为创建和销毁他们的成本很低,通常数百上千的goroutines可以在同一个程序中运行而不会引发性能问题
  • 并发通信:goruntines之间的可以通过通道(channel)进行安全的并发通信,通道是goroutines之间交换数据的一种机制,避免了竞争条件和协调任务
  • 并发模型:Go 语言的并发模型是基于 CSP(Communicating Sequential Processes)的,它强调通过通信来共享数据,而不是共享数据来通信。这种模型使并发编程更加安全和可维护。
  • 如何启动 goroutine:要启动一个goroutine,只需要在函数或方法调用前加上关键字’go’ 即可创建一个新的goroutine来执行该函数,程序继续执行后续任务,而不会等待goroutine完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
// 启动一个 Goroutine 执行 hello 函数
go hello()

// 主线程继续执行其他任务
fmt.Println("Main function")

// 等待一段时间以确保 Goroutine 有足够的时间执行
time.Sleep(time.Second)
}

func hello() {
fmt.Println("Hello, Goroutine!")
}

  • 程序执行的背后:当程序启动的时候,只有一个goroutine来调用main函数,可以理解为主goroutine,新的goroutine通过go语句进行创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"time"
)

func DelayPrint() {
for i := 1; i <= 4; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Println(i)
}
}

func HelloWorld() {
fmt.Println("Hello world goroutine")
}

func main() {
go DelayPrint() // 开启第一个goroutine
go HelloWorld() // 开启第二个goroutine
time.Sleep(2*time.Second)
fmt.Println("main function")
}

  • tips:DelayPrint里面的sleep ,会导致第二个goroutine阻塞或者等待吗?
    • 答案肯定是不会
  • 当程序执行go func()的时候,只是简单的调用然后就立即返回了,并不关心函数内部发生的事情,所以不同的goroutine直接不影响,main会继续按顺序执行语句,所以两个go rountine同时在跑,但是肯定是第一个gorounine先执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"time"
)

func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("hello")
fmt.Printf("这是sayHello函数内打印的第%v次\n", i)
time.Sleep(time.Millisecond * 100) // 暂停等待100毫秒
}
}
func sayWorld() {
for i := 0; i < 5; i++ {
fmt.Println("world")
fmt.Printf("这是sayWorld函数内打印的第%v次\n", i)
time.Sleep(time.Millisecond * 100) // 暂停等待100毫秒

}
}

func main() {
// sayHello()
go sayHello() //在函数前面加一个go关键字就将这个函数单独的放在一个Goroutine中执行,与主Goroutine并行执行
go sayWorld()
fmt.Println("这句话先走还是后走")
for i := 0; i < 5; i++ {
fmt.Println("yes")
fmt.Printf("这是main函数内打印的第%v次\n", i)
time.Sleep(time.Millisecond * 100) // 暂停等待100毫秒
}

// time.Sleep(time.Second)
}
/*
输出内容如下:
这句话先走还是后走
yes
这是main函数内打印的第0次
world
这是sayWorld函数内打印的第0次
hello
这是sayHello函数内打印的第0次
*/
  • 这里从输出内容看得出来,当程序碰到go func()的时候,并不管go func()的内容,直接就去执行后面的代码了

通道(channel)

概念:

  • 通道是什么:是一种数据结构,所以通道可以用var 来声明数据的类型的
  • 干什么?:通道是一种在goroutines之间传递的数据结构,它类似于一个队列,同于在通道与通道之间发送和接收数据的
  • 通道的类型:道中传递的数据必须与通道的类型匹配。通道类型使用 chan 关键字,如 chan int 表示一个整数类型的通道。
  • 发送和接收:通道的基本操作有发送(send)和接收(receive)。通过通道发送数据时,数据会被发送到通道,然后可以在另一个 Goroutine 中接收。
  • 阻塞:当发送或接收操作发生时,它们可能会阻塞当前 Goroutine,直到有另一个 Goroutine 准备好接收或发送数据。这有助于同步不同 Goroutines 之间的操作。
    • 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止

强调一下:

  • 通道是在传递数据,而不是在赋值数据,当通道A 体内的数据,传给了B的时候,A就没有数据了,就空了,B就接收到了B就有了

接收操作(<-channel)、发送操作(channel <- data)或关闭操作(close(channel))。

声明:

1
2
3
4
5
6
7
8
9
10
11
var ch chan int      // 声明一个传递int类型的channel
ch := make(chan int) // 使用内置函数make()定义一个channel

ch <- value // 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据
value := <-ch // 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止
//=========
close(ch) // 关闭channel
c := make(chan int)
fmt.Println(len(c)) // 通道内实际使用长度,
fmt.Println(cap(c)) // 通道的容积长度,

类型:通道理论上来说可以分2种

  • 无缓冲通道和缓冲通道

  • 无缓冲通道:无缓冲通道上的发送操作将会被阻塞,直到另一个goroutine在对应的通道上执行接收操作,此时值才传送完成,两个Goroutine都继续执行(发送时阻塞,直到接收才会畅通继续)

    • 定义的时候,不给大小就是一个无缓冲通道了,ch:=make(chan int) int后面不设容量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var done chan bool //:这里声明了一个名为 done 的布尔类型通道 用来进行通道之间数据通讯

func HelloWorld() {
fmt.Println(" hello world channel") //打印,这里注意下,下面有用
time.Sleep(time.Millisecond * 500) // 等待500ms
done <- true // 这里是向done这个channel 发送数据,发送的内容是 true 因为他是一个bool类型的channel 注意,发送数据的时候将会阻塞
}

// func main() {
// done = make(chan bool) // 创建一个bool类型的channel 名字叫done 这个通道将用于等待helloWorld函数的完成 分配内存了
// go HelloWorld() //启动一个新的goroutine 用来执行helloWorld函数
// <-done // 接收操作,它从 done 通道接收数据
// 这个操作会阻塞,直到 HelloWorld 函数发送数据到 done 通道。一旦数据到达,程序将继续执行,然后退出
// 如果没有发送和接收通道的操作,那么主goroutine函数main将不会等待goroutine函数helloWorld直接完成就会直接退出mian结束
// }

func main() {
ch := make(chan int) // make就是创建了一个channel类型没有容量(无缓冲)类型的变量名称为ch 后面没设置缓冲通道的大小,就是无缓冲 分配内存了
go func(ch chan int) { // func匿名函数,go是表示他是goroutine 入参是无缓冲channel类型,
fmt.Println(<-ch) // 使用<-channel来接收channel类型 作用是冲通道ch接收数据,并且答应出来,接收到的是多少就打印多少
}(ch)
ch <- 10 //主函数main将整数10发送到ch通道中,使用<- 用来发送和接收 <-左边是channel就代表想ch发送,<-右边是ch就代表在接收channel
/*
总体来说,这段代码的主要目的是创建一个通道 ch,然后启动一个协程,
该协程从通道 ch 中接收数据并将其打印到标准输出。
同时,主函数将整数值 10 发送到通道 ch 中。由于通道是无缓冲的,
这个发送操作会导致协程解除阻塞,接收并打印值 10。因此,你会在标准输出中看到 "10"。这个示例展示了 Go 语言中的并发和通道的基本使用。
*/
}
  • 通道可以用来连接goroutine,这样一个的输出是另一个输入。这就叫做管道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var echo chan string
var receive chan string

func Echo() {
time.Sleep(time.Microsecond * 500)
echo <- "咖啡色的羊驼" // 这里在向echo这个通道发送数据
}

func Receive() {
temp := <-echo // 接收echo通道传过来的数据 这里会阻塞,等待数据传输结束后,
receive <- temp // 将temp接收过来的通道数据,传给receive,
}

func main() {
echo = make(chan string)
receive = make(chan string)

go Echo()
go Receive()
getStrt := <-receive // 接收 receive 通道传过来的数据
fmt.Println(getStrt)
}
// 在这里不一定要去关闭channel,因为底层的垃圾回收机制会根据它是否可以访问来决定是否自动回收它。(这里不是根据channel是否关闭来决定的)
  • 单向通道:
    • 单向就是指限制一头通信,比如限制仅接收,或者限制仅发送数据
    • 双向通道可以修改为单向通道,反之不行
    • 当程序则够复杂的时候,为了代码可读性更高,拆分成一个一个的小函数是需要的。

goroutine的通道默认是阻塞的,那么有什么办法可以去缓解阻塞呢:?

答案:加一个缓冲区

  • 缓冲通道: 一个有容量的通道,可以定义他的容积大小。
1
2
3
4
5
6
ch := make(chan string, 3) // 创建了缓冲区为3的通道

//=========
len(ch) // 长度计算
cap(ch) // 容量计算

channel

  • 当他体内的容积被塞满后,就会阻塞 ,就会死锁
  • 必须要让通道体内元素个数不大于其设定的容积大小,不然就会报错

通道有点类似于python的锁一样,谁拿到这个锁,谁就可以操作数据,通道就是谁进去了这个通道,谁就可以操作这个通道内的数据,当这个通道内塞满了的时候,就关门了,进不去了。

  • 对通道循环取值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对channel循环取值

func main() {
ch1 := make(chan int, 100) // 给ch1这个变量分配了内存(实例化了) 是一个chan,里面的内容是int类型,100个空间
time.Sleep(time.Millisecond * 100)
for i := 1; i < 100; i++ {
ch1 <- i // 往这个ch1里面塞数据,把i塞进去了
}
close(ch1) // 注意要关闭通道
// 方式 1

// for {
// a, err := <-ch1
// if !err {
// return
// }
// fmt.Println(a)
// }
// 方式 2
for a := range ch1 {
fmt.Println(a)
}

}

goroutine 阻塞死锁和友好退出

锁:

锁用来控制并发访问共享资源的一种同步机制避免多个goroutine同事访问和修改相同的数据,从而导致数据竞争或者不一致的状态,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

// 锁的概念与理解
// 先创建一个阻塞
//
// func main() {
// ch1 := make(chan int) // 创建了一个通道ch1 它是个无缓冲通道,容积是0,体内目前数据也是0
// <-ch1 //把ch1体内的数据给出去,但是这个时候ch1体内是没数据的,就阻塞(deadlock死锁)了,这个时候阻塞的main函数goroutine ,通道被锁了
// // 提示:fatal error: all goroutines are asleep - deadlock!所有的goroutine都睡眠阻塞了,
// }
// func main() {
// ch1, ch2 := make(chan int), make(chan int) // 创建了两个通道,无缓冲的
// go func() {
// ch1 <- 1 // 往ch1里面塞了个1
// ch2 <- 0 // ch2 里面也塞了个0

// }() //后面加括号是表示返回值

// <-ch2 // 把ch2的数据取走,
// // 这样运行会阻塞(deadlock死锁),因为往ch1里面塞了一个数据,
// // 但是ch1是个无缓冲的通道,现在他的体内被塞了一个数据,如果不把这个数据取走,就会阻塞,
// // 反之,ch2的数据被取走了,他体内的数据回到了定义的容积0个,所以它没有阻塞,

// }

// 接下来解锁思路 方式一,把体内数据给出去,让体内数据回到之前定义的大小
// func main() {
// ch1, ch2 := make(chan int), make(chan int) // 创建了两个通道,无缓冲的
// go func() {
// ch1 <- 1 // 往ch1里面塞了个1
// ch2 <- 0 // ch2 里面也塞了个0

// }() //后面加括号是表示返回值
// <-ch1 // 解锁思路,把ch1的数据也取走给出去,ch1就回到了体内数据为0的状态,这个时候就没阻塞了
// <-ch2 // 把ch2的数据取走,
// // 这样运行会阻塞,因为往ch1里面塞了一个数据,
// // 但是ch1是个无缓冲的通道,现在他的体内被塞了一个数据,如果不把这个数据取走,就会阻塞,
// // 反之,ch2的数据被取走了,他体内的数据回到了定义的容积0个,所以它没有阻塞,

// }

// 接下来解锁思路 方式二,把体内容积扩容,
func main() {
ch1, ch2 := make(chan int, 1), make(chan int) // 创建了两个通道,无缓冲的
go func() {
ch1 <- 1 // 往ch1里面塞了个1 ,这个时候ch1体内数据1小于其容积2,所以不会阻塞
ch2 <- 0 // ch2 里面也塞了个0,这个时候ch2体内数据1大于其容积1,如果不把体内数据给出去就会阻塞

}() //后面加括号是表示返回值
// <-ch1 // 解锁思路,把ch1的数据也取走给出去,ch1就回到了体内数据为0的状态,这个时候就没阻塞了
<-ch2 // 把ch2的数据取走, 这个时候ch2的体内数据回到0,等于其容积,所以不会阻塞
// 这样运行会阻塞,因为往ch1里面塞了一个数据,
// 但是ch1是个无缓冲的通道,现在他的体内被塞了一个数据,如果不把这个数据取走,就会阻塞,
// 反之,ch2的数据被取走了,他体内的数据回到了定义的容积0个,所以它没有阻塞,

}

SELECT + CASE 多路复用

  • select语句用于处理并发操作中的多个通道操作,它可以让你同事等待多个通道,并在其中任意一个通道就绪时执行对象的操作
  • 与switch case语句类似,他有一系列的case分支和一个默认的分支,
  • 每个分支case都会对应一个通道的通信(发送或者接收)过程,select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句,
  • 如果多个case同时满足,select会随机选择一个去运行,
  • 如果没有满足的case,则一直等待直到最后执行default分支,
  • 如果没有任何通道就绪,且没有default子句,则sselect语句会阻塞,直到至少有一个通道就绪。
  • select应用场景一:多路复用,通讯,同时监听多个通道,一旦某个通道可以进行读写操作,对应的case语句就会被执行。这种方式可以有效实现多个并发任务之间的协调和同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 100)
// ch1 <- 1
// select {
// case a := <-ch1:
// fmt.Println("走了第一条,如果ch1里面有元素发送给a的情况下,才会走这条路", a)
// case ch1 <- 5:
// fmt.Println("走了第二条,往ch1里面发送一个元素,如果ch1有位置塞数据的话,就会走这条路")
// default:
// fmt.Println("这是最后一条,如果以上都不满足,就会走这里来。")
// }
ch2 <- 1
select {
case a := <-ch1: // 把ch1里的数据发送给a,如果ch1里有数据的话,如果没有就不会触发这条判断
fmt.Println("第一条", a)
case ch1 <- 19: // ch1接收发送给他的数据19 , 如果ch1里面还有空位的话,就会触发这一条判断
fmt.Println("走了第二条")
case ch2 <- 20: // ch2接收发送给他的数据20,如果ch2里面还有空位的话,就会触发这一条判断
fmt.Println("走了第三条")
case b := <-ch2: // 将ch2里面的数据发送出来给到b,如果ch2里面有数据化,就会触发这一条
fmt.Println("走了第四条", b)
default: // 注意下,如果上面的通道都无法满足,且没有定义default,那么select就会阻塞 报错select case must be receive, send or assign recv
fmt.Println("都走不通,就会走这条")
}
// 上面这段代码判断里面,第二条第三条第四条都会满足条件,所以会在这三条判断里面随机走一条
// 如果没有运行case,就会阻塞事件发送报错(死锁)
// fatal error: all goroutines are asleep - deadlock!

}


  • select 应用的场景二:超时处理,结合selcet和time.After函数,事件对某个操作的超时控制,当某个操作超过一定时间没有完成时,可以执行响应的超时处理逻辑,一下两断代码一个基础版一个简单版的,:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 我们开解析下这段代码
func main() {
timeOut := make(chan bool, 1) // 定义timeOut为chan,内部类型为bool,容积为1,且分配内存(创建timeOut这个通道)
go func() { // func(){代码块}()创建匿名函数
time.Sleep(time.Second * 1) //sleep睡1秒钟
timeOut <- true // 往timeOut这个通道发送一个数据,数据是ture (bool)
}()
ch1 := make(chan int) // / 定义timeOut为chan,内部类型为int,容积为0,无缓冲通道,且分配内存(创建timeOut这个通道)
select { //select case
case <-ch1: // 第一个case 将ch1通道里面的数据发出来,但是ch1里面是空的,所以没东西发出来所以不会走这条case
case <-timeOut: // 第二个case,把timeOut通道里面的数据发出来,因为timeOut里面有个ture这个数据,所以满足,会走这条case
fmt.Println("超时了,走了这一步") //
}
}
  • 下面这个是常用的简化版的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 简化写一个通俗版,这段代码原理和上面类型,但有个小技巧,我们解读一下
func main() {
ch1 := make(chan int, 1) // 创建ch1通道,容积为0
ch1 <- 8
select {
case <-ch1: // 从ch1通道读取数据(把ch1的数据发送出来),ch1为空,不满足该判断
fmt.Println("没有超时就会走这条路")
case <-time.After(time.Second * 1): // 这里会有疑问的:因为这里的函数time.After()的返回值就是一个有数据的通道,所以满足该case判断
// 翻看源码就知道:func After(d Duration) <-chan Time {return NewTimer(d).C }返回的是一个chan类型 Time
fmt.Println("超时1秒且走了这条路")
// 这里要注意一下,time.After必须要等待时间结束后才会返回一个chan,如果在等待时间内,有其他case满足了,就会去运行其他case.

}
// ch1 <- 8 // 我们试一下往这个ch1里塞一个数据,让他满足第一个case的判断。在Second*1的时间内。满足了第一个case,所以不会运行打印第二个case的内容
}
  • select 场景三:非阻塞通信,通过定义default语句,实现非阻塞的通信操作,当没有任何通信操作可以立即进行时,default语句会被执行,可以写一下默认逻辑。(判断channel是否阻塞(或者说channel是否已经满了))
1
2
3
4
5
6
7
8
9
func main() {
ch := make (chan int, 1) // 注意这里给的容量是1
ch <- 1
select {
case ch <- 2:
default: // 走到这里说明select case阻塞了,因为通道满了
fmt.Println("通道channel已经满啦,塞不下东西了!")
}
}
  • slelect场景四:.退出机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 解读下这段代码
func main() {
i := 0 // 初始化i 值为1
ch1 := make(chan string, 0) // 创建一个通道,内容为字符串,容积为0
defer func() { // defer main函数结束的时候执行这个方法
close(ch1) // 关闭通道
}()

go func() { // goroutine执行匿名方法
DONE: // DONE:只是在打标签,标记一下循环开始了,没有实际作用
for {
time.Sleep(time.Second * 1) // 暂停一秒
// fmt.Println(time.Now().Unix()) // 打印Unix时间戳
fmt.Println(time.Now().UTC()) // 打印UTC国际标准时间
i++ // 循环一次 加等与1

select { // 开启select case
case m := <-ch1: // 将ch1里面的数据给m变量,但是ch1里是空的,所以不会触发这个
fmt.Println("打印ch1里面的内容 :", m) // 打印接收通道数据的m,就是下面的stop会被塞进来
// 这里的DONE也是一个意思标记一下循环从这里结束,收尾呼应
break DONE // 当这个通道里面有内容时,结束这个无限循环
default: // 没有通道满足case,触发default 打印下面的话
fmt.Println("以上买满足的条件")
}
}
}()
time.Sleep(time.Second * 5) // 会一直等5秒,但是等待的时候,go func匿名函数里面的死循环会一直去循环
ch1 <- "stop,塞进通道里面去" // 五秒结束,往 ch1通道里面塞了一个数据 "stop" , ch1里面塞数据的时候,等待到无限循环里面就会被终止的
fmt.Println("看一下这段代码在什么时候执行") // 这个是在循环结束后才会执行
}
  • 强调一下: 要跳出循环,一定要用break+ 具体标记,或者goto 标记也可以,否则其实不是真的退出,因为在goroutine里面,会不停地跑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()

go func() {

for {
time.Sleep(1*time.Second)
fmt.Println(time.Now().Unix())
i++

select {
case m := <-ch:
println(m)
goto DONE // 跳出 select 和 for 循环
default:
}
}
DONE:
}()

time.Sleep(time.Second * 4)
ch<-"stop"
}

锁是一种同步机制,用于控制对共享资源的访问,确保一次只有一个goroutine可以反问共享资源。

锁有两种状态:锁定和解锁,一旦有一个goroutine获得了锁,那其他goroutines将被阻塞。直到锁被释放

锁:互斥锁、读写锁

锁(互斥锁)简单解析

互斥锁:在并发执行时,多个goroutine同事读写一个数据,就会造成数据的读写混乱,

解决方式:加锁,加互斥锁,

方式:控制对共享资源的访问,让它可以却道在任何给定的时刻都只有一个线程或者说goroutine能够访问到被保护的临界区。

每一个

channel通道是解决协程同步,锁是解决协程(线程)访问资源优先性,

使用互斥锁时一定要注意,对资源(文件、数据等)操作完成

弊端:加了互斥锁之后,并发就变成了串行了,或者说,走到此处时,是串行,牺牲了效率,但是保证了数据安全性

互斥锁等待组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var x int             // 定义了一个整形变量
var wg sync.WaitGroup // 定义了一个等待组WaitGroup
var lock sync.Mutex // 定义了一个Mutex互斥锁

func add() {
for i := 0; i < 1000; i++ {
lock.Lock() // 开启锁
fmt.Println("x开始前为=", x)
x++
fmt.Println("x此时为=", x)
lock.Unlock() // 关闭锁
}
wg.Done() // 通知等待组wg完成了
}
func main() {
fmt.Println("x=", x)
wg.Add(2) // 等待组添加了2个 就是把两个goroutine放进等待组里面,他们两个在抢锁
// 两个add方法都在同时运行,但是只有一把锁,谁拿到了锁就是谁在执行x++的这个操作
go add()
go add()
// go add()
wg.Wait() // 等待组进入等待状态,等这两个goroutines完成任务
fmt.Println(x)
}
// 输出为2000

读写锁

  • 场景:读多写少,读是不需要加锁的,写需要加锁
  • 解决:读写互斥锁
1
2
- 当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁,就会等待;
- 当一个`goroutine`获取写锁之后,其他的`goroutine`无论是获取读锁还是写锁都会等待

sync.Map 并发安全映射(Map)

  • Go内置的map 并不是并发安全的,所以高并发下使用sync.Map类型
  • sync.Map不需要使用make()分配内存,使用另一种便捷方式:Store/Load等

1、并发安全性:sync.Map 在多个goroutines之间提供了并发安全的读取和写入操作,这意味着你可以同时在多个goroutines中访问和修改同一个映射,而不需要额外的锁或者同步机制去限制

2、自动扩容:sync.map在需要时会自动扩容以适应更多的键值对,无需手动管理容量问题

3、性能优化:sync.map在内部使用了一些性能优化策略,以提高并发访问的性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main() {
// // 写入键值对
// m.Store("key1", "value1")
// m.Store("key2", "value2")

// // 读取键的值
// value, found := m.Load("key1")
// if found {
// fmt.Println("Value:", value)
// }

// // 删除键值对
// // m.Delete("key2")

for i := 0; i < 10; i++ { // 循环10次,10个goroutine
wg3.Add(1) // 添加等待组,添加1个,循环了i遍
go func(n int) {
key := strconv.Itoa(n) //将n转化成字符串,用key接收,Itoa转换
m.Store(key, n) // 组装一个sync.Map 键是kyc字符串,值是n,所以就是 "1":1这样的形式
//读取sync,Mao的内容,通过Key拿值,
// 下划线表示不处理那个返回值,Load会返回两个值,前面的是kyc对应的value,后面的是一个布尔值,有就真,无就假
value, _ := m.Load(key)
fmt.Println(value) //打印的是key 的值
wg3.Done() // 结束这个等待组

}(i)
wg3.Wait() // 等待所有等待组完成
}
}


【Golang】go语言之并发编程基础(goroutine、channel、SELECT)
http://example.com/2023/12/07/802go语言并发编程基础(goroutine、channel、SELECT)/
作者
Wangxiaowang
发布于
2023年12月7日
许可协议