Featured image of post 【GO】并发编程

【GO】并发编程

文章共3127字

0x01 协程

什么是进程、线程、协程????????

概念

进程概念:

1
进程(Process)是操作系统中运⾏中的程序,它是程序执⾏的⼀个实例。当你运⾏⼀个应⽤程序时,⽐如打开浏览器、启动游戏,操作系统会为这个程序创建⼀个进程。进程包含了程序执⾏的所有必要信息,例如代码、数据、资源和内存。

线程概念:

1
线程是比进程更轻量的执⾏单元,是进程的⼀部分。⼀个进程可以包含多个线程,每个线程可以独⽴执⾏任务,但它们共享相同的内存空间和资源。

举例:

1
2
3
4
5
进程就像⼀个厨房,⾥⾯有完整的设备、材料和⻝谱,允许你做饭。⽽线程则是厨房⾥的厨师。⼀个厨房可以有多个厨师(线程),每个厨师可以同时进⾏不同的任务(切菜、炒菜、煮汤等),并且他们共享同样的厨房资源,⽐如锅、灶台和⻝材。
单线程
如果是单线程的状态下,⼀个厨师(线程)就需要处理⼀整个厨房的事情(进程),有⼀个⼤菜,厨师只能⼀个⼀个来弄,效率低下
多线程
⼀个厨房有多个厨师,他们的效率就会更⾼,每个厨师可以同时进⾏不同的任务,并且因为他们是在⼀个厨房⾥(进程),他们可以直接沟通,(共享的数据结构进⾏通信),⽽进程是独⽴的厨房,两个厨房(进程)之间的通信,相对于来说要复杂很多。

协程概念:

1
2
3
协程 Coroutines 是⼀种⽐线程更加轻量级的微线程。⼀个进程可以拥有多个线程,⼀个线程也可以拥有多个协程,因此协程⼜称微线程和纤程。
可以粗略的把协程理解成⼦程序调⽤,每个⼦程序都可以在⼀个单独的协程内执⾏。
Go协程⽐传统的操作系统线程更轻量级。它们的启动和销毁开销⾮常⼩,并且允许并发地执⾏代码,这意味着多个协程可以在同⼀时间段内运⾏,协程相当于是⼀个⼦程序,他属于⽤户态,也就是由语⾔来操作,线程与进程,都是属于内核态,由操作系统来操作,⽆法⼲预,但是协程,我们可以控制它。

调度器
线程调度

1
线程调度通常由操作系统的内核负责,是⼀种抢占式调度机制。抢占式调度意味着操作系统会强制地决定何时停⽌⼀个线程的执⾏,并切换到另⼀个线程。这种调度机制称为抢占式多任务,由于抢占式多任务,操作系统会根据线程的优先级、状态(如阻塞、就绪、运⾏)以及可⽤资源来决定哪个线程获得CPU时间⽚。每个线程在操作系统中都会分配⼀个时间⽚,当时间⽚⽤完时,操作系统会切换到另⼀个线程,在⼀个CPU时间⽚⽤完时,需要进⾏上下⽂切换,操作系统在线程之间切换时,需要保存和恢复线程的上下⽂(包括寄存器状态、程序计数器、内存⻚表等)。这种上下⽂切换发⽣在内核态和⽤户态之间,开销较⼤。但由于抢占式调度,操作系统会随时中断正在运⾏的线程,以便处理其他线程的任务,线程本身对调度过程没有控制权。

Go Goroutines 调度器

1
2
协程调度通常是由编程语⾔的运⾏时(⽤户态)来负责,在go中,由Goroutines负责,Goroutines 的调度由 Go 的运⾏时系统(runtime)管理,通过协作式调度和抢占式调度结
合的⽅式,来保证 Goroutines 在多核 CPU 上⾼效执⾏。

Go调度的优势
轻量性:

1
相⽐于传统的操作系统线程,Goroutines 是⾮常轻量的。操作系统线程通常需要消耗⼤量内存(每个线程的栈⼤⼩默认⼏乎占 1-2 MB),⽽ Goroutine 的初始栈⼤⼩只有 2 KB,并且可以按需动态增⻓,这使得在同样的资源下,Go 程序可以轻松创建成千上万的 Goroutines ⽽不会占⽤过多系统资源。

⾃动调度:

1
Goroutines 不需要程序员⼿动管理。Go 的调度器会根据负载和系统资源⾃动进⾏调度,使得程序员能够专注于业务逻辑,⽽⽆需担⼼如何调度 Goroutines。

多核优化:

1
Goroutines 的调度器充分利⽤了多核 CPU。Go 的运⾏时调度器通过逻辑处理器 P 和操作系统线程 M 的绑定机制,能够在多核 CPU 上⾃动并⾏执⾏ Goroutines,最⼤化性能。

协程的使用

协程创建

1
2
3
Golang中的并发是函数相互独的能⼒,
创建协程在任务函数前加上go关键字
go test()

示例代码:
先演示一下正常情况下的方法使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main  
  
