GO 重新进阶学习(三)

https://learnku.com/docs/the-way-to-go/chapter-description/3684

重点协程,线程,通信
(直接复制粘贴没有任何意义,自己敲才是和理解才是硬道理)

进程的基本概念

进程是进程实体的运行过程,是系统进行资源分配和调度的基本单位

进程的特征

1动态性:进程的生命周期
2并发性:多个进程实体能在内存中,同时运行
3独立性:每一个进程都有PCB
4异步性:进程同步机制
5结构性:进程实体的结构

  1. 程序段
  2. 数据段
  3. 进程控制模块
进程的状态

主要就是五个状态
1创建:申请PCB,然后初始化PCB,系统分配资源。后转为就绪态
2就绪:除了处理机,什么都有。系统中有就绪队列
3运行:处理器进行处理
4阻塞:等待IO,或其他操作。
5终止(结束):完成整个进程

进程的控制

PCB(进程控制块)
进程的控制是通过PCB
进程本身是三个部分
有程序段、数据段和PCB
创建进程的时候就会创建一个PCB,然后常驻内存,任意时刻都可以读取。并在进程结束的时候删除。
重要的
PID 进程标志符 唯一编号
UID 用户标志符

进程控制和管理信息:当前状态,优先级
资源分配:资源分配的信息
处理器:寄存器的信息

在操作系统中,一般把进程控制用到程序段称为原语
这里go中还有一个原子操作

https://cloud.tencent.com/developer/article/1882225
https://www.jianshu.com/p/a0be632df99b

原语的的特点:
执行期间不允许打断,它是一个不可分割的基本单位
进程的创建
创建原语过程

  1. 分配一个唯一的进程号,PID
  2. 分配资源(如果资源不够,进入阻塞状态)
  3. 初始化PCB
  4. 进入就绪队列

进程的终止
进程终止的原因

  1. 正常结束
  2. 异常处理
  3. 外界干预

进程终止的原语过程

  1. 读PCB,得到状态
  2. 若在执行状态,也会被终止
  3. 有子孙进程也会被全部终止
  4. 资源回归父进程或系统
  5. 移除PCB

进程的阻塞和唤醒
分别是block 和 wakeup 必须成对使用的原语
首先block

  1. 找到被标记PCB
  2. 若为运行状态,则保护现场,状态转为阻塞状态,停止运行
  3. PCB插入等待序列

wakeup

  1. 在等待序列找到PCB
  2. 将堵塞状态变成就绪状态
  3. PCB插入就绪序列

进程的切换

  1. 保存处理机上下文,包括程序计数器和寄存器(保护进程的运行环境)
  2. 更新PCB
  3. 把程序PCB移入相依的队列。
  4. 选择另一個程序PCB
  5. 更新內存数据结构
  6. 回复处理器上下文
进程的通信

共享内存
共享一个空间
消息传递
消息放在消息队列
管道通信
这里会在GO中详细介绍

线程

书上只是简单介绍了一下线程的概念
具体看一下网上

多线程模型

多线程的实现可以分为两类:
用户级线程:
(1)用户线程由用户空间的代码创建、管理和销毁,线程的调度由用户空间的线程库完成(可能是编程语言层次的线程库),无需切换内核态,资源消耗少且高效。同一进程下创建的用户线程对CPU的竞争是以进程的维度参与的,这会导致该进程下的用户线程只能分时复用进程被分配的CPU时间片,所以无法很好利用CPU 多核运算的优势。我们一般情况下说的线程其实是指用户线程。

其中kotlin中的协程就是一个用户级的线程。因为它可以在单个线程上运行多个协程。
而且支持挂起和恢复
支持协程是我们在 Android 上进行异步编程的推荐解决方案:
轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
(但是我觉得用JAVA去反编译kotlin的源码是一件很蠢的事情,就像现在明明有jetpack包,却没有人用Kolin)

