本文摘自网络,作者,侵删。
参考链接: 角度6-管道
Go 语言 协程和管道讲解
一、进程和线程基本说明:
进程是程序在操作系统中一次执行过程,是系统进行资源分配和调度的基本单位;线程是进程的一个执行实例,是程序最小单元,它是比进程更小的能独立运行的基本单位;一个进程可创建和销毁多个线程,同一个进程的多个线程可以并发执行;一个程序至少有一个进程,一个进程至少有一个线程;
举个栗子:
使用的迅雷客户端,打开迅雷就是开启了一个进程,而下载多个视频,就是多个线程在工作;
二、并发、并行简单说明:
1.并发:
多线程程序在单核上运行,就是并发;
特点:
多个任务作用在一个cpu上;从微观的角度看,在一个时间点上,其实只有一个任务在执行,只是时间切片较块;
2.并行:
多线程程序在多核上运行,就是并行;
特点:
多个任务作用在多个cpu上;从微观的角度看,在一个时间点上,多个任务在同时执行;
并行的速度要快
三、协程基本介绍:
1.基本概念:
一个线程上,可以有多个协程,协程是轻量级的线程;
协程特点:
有独立的栈空间;共享程序堆空间;调度由用户控制;协程是轻量级的线程;
2.快速案例:
package main
import (
"fmt"
"strconv"
"time"
)
func test(){
for i:= 0; i < 10; i++{
fmt.Println("test() " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启一个协程
for i := 0; i < 10; i++{
fmt.Println("main()" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
主线程是一个物理线程,直接作用在CPU上,非常消耗CPU资源;协程从主线程开启的,是轻量级的线程,对资源消耗小;其它语言的并发机制一般是基于线程,开启过多的线程,资源消耗较大,这就体现出golang的优势;
3.MPG模式基本介绍:
M:操作系统的主线程(物理线程);P:协程执行需要的上下文;G:协程;
MPG模式介绍
4.设置cpu数:
package main
import (
"fmt"
"runtime"
)
func main () {
// 查看系统cpu个数
cpuNum := runtime.NumCPU()
// 可以自己设置使用多个cpu
runtime.GOMAXPROCS(cpuNum)
fmt.Println("cpuNum", cpuNum)
}
go 1.8版本以后,默认让程序运行在多核上,可不用设置;go 1.8版本前,需要设置,才可以更高效的利用cpu;
四、协程之间如何通讯?
1.全局变量加锁:
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int, 10)
// 声明一个全局互斥锁
// lock 是一个全局互斥锁 sync 是包 同步的意思 Mutex:是互斥
lock sync.Mutex
)
// test函数计算 n的阶乘, 将结果放到map中
func testCount(n int) {
res := 1
for i := 1; i <= n; i++ {
res *=i
}
// 加锁
lock.Lock()
myMap[n] = res
// 解锁
lock.Unlock()
}
func main () {
// 开启多个协程完成20个任务
for i := 1; i <= 20; i++{
go testCount(i)
}
time.Sleep(time.Second * 5)
lock.Lock()
for k, v := range myMap{
fmt.Printf("map[%d]=%d\n", k, v)
}
lock.Unlock()
}
声明全局互斥锁;写的时候加索,写完释放锁;读的时候加索,读完释放锁;
否则会出现资源竞争的问题;报错信息:fatal error: concurrent map writes
全局变量加锁同步是低级程序操作:
主线程等待所有协程全部完成时间很难确定,因为主线程结束,不管协程是否执行完,程序就此结束;通过全局变量加锁同步实现通讯,也不利于多个协程对全局变量的读写操作;
2.使用管道channel解决:
2.1.channel的介绍:
声明方式:
var 变量名 chan 数据类型
channel 本质 就是一个数据结构(队列); 数据是先进先出(FIFO); 线程安全,多个协程访问,不需要加锁; channel只能存放指定数据类型;
如:一个string的channel只能存放string类型数据
channel是引用类型;
必须初始化才能写入数据,即make后才能使用
channel数据放满后,就不能在放; channel数据取完后,再取就会报错;
2.2.快速栗子:
package main
import "fmt"
func main() {
// 管道的使用
// 1.创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
// 2.查看intChan 是什么?
fmt.Printf("intchan:%v\n", intChan) // 输出结果: intchan:0xc00008c080 可以看出是引用类型
// 3.向管道写入数据
intChan<- 10
num := 100
intChan<- num // 也可以写入常量
// 4.看看管道的长度和cap(容量:定义的长度跟容量是相等的, 不同于map类型等)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
// 4输出结果:channel len=2 cap=3
//5.从管道中读取数据
//var num2 int
num2 := <-intChan
fmt.Println("取出的num2=", num2)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
// 5输出结果:channel len=1 cap=3
}
注意:
如果往管道中存入数据,管道已经满了,或者取数据,管道中已经没有值,会报错信息fatal error: all goroutines are asleep - deadlock!
2.3.channel关闭:
使用内置函数close可以关闭channel,当channel关闭后,就不能向channel写数据,但是仍然可以读数据;
举个栗子:
package main
import "fmt"
func main() {
// 创建一个管道,大小为3
intChan := make(chan int, 3)
intChan <- 3
intChan <- 5
// 将管道进行关闭
close(intChan)
// 此时会无法写入, 因为管道已经关闭: 报错信息 panic: send on closed channel
//intChan <-6
n1 := <- intChan
fmt.Println("可以从管道中读取值:", n1)
}
2.4.channel遍历:
channel 支持 for-range的方式进行遍历:
相关阅读 >>
更多相关阅读请进入《Go》频道 >>
Go语言101
一个与时俱进的Go编程知识库。