import "fmt"  
  
func show(msg string) {  
    for i := 0; i < 5; i++ {  
       fmt.Println(msg)  
    }}  
func main() {  
    show("go")  
    show("java")  
}
输出正常打印5次go和5次java
go
go
go
go
go
java
java
java
java
java

创建协程的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main  
  
import (  
    "fmt"  
)  
  
func show(msg string) {  
    for i := 0; i < 5; i++ {  
       fmt.Println(msg)  
       //time.Sleep(100 * time.Millisecond) //每次循环,睡100毫秒  
    }  
}  
func main() {  
    go show("go") //协程  
    show("java")  //主线程  
}
输出
java
java
java
java
java
一般会挤进来一次go但次数很少

0x02 通道

概念

1
Go 提供了⼀种称为通道的机制,⽤于在 goroutine 之间共享数据。当 goroutine 执⾏并发活动时,需要在goroutine 之间共享资源或数据,通道充当 goroutine 之间的管道(管道)并提供⼀种机制来保证同步交换。
1
Channels 需要在声明通道时指定数据类型。我们可以共享内置、命名、结构和引⽤类型的值和指针。数据在通道上传递:在任何给定时间只有⼀个 goroutine 可以访问数据项:因此按照设计不会发⽣数据竞争。根据数据交换的⾏为,有两种类型的通道:⽆缓冲通道和缓冲通道。⽆缓冲通道⽤于执⾏goroutine 之间的同步通信,⽽缓冲通道⽤于执⾏异步通信。⽆缓冲通道保证在发送和接收发⽣的瞬间执⾏两个 goroutine 之间的交换。缓冲通道没有这样的保证。
1
协程之间⽆法直接通信,但可以使⽤通道来使协程之间互相通信通道分为有缓冲和⽆缓冲,有缓冲,容量设置后,可以异步读取数据,不会堵塞,⽆缓冲的话,放⼀个数据就必须取出来,否则就会阻塞。

创建⽆缓冲和缓冲通道

1
2
3
make(chan int) //整型⽆缓冲通道,写入和读取同步进行,容易死锁,通常用于同步执行

make(chan int ,10) //整型有缓冲通道,通常用于异步执行。

使⽤内置函数make创建⽆缓冲、缓冲通道,make第⼀个参数需要是关键字chan,也就是通道Channels的缩写,然后是通道交换的数据类型

使用

管道在左边,则说明是往管道中传输数据,管道在右边,则说明要取出数据

1
2
3
4
5
ch := make(chan int,5) //整型缓冲通道

ch <- 5 //通过管道发送值

data := <-ch

⽆缓冲区实例
阻塞+主协程结束

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main  
  
import (  
    "fmt"  
)  
  
func main() {  
    ch := make(chan int) //创建一个int类型的管道  
    go func() {          //创建一个协程  
       fmt.Println("准备向⽆缓冲通道发送数据")  
       ch <- 111 //往管道里添加一个111数据  
       fmt.Println("向⽆缓冲通道发送数据完毕")  
    }()    //time.Sleep(1 * time.Second)  
    fmt.Println("准备从⽆缓冲通道接收数据")  
    data := <-ch  
    fmt.Println("从⽆缓冲通道接收到数据:", data)  
}
输出
准备从缓冲通道接收数据
准备向缓冲通道发送数据
缓冲通道发送数据完毕
缓冲通道接收到数据 111

缓冲区实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main  
  
import (  
    "fmt"  
    "time")  
  
func main() {  
    ch := make(chan int, 1)  
    go func() {       fmt.Println("准备向⽆缓冲通道发送数据")  
       ch <- 111  
       fmt.Println("向⽆缓冲通道发送数据完毕")  
    }()    time.Sleep(10 * time.Second)  
    fmt.Println("准备从⽆缓冲通道接收数据")  
    data := <-ch  
    fmt.Println("从⽆缓冲通道接收到数据:\n", data)  
}
输出
准备向缓冲通道发送数据
缓冲通道发送数据完毕
准备从缓冲通道接收数据
缓冲通道接收到数据
 111

实例1
这个实例代码创建了⼀个通道 ch,在 main 函数中启动⼀个 goroutine 执⾏ Ion 函数,Ion 函数⽣成⼀个随机数并发送到通道,main 函数等待接收通道中的数据并打印,最后关闭通道。

 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
package main  
  
import (  
    "fmt"  
    "math/rand"    
    "time"
    )  
  
var ch = make(chan int)  
  
func Ion() {  
    rand.Seed(time.Now().UnixNano())  
    intn := rand.Intn(100)  
    fmt.Println("随机数为:", intn)  
    time.Sleep(5 * time.Second)  
    ch <- intn  
}  
func main() {  
    defer close(ch) //main关闭后,关闭通道  
    go Ion()  
    fmt.Println("Wait...")  
    data := <-ch  
    
    fmt.Println("值为:", data)  
    fmt.Println("Done")  
}
输出
Wait...
随机数为 76
值为 76
Done
站点已运行计算中...