go goroutine 和 channel

goroutine

进程和线程

  1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
  4. 一个程序至少有一个进程,一个进程至少有一个线程

程序,进程和线程的关系

在这里插入图片描述

并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行

在这里插入图片描述

并发:
因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

并行:
因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

go协程和go主线程

go主线程(可以称为线程/也可以理解成进程):一个go线程上,可以起多个协程,协程是轻量级的线程【编译器做优化】

go协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

在这里插入图片描述

goroutine案例

  1. 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 “hello, world”
  2. 在主线程中也每隔1秒输出"hello, golang",输出10次后退出程序
  3. 要求主线程和goroutine同时执行
package main

import (
	"fmt"
	"strconv"
	"time"
)

func test()  {
	for i := 1; i <= 10; i++ {
		fmt.Println("test() hello, world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
func main()  {
	go test() //开启了一个协程
	for i := 1; i <= 10; i++ {
		fmt.Println("main() hello, golang " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

在这里插入图片描述

说明:

  1. 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
  3. golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显golang在并发上的优势

goroutine的调度模型

MPG模式

在这里插入图片描述

  1. M: 操作系统的主线程(物理线程)
  2. P: 协程执行需要的上下文
  3. G: 协程

MPG模式运行的状态1

在这里插入图片描述

  1. 当前程序有三个M,如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行
  2. M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3协程队列有2个
  3. 可以看到: go的协程是轻量级的线程,是逻辑态的。go可以容易的起上万个协程
  4. 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu

MPG模式运行的状态2

在这里插入图片描述

扫描二维码关注公众号,回复: 9412737 查看本文章
  1. 分成两个部分来看
  2. 原来的情况是M0主线程正在执行G0协程,另外有三个协程在队列等待
  3. 如果G0协程阻塞,比如读取文件或者数据库等
  4. 这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写
  5. 这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行
  6. 等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒

设置golang运行的cpu数

为了充分利用多cpu的优势,在golang程序中,设置运行的cpu数目

package main

import (
	"fmt"
	"runtime"
)

func main()  {
	//获取当前系统cpu的数量
	num := runtime.NumCPU()
	//设置num-1的cpu运行go程序
	runtime.GOMAXPROCS(num)
	fmt.Println("num=", num)
}
  1. go1.8后,默认让程序运行在多个核上,可以不用设置了
  2. go1.8前,还要设置下,可以更高效的利用cpu

channel(管道)

计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。使用goroutine完成

package main

import (
	"fmt"
	"time"
)

//1.编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的多个协程,统计的将结果放入到map中
//3. map应该做出一个全局的
var (
	myMap = make(map[int]int, 10)
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n int)  {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	myMap[n] = res
}
func main()  {
	//开启多个协程完成这个任务[200个]
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	//休眠10秒
	time.Sleep(time.Second * 10)
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
}

在这里插入图片描述

不同goroutine之间如何通讯

  • 全局变量的互斥锁
  • 使用管道channel

使用全局变量加锁同步改进程序

  • 没有对全局变量加锁,会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
  • 解决:加入互斥锁
  • 数的阶乘很大,结果会越界,可以将求阶乘改成 sum += i
package main

import (
	"fmt"
	"sync"
	"time"
)

//1.编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的多个协程,统计的将结果放入到map中
//3. map应该做出一个全局的
var (
	myMap = make(map[int]int, 10)
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁
	//sync 是包: synchornized 同步
	//Mutex : 是互斥
	lock sync.Mutex
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n int)  {
	res := 1
	for i := 1; i <= n; i++ {
		res += i
	}
	//res放入到myMap
	//加锁
	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 i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()
}

为什么需要channel

发布了97 篇原创文章 · 获赞 25 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/wuxingge/article/details/104496836