golang的入门day5(并发编程 + WaitGroup + 互斥锁 + )

Go是一个 并发语言: 同时处理多个事情的能力称为并发性(同一时间点只能做一件任务), 而并行性(你边跑,边听歌)。

并行不一定快: 因为并行运行组件之间需要通信的,这个通信的成本很高,而并发通信的成本就很低了。

所以 线程间通信成本远远低于进程间通信。
进程间通信(并行,成本高)
线程间通信 (并发: 成本低)

协程 : Coroutine

协程 又称为 轻线程

协程可以开几百万个
而线程 进程 不会超过一万个

Go语言的并发: 协程(Goroutine)来实现

看下 macOS 下的 进程线程

在这里插入图片描述

Goroutine 轻量级的 线程: 是Go的专有名词,区别于进程Process ,线程Thread , 协程 Coroutine . 因为 Go语言的创造者认为Go的协程 和 其他语言的协程是有所区别的,因此: 专门创造了Goroutine

Goroutine 主要执行 并发 : 可以理解为函数

Goroutine 协程的创建

go + 函数 () 函数没有返回值, 有返回值 也会被舍弃

package main

import "fmt"

func printNum(){
	for i:=1;i <=1000;i++{
		fmt.Println(i)
	}
}

func  printChar(){
	for i:=1; i<1000; i++{
		fmt.Println('A')
	}
}
func main(){
	//一个Goroutine打印数字,  另一个过Goroutine打印z字母:
	go printNum()

	for i:=1; i<1000; i++{
		fmt.Printf("%c\n",'A')
	}
	fmt.Println("main over...")

}

在这里插入图片描述
发现 字母数字 交叉着 打印 :

注意 ; 对于Go 语言 来说 : 主Goroutine 结束了 ,那么,整个程序就结束了 ,不会管你 子Goroutine 是否执行完毕

在这里插入图片描述我们可以借助 : time.sleep(1* time.Second)来进行验证

因此 : 引入了 Channel通道

Goroutine 的规则:

1 : 当一个 新的 Goroutine开始时:Goroutine 调用立即返回,与函数不同, go程序 不等待Goroutine执行结束。 当Goroutine调用,任何返回值都会被忽略,接着 go程序 立即执行下一行代码。

2 如果主的 Goroutine执行结束,程序就会终止, 其他Goroutine 将不再运行。

主的Goroutine :

封装main函数的 Goroutine 称为主的Goroutine, 主协程 并不仅仅是 执行main 函数,

1 : 首先 : 主协程 要设定每一个协程栈空间的最大值, 32位 协程栈空间的最大值位250M, 64位为1GB, 当协程的栈空间超过设定的空间时, 就会引起panic恐慌,俗称栈溢出, (依旧是异常中断),go程序终止

2 : 接着 : 主协程 进行一系列的初始化工作:
a: 设置一个 特殊的 defer 语句:用于在主协程退出之后的善后工作,因为主协程也可能非正常结束。
b: 启动专门用于在后台清扫内存垃圾的协程goroutine, 并设置GC(内存清理)可用的标识
c: 执行main包中的 init函数
d: 执行main函数
执行完main函数之后, 他还会检查主协程是否引发了运行时的panic 恐慌, 并进行必要的处理, 最后主协程会结束自己以及当前进程的运行。

goroutine 运行时会像线程一样 抢占系统资源的,抢占式执行,我们也可以 通过睡眠 控制 goroutine的执行流程。

Go的 并发模型

并发模型在操作系统层面都是以线程的方式展现的。、

内核空间; 访问CPU资源, I哦资源, 内存资源等硬件资源,
用户空间: 不可直接访问资源, 必须通过系统调用,函数库、 Shell 脚本等带哦用系统资源。。

线程的实现模型主要左右3种 :

内核级线程模型、 用户级线程模型、 和 两级线程模型
最大差异: 线程 与 内核调度实体之间的 对应关系上

内核调度实体(KSE): 可以被内核调度器调度的对象。

1 内核级线程模型: 1对1 模型(linux , C++)

将用户级线程 和 内核级线程1对1 的联系起来。

好处 ; 支持真正的 并行
缺点 : 线程创建开销大,影响效率

1.2 用户级线程模型(现在很多语言都是这样)

多个用户级线程 : 1个内核级线程(M:1)
缺点: 当某个用户线程上调用 阻塞式系统调用(read网络IO), 那么内核级线程可能发生阻塞, 导致所有的用户级线程全部阻塞。

