文章目录
前言
进程和线程基本介绍
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以
并发
执行 - 一个程序至少有一个进程,一个进程至少有一个线程
程序,进程和线程的关系图
并发和并行
-
多线程合成在
单核
上运行,就是并发
-
多线程乘车在
多核
上运行,就是并行
总结:
并发:
因为是在一个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)
}
}
小结:
- 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
- Golang的协程机制是特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源消费大,这里就突显Golang在并发的优势了
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 在运行时也会提示一些错误 看演示
go build -race .\main.go
.\main.exe
错误信息
myMap[140] = 0
myMap[32] = -6045878379276664832
myMap[82] = 0
myMap[127] = 0
myMap[149] = 0
Found 2 data race(s)
//也会提示 有2个数据产生并发写入问题
解决方法
- 全局变量加锁同步
- 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类型数据
channel基本使用
- var 变量名 chan 数据类型
- var intChan chan int (intChan用于存放int数据)
- var mapChan chan map[int]string(mapChan用于存放map[int]string类型)
- var perChan chan Person
- var perChan2 chan *Person
说明
:
- channel是引用类型
- channel必须初始化才能写入数据,即make后才能使用
- 管道是由类型的,intChan只能输入整数int
package main
import(
"fmt"
)
func main() {
//演示一下管道的使用
//1.创建一个存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2.查看intChan结果
fmt.Printf("intChan 的值=%v intChan的地址值=%p\n",intChan,&intChan)
//结果 intChan 的值=0xc00001e100 intChan的地址值=0xc000006028
//3.向管道写入数据
intChan <- 10
num := 20
intChan <- num
intChan <- 50
// intChan <- 60 //写入管道的数据,不可以超过其容量 否则会报错 deadlock!
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 3,3
//4. 从管道中读取数据
var num2 int
num2 = <- intChan
fmt.Println(num2)//结果 10
// 读取时同理不可以超过存储容量 否则会报错 deadlock!
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 2,3
//5.当chan 存储满后 需要读取出一条数据后 才可以再次存储
intChan <- 60
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 3,3
}
channel 演示案例一
package main
import(
"fmt"
)
type Cat struct{
Name string
age int
}
func main() {
//定义一个存放任何数据类型的管道 3个数据
/*
var allChan chan interface{}
allChan = make(chan interface{}, 3)
*/
allChan := make(chan interface{}, 3)//和上面命名同样效果
allChan <- 10
allChan <- "tom"
cat := Cat{"喵喵",12}
allChan <- cat
//希望获取第三个元素,则先将前俩个推出
<- allChan
<- allChan
newCat := <- allChan
fmt.Printf("newCat=%T newCat=%v\n",newCat,newCat)
//结果 :newCat=main.Cat newCat={喵喵 12}
//下面获取newCat.Name 是无法获取到名称的 ,需要进行断言
// fmt.Printf("newCat=%T newCat=%v\n",newCat,newCat.Name)
newCatInfo := newCat.(Cat)
fmt.Printf("newCat=%T newCat=%v\n",newCatInfo,newCatInfo.Name)
/*
结果
newCat=main.Cat newCat=喵喵
*/
}
channel的遍历和关闭
channel关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能在向channel写数据了,但是仍然可以从该channel读取数据
package main
import(
"fmt"
)
func main(){
intChan := make(chan int ,3)
intChan <- 10
intChan <- 20
close(intChan)//关闭后无法写入
//intChan <- 30 //提示:panic: send on closed channel
fmt.Println("ok~!")
//可以正常获取管道中的值
n1 := <- intChan
fmt.Println(n1)
//结果 10
}
channel的遍历
package main
import(
"fmt"
)
func main(){
//创建一个管道并且添加数据
intChan := make(chan int , 100)
for i := 0; i < 100; i++ {
intChan <- i * 2
}
//在遍历时,如果channel没有关闭,则会出现all goroutines are asleep - deadlock!的错误
//在遍历时,如果channel已经关闭,则会正常遍历数据
close(intChan)
//循环获取intChan数据
for v := range intChan {
fmt.Printf("v=%v\n",v)
}
}
goroutine和channel结合
应用案例一
package main
import(
"fmt"
)
//writeData写入数据
func writeData(intChan chan int){
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Printf("writeData 插入的数据=%v\n",i)
}
//关闭管道
close(intChan)
}
//readData 读取数据
func readData(intChan chan int,exitChan chan bool){
for {
v,ok := <-intChan
//关闭管道时 intChan返回ok数据是否读取完毕
if !ok {
break
}
fmt.Printf("readData 读取的数据=%v\n",v)
}
//读取完成后 验证改为true
exitChan<- true
close(exitChan)
}
func main() {
//创建俩个管道
intChan := make(chan int,50)
exitChan := make(chan bool, 1) //只要有数据就表示已读取完成
//开启协程
go writeData(intChan)
go readData(intChan,exitChan)
for {
_,ok := <-exitChan
//数据为空时 ,表示都以完成
if !ok {
break
}
}
}