内核级线程
(2)内核线程由操作系统管理和调度,能够直接操作计算机底层的资源,线程切换的时候CPU需要切换到内核态。它能够很好利用多核CPU并行计算的优势,开发人员可以通过系统调用的方式使用内核线程。

两级线程模型
一个进程对应多个内核线程
多对多的一种关系

Go中的协程模型

协程挂起,恢复
最核心的就是挂起和恢复,他不会堵塞线程

GO中是用户级和内核级的混合复用
CPU M Machine
内核线程 P Processor
线程 G goroutine

不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。
协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用 sync 包来实现(参见第 9.3 节),不过我们很不鼓励这样做

Go 使用 channels 来同步协程

协程是轻量的,比线程更轻。它们痕迹非常不明显(使用少量的内存和资源):使用 4K 的栈内存就可以在堆中创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个地址空间中 100,000 个连续的协程)。并且它们对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。
并发和并行的的图解

协程的栈会根据需要进行伸缩,不会出现栈溢出;
协程会随着程序的结束而消亡

协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序

Go 协程意味着并行(或者可以以并行的方式部署)
Go 协程通过通道来通信;协程通过让出和恢复操作来通信

比如最简单的协程

package main

import (
	"flag"
	"fmt"
	"runtime"
	"time"
)

var numCores = flag.Int("n", 4, "number of CPU cores to use")

func initTest() {
	runtime.GOMAXPROCS(*numCores)
}

func longWait() {
	var name = "long wait"
	fmt.Printf("%s", name)
	time.Sleep(10 * 1e9)
	fmt.Printf("%s", name)
}

func shortWait() {
	var name = "short wait"
	fmt.Printf("%s", name)
	time.Sleep(5 * 1e9)
	fmt.Printf("%s", name)
}

func main() {
	initTest()
	go longWait()
	go shortWait()
	time.Sleep(20 * 1e9)
	fmt.Printf("this is end")
}

协程更有用的一个例子应该是在一个非常长的数组中查找一个元素。
将数组分割为若干个不重复的切片,然后给每一个切片启动一个协程进行查找计算。
这样许多并行的协程可以用来进行查找任务,整体的查找时间会缩短(除以协程的数量)。

如果是传统方法
遍历整个大的字符串。但这个很大的时候。就很卡了

func withoutGoroutine(text []byte) {
	//imageUrl
	var stringText = string(text)
	var result = 0
	for i := 0; i < len(stringText)-10; i++ {
		if stringText[i:i+8] == "imageUrl" {
			result = result + 1
		}
	}
	fmt.Printf("%d", result)
}

如果是用协程处理每个部分,速度会显著提高。问题在如何传入参数。表示已经找到几个

func withGoroutine(text []byte) {
	for i := 0; i < len(text)-10; i++ {
		go find(text[i : i+8])
	}
}

func find(oneSlice []byte) {
	var toString = string(oneSlice)
	if toString == "imageUrl" {
		fmt.Printf("find")
	}
}
Go中的协程调度

P操作:申请资源操作。

V操作:释放资源操作。

信号量S:用来记录资源数量,看是否能满足申请资源的操作。

Go中的协程通信

他们必须通信才会变得更有用:彼此之间发送和接收信息并且协调 / 同步他们的工作。协程可以使用共享变量来通信,但是很不提倡这样做,因为这种方式给所有的共享内存的多线程都带来了困难。

通常使用这样的格式来声明通道:

var identifier chan datatype

未初始化的通道的值是 nil。
当然可以更短: ch1 := make(chan string)。
这里我们构建一个 int 通道的通道: chanOfChans := make(chan int)。
或者函数通道:funcChan := chan func()

var chanOne = make(chan type)

通道实际上是类型化消息的队列:使数据得以传输。
它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序
(通道可以比作 Unix shells 中的双向管道(two-way pipe))

首先是简单的通信

package main

import (
	"fmt"
	"time"
)

/*
通道实际上是类型化消息的队列:使数据得以传输。
它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序
(有些人知道,通道可以比作 Unix shells 中的双向管道(two-way pipe))
*/
type chnnelValue interface {
	int | int8 | int64 | float64 | float32 | byte | string
}