使用的语言会自己封装实现阻塞式的系统调用。

1.3 两级线程模型:Go语言使用这种模型

混合型线程模型: 用户调度器实现用户线程到(内核调度实体)KSE的调度;而内核调度器(KSE)实现KSE到CPU上的线程调度。

Go 并发调度: G-P-M 模型:

在操作系统提供的那个线程之上, Go语言搭建了一个特有的两级线程模型:。 goroutine 机制实现了M:N的线程模型, goroutine机制是协程的一种实现, golang 内置的调度器可以让多核CPU中每个CPU执行一个协程

理解goroutine 机制的原理: 关键在理解 go语言 scheduler 的实现。

Go语言中支持scheduler 的实现靠4个结构,分别是:M - G - P - Sched

前三个定义在runtime.h中, Sched 定义在 proc.c中

Sched结构就是调度器, 他维护有存储M 和 G 的队列以及调度器的一些状态信息等。

**M结构是Machine ,**属于系统线程,由操作系统管理, goroutine 就是跑在M之上的, M是一个很大的结构,里面维护小对象的内存(Cache)、当前执行的Goroutine、随机数发生器等非常多的信息

P 结构是 processor, (逻辑)处理器(上下文环境), 他的主要用途就是用来执行 goroutine的, 他维护了一个goroutine队列,即runqueue , processor 处理器 是让我们从 N:1 调度 到 M:N调度 的重要部分。

G是goroutine实现的核心结构, 它包含了栈,指令指针, 以及其他对goroutine很重要的信息, 例如 channel

在单核处理器的场景下: 所有的goroutine运行在同一个M系统线程中,每一个M系统线程维护一个P(processor),任何时刻,每一个上下文环境P中只有一个G(goroutine) 其他的gorountine 在runqueue中等待, 一个goroutine运行完自己的时间片后, 让出上下文P, 回到runqueue中,

多核处理器环境下,为了运行多个goroutine, 每个M系统线程会持有一个P.

在正常情况下,scheduler会按照上面的流程进行调度,但是线程会发生阻塞等情况在这里插入图片描述
在这里插入图片描述

runtime 包:

包含了 Go运行时的系统交互的操作,goroutine的控制函数, 运行时类信息等等。 内存分配, 栈的信息、 GC处理等等

在这里插入图片描述

在这里插入图片描述

fmt.Println("goroot目录:", runtime.GOROOT())
fmt.Println("当前操作系统:",runtime.GOOS)//darwin, mac系统
fmt.Println("逻辑CPU的数量-->",runtime.NumCPU())//
//注意最在程序运行之前就设置好, 可以写在init函数里

n := runtime.GOMAXPROCS(runtime.NumCPU())//设置逻辑CPU的数量,返回值是上一次设置的数量


fmt.Println("逻辑CPU的最大数量为:",n)// 说明最大只能为4

runtime.GoSched() //使当前goroutine让出CPU,先让别的goroutine执行

//goSched  让当前GoRoutine让出CPU的时间片
//一个goroutine
go func (){
	for i:=0; i<4; i++ {
		fmt.Println("goroutine...")
	}
}()
//main  goroutine
for i :=0; i<4; i++{
	runtime.Gosched()//主goroutine抢到CPU时,让出时间片
	fmt.Println("main。。。。")
}

在这里插入图片描述
但是 runtime.Gosched 本身也是可以抢占资源的 ,多执行几次就会发现:

在这里插入图片描述

runtime,Goexist() :终止当前的goroutine : 但是defer 语句还是会执行的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package main
	func main(){
	go func(){
		fmt.Println("go 开始...")
		fun()
		fmt.Println("go程序结束")
	}()
	time.Sleep(3 * time.Second)//主 goroutine睡3秒
}
func fun(){
	fmt.Println("进入fun函数...")
	defer fmt.Println("defer...")
	//return // 只是结束当前fun函数
	runtime.Goexit() // 完全退出当前的goroutine
	fmt.Println("退出fun函数...")
}

临界资源安全问题

在这里插入图片描述

var count int =100
func fun1(name string) {
	rand.Seed(time.Now().UnixNano())
	for true{
		if count > 0 {
			//模拟 业务处理耗费时间
			time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)

			fmt.Println(name,": 出票成功...  当前余票: %d", count-1)
			count--;
		} else {
			fmt.Println(name,": Sorry, 暂无余票...")
			break
		}
	}
}

