文章目录
前言
进程和线程基本介绍
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以
并发
执行 - 一个程序至少有一个进程,一个进程至少有一个线程
程序,进程和线程的关系图
并发和并行
-
多线程合成在
单核
上运行,就是并发
-
多线程乘车在
多核
上运行,就是并行
总结:
并发:
因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这个10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
并行:
因为是在多个cpu上(比如有10个cpu),比如有10个进程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这个10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
Go协程和Go主线程
- Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,你可以这样理解,
协程就是轻量级的线程
- Go协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的协程
goroutine 快速入门
案例一
package main
import(
"fmt"
"time"
"strconv"
)
// 在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔1秒 输出 ”hello,world“
// 在主线程中也每隔一秒输出 ”hello,golang“,输出10后,退出程序
// 要求主线程和 goroutine同时执行
//编写一个函数,每个一秒输出 ”hello,world“
func test(){
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world " + strconv.Itoa(i))
time.Sleep(time.Second) //宕机一秒
}
}
func main(){
//普通方式
// test()
/*
结果
test() hello,world 1
test() hello,world 2
....
test() hello,world 10
main() hello,golang 1
main() hello,golang 2
....
main() hello,golang 10
普通方式只有test()执行完成后才会执行主进程for
*/
go test()//开启一个协程
/*
main() hello,golang 1
test() hello,world 1
test() hello,world 2
main() hello,golang 2
main() hello,golang 3
test() hello,world 3
....
test() hello,world 10
main() hello,golang 10
开启协程后会同时执行
*/
for i := 1; i <= 10; i++ {
fmt.Println("main() hello,golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
MPG模式基本介绍
- M:操作系统的主线程(是物理线程)
- P:协程执行需要的上下文
- G:协程
设置Golang 运行的CPU
package main
import(
"fmt"
"runtime"
)
func main() {
//NumCPU返回本地机器的逻辑CPU个数
cpuNum := runtime.NumCPU()
//可以自己设置使用多个CPU
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Printf("cpuNum=%v",cpuNum)
//结果cpuNum=12
}
channel(管道) 快速入门
案例一
package main
import(
"fmt"
)
// 需求:线程要计算 1-200 的各个数的阶乘,请求把各个数的阶乘放入到map中
// 最后显示出来。要求使用goroutine完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并且放入到 map中
// 2. 启动多个协程,将统计的结果放入到 map中
// 3. map 应该做出一个全局
var (
myMap = make(map[int]int,10)
)
// test 函数 计算 n,将这个结果放入到map中
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//将结果放入map中
myMap[n] = res
}
func main(){
//开启协程 开启200个协程完成这个任务
for i := 1; i <= 200; i++ {
go test(i)
}
//输出结果
for k, v := range(myMap){
fmt.Printf("myMap[%d] = %d\n",k,v)
}
/*
错误:
fatal error: concurrent map writes
fatal error: concurrent map iteration and map write
fatal error: concurrent map writes
错误为 并发映射输入
*/
}
在编译该程序时,郑家一个参数 -race 在运行时也会提示一些错误 看演示
解决方法
- 全局变量加锁同步
- channel
全局变量加锁同步
package main
import(
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int,10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
lock sync.Mutex
)
// test 函数 计算 n,将这个结果放入到map中
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//将结果放入map中
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
func main(){
//开启协程 开启200个协程完成这个任务
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒钟
time.Sleep(time.Second * 10)
//输出结果
//加锁
lock.Lock()
for k, v := range(myMap){
fmt.Printf("myMap[%d] = %d\n",k,v)
}
//解锁
lock.Unlock()
/*
结果:
myMap[29] = -7055958792655077376
myMap[37] = 1096907932701818880
myMap[44] = 2673996885588443136
myMap[50] = -3258495067890909184
myMap[61] = 3098476543630901248
myMap[123] = 0
myMap[136] = 0
myMap[139] = 0
myMap[143] = 0
结果成功 没有提示并发插入数据
*/
}
channel基本介绍
为什么需要channel
前面使用全局变量加锁同步解决goroutine的通讯,但不完美
- 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置了10秒,仅仅是估算
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随着主线程的退出而销毁
- 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
- 上面种种分析都在呼唤一个新的通讯机制
channel
channel的介绍
- channel本质就是一个数据结构-队列 如下图
- 数据是先进先出
- 线程安全,多个goroutine访问时,不需要加锁
- channel是有类型的,一个string的channel只能存放string类型数据
待续。。。。