go语言学习-并发编程(并发并行、线程协程、通道channel)
1、 概念
1.1 并发和并行
- 并发:具有处理多个任务的能力 (是一个处理器在处理任务),cpu处理不同的任务会有时间错位,比如有A B 两个任务,某一时间段内在处理A任务,这时A任务需要停止运行一段时间,那么会切换到处理B任务,B任务停止运行,在切换处理A任务,只不过CPU处理快,看起来是同时处理多个任务。
- 并行:同时执行多个任务的能力 (多个处理器) 。 比如3 个任务同时创建,cpu是3核的,那么3个处理器同时处理就是并行。
1.2 线程和协程
- 线程(Thread)是计算机操作系统中程序运行时最小的执行单元,,是CPU调度的基本单元。一个线程可以理解为一个任务,都具有一个独立的控制流。(python和java一般会使用,一个线程占用内存8MB)
在多线程编程中,可以将不同的任务或操作封装在不同的线程中,然后并发地执行这些任务,从而提高程序的性能和响应速度。 - 协程(Coroutine)是一种轻量级的线程,也被称为用户态线程。与操作系统中的线程不同,协程由程序员自己管理调度。协程可以使用更少的资源,更快的创建和销毁,协程之间可以通过通道 (Channel) 进行通信和同步,避免了传统线程中锁等同步机制的开销和复杂性。(go使用的是协程,一个协程占用内存2KB)
2、go实现并发处理(GoRoutine)
GoRoutine实现: 只需要语句前添加go关键字即可。
注意: 主程序创建了协程以后,并不会等待所有的协程执行成功,需要主程序等待协程处理完成之后再去退出主程序
package main
import (
"fmt"
"time"
)
// 做包子的函数
func makeBuns(filling string) {
startTime := time.Now()
fmt.Printf("%s馅开始的时间:%s", filling, startTime)
fmt.Printf("开始做%s馅的包子\n", filling)
// 1. 剁馅
fmt.Printf("开始剁%s馅...\n", filling)
// 2. 擀皮
fmt.Println("开始擀皮...")
time.Sleep(time.Second)
// 3. 包包子
fmt.Printf("开始包%s馅的包子...\n", filling)
// 4. 蒸
fmt.Printf("开始蒸%s馅的包子...\n", filling)
cost := time.Since(startTime)
fmt.Printf("%s馅共耗费的时间:%s", filling, cost)
}
func main() {
// go: goroutine并发处理,go 代码语句
// 协程:2KB,线程:8MB
// 不需要关心协程的底层逻辑、内存管理以及垃圾回收
fillings := []string{"韭菜", "鸡蛋", "西葫芦"}
for _, v := range fillings {
// 包
go makeBuns(v) // 它的时间会缩短吗?
// 主程序创建了协程以后,并不会等待所有的协程执行成功,需要主程序等待协程处理完成之后再去退出主程序
}
time.Sleep(time.Second * 3) // 开发程序时不能用time.Sleep去等待!!!!
}
通过运行函数可以发现基本是同一时间执行的。
3、通道
3.1 基本语法
// go的通道,也是一种类型。channel1 := make(chan 类型)就可以声明一个通道。这个通道可以理解为类似于共享文件的存储。
//如果通道内没有数据,就会一直在此处阻塞,直到取到数据为止
//通道打开之后需要通过closer关闭
buns := make(chan string) //该通道只有一个位置
buns := make(chan string,5) //该通道有五个位置
channel <- "数据1" //发送数据
data := <- channel //取数据
defer close(buns) //等程序执行完后,关闭通道
3.2 阻塞原理
buns := make(chan string)
//1、此时的通道默认只有一个位置,比如传入了值,那么下一个值传需要等待这个值被拿走,才可以有另外的一个数据进入。
//2、如果这个通道里面没有数据,也会被阻塞,等待有数据后,才可以继续往下走流程。
3.3 实践
package main
import (
"fmt"
"time"
)
// 做包子的函数
func makeBuns(filling string, buns chan string) {
fmt.Printf("开始做%s馅的包子\n", filling)
// 1. 剁馅
fmt.Printf("开始剁%s馅...\n", filling)
// 2. 擀皮
fmt.Println("开始擀皮...")
time.Sleep(time.Second)
// 3. 包包子
fmt.Printf("开始包%s馅的包子...\n", filling)
// 4. 蒸
fmt.Printf("开始蒸%s馅的包子...\n", filling)
// 5. 蒸好了
time.Sleep(time.Second * 1)
fmt.Printf("%s馅包子已经蒸好了,可以上菜了,蒸好的时间是:%s\n", filling, time.Now())
// 6. 上菜
// 在这个位置把蒸好的包子放到通道内
buns <- filling
fmt.Printf("%s馅包子已经放在了上菜区,放置时间是:%s\n", filling, time.Now())
}
// 上菜的函数
// func waiter(buns chan string) {
// // 把蒸好的包子拿出来去上菜
// bun := <-buns
// fmt.Printf("上菜:%s, 上菜时间:%s\n", bun, time.Now())
// }
func main() {
// go的通道,也是一种类型。channel1 := make(chan 类型)就可以声明一个通道
// 发送数据: channel1 <- "数据1"
// 取数据: data1 := <- channel1
// 定义一个蒸好的包子的通道
buns := make(chan string, 5)
defer close(buns) //通道打开之后需要关闭
// go: goroutine并发处理,go 代码语句
// 协程:2KB,线程:8MB
// 不需要关心协程的底层逻辑、内存管理以及垃圾回收
// 做包子
//
fillings := []string{"韭菜", "鸡蛋", "西葫芦"}
startTime := time.Now()
for _, v := range fillings {
// 包
go makeBuns(v, buns)
// 主程序创建了协程以后,并不会等待所有的协程执行成功,需要主程序等待协程处理完成之后再去退出主程序
}
//取包子
for i := 0; i < len(fillings); i++ {
// 在这里直接去包子
// 把蒸好的包子拿出来去上菜
time.Sleep(3 * time.Second)
bun := <-buns // 如果通道内没有数据,就会一直在此处阻塞,直到取到数据为止
fmt.Printf("上菜:%s, 上菜时间:%s\n", bun, time.Now())
}
cost := time.Since(startTime)
fmt.Println("共耗费的时间:", cost)
// time.Sleep(time.Second * 3) // 开发程序时不能用time.Sleep去等待!!!!
}
3.4 Select处理多个channel通道
比如一个饭店多个菜系的师傅,然后相关菜系会放到对应的存放位置,然后服务员到对一个的位置取菜。
//select 基础语法
select {
case dish := <-chef1:
fmt.Println("厨师chef1已经做好了:", dish)
case dish := <-chef2:
fmt.Println("厨师chef2已经做好了:", dish)
case <-time.After(time.Second * 3): //对通道进行超市时间处理时候常用time.After
fmt.Println("你们做饭太慢了,我不吃了,拜拜")
}
// select包含多个case语句,每个case语句用于接收某一个通道的数据,当某一个通道有了数据之后,就会执行对应的case语句。多个case语句,同时都包含了数据,不确定,会随机执行某一个
//练习
package main
import (
"fmt"
"time"
)
// 做菜的函数
func cookDish(chef, dishName string, c chan string) {
fmt.Printf("厨师:%s正在做:%s\n", chef, dishName)
time.Sleep(time.Second * 5)
// 做好的菜放进通道内
c <- dishName
}
func main() {
// 定义两个channel去存放不同的数据
chef1 := make(chan string)
chef2 := make(chan string)
go cookDish("chef1", "烤鸭", chef1)
go cookDish("chef2", "佛跳墙", chef2)
// 等待获取数据
// select包含多个case语句,每个case语句用于接收某一个通道的数据
// 当某一个通道有了数据之后,就会执行对应的case语句
// 多个case语句,同时都包含了数据,不确定,会随机执行某一个
select {
case dish := <-chef1:
fmt.Println("厨师chef1已经做好了:", dish)
case dish := <-chef2:
fmt.Println("厨师chef2已经做好了:", dish)
case <-time.After(time.Second * 3):
fmt.Println("你们做饭太慢了,我不吃了,拜拜")
}
close(chef1)
close(chef2)
}
3.5 监听通道的退出信号
//通过定义Bool类型的通道,可以快速监听退出信号或者监听错误信息
package main
import (
"fmt"
"time"
)
//定义拧螺丝的函数
func screw(c chan int) {
i := 1
for {
fmt.Printf("正在拧第%d个螺丝\n", i)
c <- i
i++
time.Sleep(time.Second)
}
}
func main() {
// 定义一个拧螺丝的通道
screwChan := make(chan int, 100)
// 定义一个关闭通道的通道
stop := make(chan bool)
go screw(screwChan)
go func() {
time.Sleep(10 * time.Second)
fmt.Println("下班了,走啦,不干了")
stop <- true
}()
for {
select {
case <-stop:
// 说明我们的倒计时已经到了,并且往stop这个通道内发送了true的数据
fmt.Println("下班了,不弄了")
return
case s := <-screwChan:
fmt.Printf("第%d个螺丝已完成\n", s)
}
}
}