func SellTicket(){
	go fun1("窗口1")
	go fun1("窗口2")
	go fun1("窗口3")
	go fun1("窗口4")
}

func main() {
	SellTicket()
	time.Sleep(3 * time.Second)
	}

在这里插入图片描述

注意: 是不是发生了临街资源不安全的问题: 在 C++下也是一样的, 会发生临街资源安全问题。

解决策略: 1 加锁 : 2 无锁编程

在这里插入图片描述

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup //创建同步等待组对象

func func1(){
	for i:=0; i<10; i++{
		fmt.Println("func1 函数打印... A ",i)
	}
	defer wg.Done()// 该函数执行结束时: wg同步等待组中的goroutine数量 -1
}

func func2(){
	defer wg.Done()// 该函数执行结束时: wg同步等待组中的goroutine数量 -1
	for i:=0; i<10; i++{
		fmt.Println("\tfunc2 函数打印...  ",i)
	}
}

func main(){
	/*
		WaitGroup: 同步等待锁
		Add()设置等待组中要执行的 goroutine的数量
		Wait()  让主的goroutine等待
	*/
	wg.Add(2)// 我们目前有两个子的goroutine 要执行
	go func1()
	go func2()

	fmt.Println(" main 函数进入阻塞状态。。等待wg同步等待组中的goroutine全部结束")
	wg.Wait()// 表示 main  goroutine进入等待,意味着阻塞 
	//当 wg中goroutine 数量为0时 ,主 goroutine解除阻塞
	fmt.Println("main  接触 阻塞状态")
}

注意 wg.Add()方法 ;数量不能多, 也不能少“

多了 就会形成死锁: 导致 主goroutine无法被解除

少了 有的 goroutine 就不会被执行

想要增加 同步等待组中 goroutine的数量,必须在 Wait之前, 想要减少 ,随时都可以。

互斥锁 (在sync包下)

var count int =100
var wg sync.WaitGroup // 设置同步等待组对象,避免主goroutine的sleep
var mutex sync.Mutex //创建一个锁
func fun1(name string) {
	defer wg.Done() //同步等待组中goroutine数量及时减1,避免形成死锁(主的goroutine 解除不了阻塞)
	rand.Seed(time.Now().UnixNano())
	for true{
	//上锁:
	mutex.Lock()
		if count > 0 {
			//模拟 业务处理耗费时间
			time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)

			fmt.Println(name,": 出票成功...  当前余票: %d", count-1)
			count--
			mutex.UnLock()
		} else {
			fmt.Println(name,": Sorry, 暂无余票...")
			mutex.UnLock()
			break
		}
	}
}

func SellTicket(){
	go fun1("窗口1")
	go fun1("窗口2")
	go fun1("窗口3")
	go fun1("窗口4")
}

func main() {
	wg.Add(4)//数量不能多,不能少
	SellTicket()
	wg.Wait()//主 goroutine 进行阻塞等待,等到wg中数量为0
	//time.Sleep(3 * time.Second)
	fmt.Println("main  over ...")
	}

读写锁: 分别针对读写操作

主要是 写锁需要同步与互斥

RLock()读锁
RUnlock()渎解锁
该方法处于接口内: 不同对象调用

如果没有上锁 ,却去解锁: 是会发生错误的

在这里插入图片描述

读操作
package main

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

var wg sync.WaitGroup
var rwMutex *sync.RWMutex
func readMutex(i int){
	defer wg.Done()
	fmt.Println(i,"开始读...")
	rwMutex.RLock()
	fmt.Println(i, "正在读取数据...")

	time.Sleep(1 * time.Second)
	rwMutex.RUnlock()
	fmt.Println(i, "读结束....")
}
func main(){
	rwMutex = new(sync.RWMutex)
	go readMutex(1)
	go readMutex(2)

	wg.Add(2)
	wg.Wait()
	fmt.Println("main over...")
}
写操作

在这里插入图片描述

package main

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

var rwMutex2 *sync.RWMutex
var wg2  sync.WaitGroup

func writeData(i int){
	defer wg2.Done()
	fmt.Println("开始写...")
	rwMutex2.Lock()
	fmt.Println(i,"正在写入...")
	time.Sleep(1 * time.Second)
	fmt.Println(i,"写结束...")
	rwMutex2.Unlock()
}

func main(){
	rwMutex2 = new(sync.RWMutex)
	go writeData(1)
	go writeData(2)
	go writeData(3)


	wg2.Add(3)
	wg2.Wait()
	fmt.Println("main over...")
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值