func channelPutInTwo[C chnnelValue](ch chan C, putInValue []C) {
	for _, c := range putInValue {
		ch <- c
	}
}

//这样写有个问题,接受的时候很有可能不是顺序
func channelPutIn[C chnnelValue](ch chan C, putInValue C) {
	ch <- putInValue
}

func channelGetOut[C chnnelValue](ch chan C) {
	for {
		var outValue = <-ch
		time.Sleep(1e9)
		fmt.Printf("%v", outValue)
	}
}

func main() {
	/*
		既然都可以用泛型了,肯定要弄一下泛型呀,不想写类型判断
	*/
	var ArrayOne = [5]int{1, 2, 3, 4, 5}
	var myCh = make(chan int)
	for _, i2 := range ArrayOne {
		go channelPutIn(myCh, i2)
	}
	//go channelPutInTwo(myCh, ArrayOne[:])

	go channelGetOut(myCh)
	time.Sleep(20 * 1e9)
}

go 是协程。如果用for循环去开
那么不能确定谁放的数据,所以读出来不是顺序
当时如果一个协程去开数据,就是先进先出。顺序。这里用到的是泛型
这样就如果传float,就只用更改声明的类型就行了

package main

import (
	"fmt"
	"time"
)
type chnnelValue interface {
	int | int8 | int64 | float64 | float32 | byte | string
}
func channelPutInTwo[C chnnelValue](ch chan C, putInValue []C) {
	for _, c := range putInValue {
		ch <- c
	}
}
func channelPutIn[C chnnelValue](ch chan C, putInValue C) {
	ch <- putInValue
}
func channelGetOut[C chnnelValue](ch chan C) {
	for {
		var outValue = <-ch
		time.Sleep(1e9)
		fmt.Printf("%v", outValue)
	}
}
func main() {
	var ArrayOne = [5]float64{1.0, 2.0, 3.0, 4.0, 5.1111}
	var myCh = make(chan float64)
	for _, i2 := range ArrayOne {
		go channelPutIn(myCh, i2)
	}
	//go channelPutInTwo(myCh, ArrayOne[:])
	go channelGetOut(myCh)
	time.Sleep(10 * 1e9)
}

默认情况下,通信是同步且无缓冲的:在有接收者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送 / 接收操作在对方准备好之前是阻塞的:

写一个通道证明它的阻塞性,开启一个协程接收通道的数据,持续 15 秒,然后给通道放入一个值。在不同的阶段打印消息并观察输出。

package main

import (
	"fmt"
	"time"
)
type channelValueTwo interface {
	int | int8 | int64 | float64 | float32 | byte | string
}
func getOutData[C channelValueTwo](ch chan C) {
	for {
		time.Sleep(5 * 1e9)
		fmt.Printf("你输出了%v\n", <-ch)
	}
}
func scanfPutin[C channelValueTwo](ch chan C) {
	for {
		var value C
		var _, err = fmt.Scanf("%v", &value)
		fmt.Printf("你输入了%v\n", value)
		if err != nil {
			println(err)
		}
		ch <- value
	}
}

func main() {
	var mych = make(chan int)
	go scanfPutin(mych)
	go getOutData(mych)
	time.Sleep(20 * 1e9)
}

这里我有个问题。输入后它是会直接读取的,但是读的时候,占用了通道。对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:那么你输入任何数据都没有任何反应

Go中的协程通道问题
带缓冲的通道

在缓冲满载(缓冲被全部使用)之前,
给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空

如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。
如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功

要在首要位置使用无缓冲通道来设计算法,只在不确定的情况下使用缓冲。

buf := 100
ch1 := make(chan string, buf)
协程中用通道输出结果

在上面我们找imageUrl中
把结果放进chen中。然后最后进行调用
每个协程都去找,找到了就放入通道

进程的调度(只提算法)

进程的同步(主要结合go 中sync包进行说明)

死锁

死锁的概念
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值