时间单位
- 1S = 1000ms
- 1ms = 1000us
- 1us = 1000ns
并发与并行
- 并行: 借助多核 cpu 实现。 (真 并行)
- 并发:
- 宏观:用户体验上,程序在并行执行。
- 微观:多个计划任务,顺序执行。在飞快的切换。轮换使用 cpu 时间轮片。 【假 并行】
进程并发
- 程序:编译成功得到的二进制文件。
- 占用磁盘空间
- 死的
- 系统中相同的程序一般只有一个,有可能存在不同版本号的程序
- 进程:运行起来程序。 占用系统资源。
- 进程是根据程序创建的,运行在内存中
- 活的
- 一个程序可以启动N个进程
- 一个程序可以启动N个进程,程序和进程的关系是1对N的关系。
进程状态
- 初始态
- 就绪态:程序会经常从就绪态切换到运行态。
- 运行态:只有运行态能切换到阻塞态。
- 挂起(阻塞)态:阻塞态可以切换回运行态继续往下执行。
- 终止(停止)态
线程并发
- 线程:LWP 轻量级的 进程。最小的执行单位。是cpu分配时间轮片的对象。
- 进程: 最小的系统资源分配单位。
同步
- 协同步调。规划先后顺序。
- 线程同步机制:
- 互斥锁(互斥量):建议锁。拿到锁以后,才能访问数据,没有拿到锁的线程,阻塞等待。等到拿锁的线程释放锁。
- 读写锁:一把锁(读属性、写属性)。 写独占,读共享。 写锁优先级高。
协程并发
- Python、Lua、Rust。。。
- 21世纪
- 线程会io阻塞,协程可以在线程io阻塞期间相互切换,执行其他不需要消耗io操作的协程
- 提高程序执行的效率
总结
- 进程、线程、协程 都可以完成并发。
- 稳定性强、节省资源、效率高。
- 老板——手机:
- 生产线 —— 设备、材料、厂房 —— 进程。(资源分配单位)
- 工人 —— 线程。 —— 单进程、单线程的 程序。
- 50 工人 —— 50 线程。 ——单进程、多线程的 程序。
- 10 条生产线 —— 500 工人 —— 。多进程、多线程的 程序。
- 利用闲暇时间义务搬砖 —— 协程。—— 多进程、多线程、多协程 程序。
创建Goroutine程
- 创建于进程中。 直接使用 go 关键,放置于 函数调用前面,产生一个 go程。 并发。
示例代码:
package main
import (
"fmt"
`time`
)
// 唱歌
func sing() {
for i:=0;i<50;i++ {
fmt.Println("----正在唱:隔壁泰山----")
}
}
// 跳舞
func dance() {
for i:=0;i<50;i++ {
fmt.Println("----正在跳舞:赵四街舞----")
}
}
func main() {
go sing() // 开启goroutine
go dance() // 另一个goroutine,会抢夺CPU执行权,两个goroutine交替执行
time.Sleep(time.Second)
}
Goroutine的特性:【重点】
- 主go程结束,子go程随之退出。
示例代码:
package main
import (
"fmt"
"time"
)
func main() {
go func() { // 创建一个 子go 程
for i := 0; i < 5; i++ {
fmt.Println("------I'm goroutine -------")
time.Sleep(time.Second)
}
}()
// 主goroutine结束,子goroutine随着退出
// 所以,子goroutine很有可能没有执行完就退出了
fmt.Println("------I'm main-------")
}
runtime.Gosched()
- 出让当前go程所占用的 cpu时间片。当再次获得cpu时,从出让位置继续回复执行。
- 时间片轮转调度算法。
示例代码:
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for {
runtime.Gosched()
fmt.Println("goroutine 1执行。。。。")
}
}()
for {
runtime.Gosched() // 出让当前 cpu 时间片。
fmt.Println("主go程执行。。。。")
}
}
runtime.Goexit()
- return: 返回当前函数调用到调用者那里去。 return之前的 defer 注册生效。
- Goexit(): 结束调用该函数的当前go程。Goexit():之前注册的 defer都生效。
示例代码:
package main
import (
"fmt"
"runtime"
`time`
)
func test() {
defer fmt.Println("退出子go程之前")
// return
runtime.Goexit() // 退出当前go程。
defer fmt.Println("退出子go程之后")
}
func main() {
fmt.Println("主go程开始执行")
go func() {
fmt.Println("父go程执行之前")
// runtime.Goexit() 会确保defer执行
defer fmt.Println("defer 父go程执行之后")
go test()
fmt.Println("父go程执行之后")
}()
fmt.Println("主go程结束执行")
time.Sleep(time.Second)
}
runtime.GOMAXPROCS()
- 设置当前 进程使用的最大cpu核数。 返回 上一次调用成功的设置值。 首次调用返回默认值。
示例代码:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.GOROOT())
n := runtime.GOMAXPROCS(0) // 0表示最大
fmt.Println("n = ", n) // 返回上次设置成功的设置值
n = runtime.GOMAXPROCS(2)
fmt.Println("n = ", n) // 16
n = runtime.GOMAXPROCS(4)
fmt.Println("n = ", n) // 2
n = runtime.GOMAXPROCS(8)
fmt.Println("n = ", n) // 4
n = runtime.GOMAXPROCS(1)
fmt.Println("n = ", n) // 8
}
补充知识点
- 每当有一个进程启动时,系统会自动打开三个文件: 标准输入、标准输出、标准错误。 —— 对应三个文件: stdin、stdout、stderr
- 当进行运行结束。操作系统自动关闭三个文件。
channel
- 是一种数据类型。 对应一个“管道”(通道 FIFO)
- channel的定义:
- make (chan 在channel中传递的数据类型, 容量)
- 容量= 0: 无缓冲channel
- 容量 > 0 :有缓冲channel
- make(chan int) 或 make (chan string , 0)
- make (chan 在channel中传递的数据类型, 容量)
- channel有两个端:
- 一端:写端(传入端) chan <-
- 另一端: 读端(传出端)<- chan
- 要求:读端和写端必须同时满足条件,才在shan上进行数据流动。否则,则阻塞。
- channel是一种先进先出(FIFO)的队列类型
示例代码
package main
import (
"fmt"
"time"
)
// 全局定义channel, 用来完成数据同步
var flagChan = make(chan struct{}) // 空struct不占用空间
// 定义一台打印机
func printer(s string) {
for _, ch := range s {
fmt.Printf("%c", ch) // 屏幕:stdout
time.Sleep(300 * time.Millisecond)
}
}
// 定义两个人使用打印机
func person1() { // person 先执行。
printer("hello")
flagChan <- struct{}{} // 写入数据,解除阻塞
}
func person2() { // person 后执行
printer("world")
flagChan <- struct{}{} // 写入数据,解除阻塞
}
func main() {
go person1()
<-flagChan // 拿到数据之前,一直阻塞,所以会等到go程执行结束,直到写入
go person2()
<- flagChan
fmt.Println("\n程序执行结束!!!")
}