我们从调度上声明线程与goroutine的区别
为什么标题为goroutine,而不是协程,线程,其他语言之类,因为对于多线程编程来说,golang在此处是天生的高级语言
- cpu已经通过分配时间,自带调度器实现切换时间片帮我们解决了多程序(任务)执行问题,在此基础上推演出更小单位多线程: 多线程的执行依赖os(操作系统)的调度分配,操作系统促使硬件调度时钟,隔个一段时间发送一个信号到cpu中,cpu结束当前执行线程的函数(程序)并将执行信息从寄存器保存到内存中,再查看线程清单中接下来要继续执行的线程(执行过程:内存中取出来发到寄存器(上下文切换)),并执行。以这种方式不断进行线程切换执行/恢复/切换/销毁 (1.操作系统本身也会增加cpu负载,再加上硬件调度,属于三方调度,降低效率 2.cpu-寄存器速度大概是内存的100倍,如此切换执行,费时)
- 上面是大众的多线程运行方式,我们来看一下goroutine的运行原理,go是一种类似于c系语言编程,众多的包,编译的速度,二进制的直接执行文件,使得go开发效率极高。go运行于goroutine中,cpu运行时,本身自带线程调度器(程序调度器),使用一个go关键字即可实现多线程运行,但是并不是对应一个物理线程,假设调度器中有两个参数 A:B ,代码中goroutine数量为 A,物理线程为 B,(B的数量可配置,最好根据cpu核数来定义,此处不深究),动态自动的分配,销毁,降低了开发成本(锁的概念见下文),在goroutine中通过异步/阻塞来挂起线程,降低了线程切换成本
内存上
- 线程运行要涉及内存保存信息,每一个线程都分配一个MB级别的内存存储信息。注意,此处是固定,'很多'线程时MB单位存储对内存是一种浪费,这就意味着限制了编程中线程开辟数量,浪费了cpu/内存资源。goroutine是'动态'分配大小,重要的是它的单位是KB,你可以忽略线程的消耗,尽情的创建(此处不严谨的猜想:线程可挂起并通过通道传递信息,不难发现按需增大缩小正是动态分配的关键 <b>通讯</b>)大家可能明白协程为啥这么畅销码农市场了
我们来搞几个demo玩一下
之前参考了几篇go语言中文社区的文章,发现人家的讲解已经很清晰了,所以这里我计划多一些我自己的理解(偏理论)的东西
go协程使用,需要用到的几种通讯方式有 sync,channel ... (不下三种,水平有限,只会channel),go的官网文档介绍[sync:大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。],channel也是golang的最为核心的技术(论程序员的修养:channal是google的大佬底层封装的好)
阻塞/挂起线程类似于锁的概念或者说php/js中断点,只不过是可通过通讯方式来开启/关闭
有缓冲channel
声明方式
ch := make(chan int, 3)
运行方式'异步', 仅限不阻塞情况下异步执行,阻塞后还是同步等待执行,第二个参数为缓冲的大小
使用
- 通道的缓存无数据,但执行读通道。
- 通道的缓存已经占满,向通道写数据,但无协程读。
- 在缓冲范围内,两个goroutine不阻塞,异步执行,通过channel传输数据,超过缓冲长度时会阻塞等待消费者消费数据,消费后阻塞释放
- 无缓冲通道的读取方式:通过range 类迭代方式读取,但此处要注意的是range不会根据通道的长度来作为循环的次数,而是根据通道是否关闭来限定次数,如不关闭通道,无线循环取数据,会发生阻塞等待,发展成死锁。
ch := make(chan int, 3)
ch <- 7
ch <- 8
ch <- 9
close(ch) // 关闭通道
for item := range ch{
fmt.Println(item)
}
// stystem out : 7 8 9 //遵循先进先出顺序
// 或者通过 人工 break 跳出循环
ch := make(chan int, 3)
ch <- 7
ch <- 8
ch <- 9
for item := range ch {
fmt.Println(item)
if len(ch) == 0 {
break // 人工跳出循环
}
}
无缓冲channel
声明方式
ch := make(chan int)
运行方式'同步'
使用情况
- 通道中无数据,但执行读通道。
- 通道中无数据,向通道写数据,但无协程读取。
- 生产消息/消费消息(一下成为存/取)是原子性,每一个动作都会阻塞当前线程(挂起线程,并非销毁,一直等待对应的存/取操作发生来解锁),这也就意味着多数情况下使用无缓冲通道必须要开辟两个goroutine,一个存一个取,只要存了就必须要取出来,不然会无限阻塞形成死锁。所以无缓冲channel中len永远是0;
[阻塞:1.存进去就必须要取出来 2.如果本来就没数据去也可以取,但是要一直阻塞等待到其它goroutine存入数据]
示例方法为省代码量,全用闭包代替(主要是一些死锁情况)
存入无缓冲通道,此处触发阻塞,下面goroutine根本没机会执行,导致无线阻塞等待数据消费,形成死锁
xChan := make(chan int)
xChan<-1
go func() {
// code ...
<-xChan
}()
fmt.Println("main done.. no deadlock")
// fatal error: all goroutines are asleep - deadlock!
换个位置,再来一次,阻塞位置变化,直到子线程取出数据,阻塞释放(串行编程,没招)
xChan := make(chan int)
go func() {
// code ...
<-xChan
}()
xChan<-1
fmt.Println("main done.. no deadlock")
// main done.. no deadlock
代码竟然没报错,这是因为主线程没进行读/取操作,不存在阻塞,根本就等到子线程执行就执行完了(所有的线程的生命周期都是伴随着主线程的终止而终止,调度器和cpu就算再强大,开辟线程也是需要时间的)
c := make(chan int)
go func() {
c <- 1
}()
fmt.Println("main done.. no deadlock")
这个例子是说无缓冲channel要伴随着两个goroutine搭配,如果换成有缓冲通道,在一个goroutine就不会报错,因为是异步的,类似于queque,但也是失去了并发编程的意义
ch := make(chan int)
// 写入数据,堵塞当前线程, 没人取走数据阻塞不会释放
ch <- 1
//在此行执行之前Go就会报死锁
fmt.Println("main done.. no deadlock")
多channel管理可用 select 语句进行管理,可通过设置default方式避免死锁发生
// 每个 case 必须是一个通信操作,要么是发送要么是接收。
// select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行(运行后终止)。一个默认的子句应该总是可运行的。
input := make(chan interface{})
// 另开一个线程添加chan数据
go func() {
for i := 0; i < 5; i++ {
input <- i
}
input <- "hello, world"
}()
//os.Exit(22)
// 主线程执行过快 没存进去 select 就开始取了
time.Sleep(2*time.Second)
select {
// 3个case随机执行'一次',没有可执行的便执行defaule
case msg1:=<-input:
fmt.Println(msg1,"---1")
case msg2:=<-input:
fmt.Println(msg2,"---2")
case msg3:=<-input:
fmt.Println(msg3,"---3")
default:
fmt.Println("close")
}
- 无缓冲的信道是数据一个进一个出交替阻塞执行
- 有缓冲信道则是一个一个存储,一个个取出,异步队列不阻塞执行
这一期本来介绍一款虚拟机,但是想来想去打算学习一下docker后结合起来发表会更好,本人底层知识实属有限理解不到位,望各位兄台批评指正。
本故事纯属虚构
有疑问加站长微信联系(非本文作者)