做淘宝详情页好的网站,运行两个wordpress,西安知名网站建设公司排名,wordpress百度收录文章目录 协程#xff08;goroutine#xff09;基本介绍GMP模型协程间共享变量 通道#xff08;channel#xff09;基本介绍channel的定义方式channel的读写channel的关闭channel的遍历方式只读/只写channelchannel最佳案例select语句 协程#xff08;goroutine#xff0… 文章目录 协程goroutine基本介绍GMP模型协程间共享变量 通道channel基本介绍channel的定义方式channel的读写channel的关闭channel的遍历方式只读/只写channelchannel最佳案例select语句 协程goroutine
基本介绍 基本介绍 进程、线程与协程
进程Process是计算机中正在运行的程序的实例是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的地址空间、代码、数据和文件资源。进程之间相互独立通过进程间通信机制进行数据交换和协作。进程的创建、销毁以及切换都由操作系统自动完成开销较大。线程Thread是操作系统调度的最小执行单元是进程内的一个执行路径。线程与进程共享同一地址空间和大部分资源包括代码段、数据段和打开的文件等。线程之间通常借助互斥锁、条件变量以及信号量等进行数据交换。线程的创建、销毁以及切换的开销较小但需要注意线程间的同步和共享资源的管理。协程Coroutine协程是一种轻量级的并发执行单元通常由编程语言本身的运行时系统进行调度和管理。协程通常在一个线程内执行共享相同的地址空间和资源。协程间通常通过通道Channel实现数据交换和协作。协程的创建、销毁以及切换都由运行时系统自动完成开销非常小可以创建成千上万个协程而不会导致系统负载过高。
并发与并行
并发Concurrency指的是在单个处理器上以时间片轮转的方式交替执行多个任务使得在一段时间内这多个任务都得以推进但实际在一个时间点只有一个任务在执行。并行Parallelism指的是多个任务同时在不同的处理器上执行使得这多个任务同时得以推进并且在一个时间点来看也是多个任务在同时执行。
在Go中通过在函数或方法的调用前加上go关键字即可创建一个go协程并让其运行对应的函数或方法。如下
package mainimport (fmttime
)func Print() bool {for i : 0; i 10; i {fmt.Printf(Print: hello goroutine...%d\n, i1)time.Sleep(time.Second)}return true
}func main() {go Print() // 创建go协程for i : 0; i 5; i {fmt.Printf(main: hello goroutine...%d\n, i1)time.Sleep(time.Second)}
}在上述代码中主协程创建了一个新协程用于执行Print函数主协程进行5次打印后退出新协程进行10次打印后退出。运行结果如下 说明一下
在Go中当程序启动时会自动创建一个主协程来执行main函数该协程与其他新创建的协程没有本质的区别但主协程执行完毕后整个程序会退出即使其他协程还未执行完毕也会跟着退出。如果一个协程在执行过程中触发了panic异常但没有对其进行捕获那么会导致整个程序崩溃因此在协程中也需要通过recover函数对panic进行捕获。
GMP模型 常规的协程Coroutine 线程是在内核态视角下的最小执行单元而协程是在线程的基础上在用户态视角下进行二次开发得到的更小的执行单元。常规的协程Coroutine通常是与一个线程强绑定的而一个线程可以绑定多个协程。如下 说明一下
由于常规的协程是与一个线程强绑定的因此绑定于同一线程的多个协程只能做到并发无法做到并行。当一个协程因为某些原因陷入阻塞那么这个阻塞会直接上升到对应的线程最终导致整个协程组陷入阻塞。 Go中的协程Goroutine Go语言中的协程Goroutine与常规的协程Coroutine的实现方式有所不同Go中的协程不是与一个线程强绑定的而是由Go调度器动态的将协程绑定到可用的线程上执行。如下 说明一下
由于Go协程与线程之间的绑定是动态的因此各个协程之间既能做到并发也能做到并行。当一个Go协程因为某些原因陷入阻塞那么Go调度器会将该协程与其绑定的线程进行解绑将线程的资源释放出来使得线程可以与其他可调度的协程进行绑定。 GMP模型 GMPGoroutine-Machine-Processor模型是Go运行时系统中用于实现并发执行的模型负责管理和调度协程的执行。G、M和P的含义分别如下
GGoroutine代表Go中的协程每个G都有自己的运行栈、状态以及执行的任务函数。MMachine代表Go中的线程M不直接执行G而是先和P绑定由P来指定M所需执行的G。PProcessor代表Go中的调度器P实现G和M之间的动态有机结合。对于G而言P就是其CPUG只有被P调度才得以执行对于M而言P是其执行代理为其指定可执行的G。
GMP模型示意图如下 上图说明
全局有多个M和多个P但M和P的数量不一定是相同的。每个M在调度G之前需要先和P进行绑定不是强绑定每个M调度的G由其对应的P指定。M无需记录所调度的G的状态信息因此G在全生命周期中可以实现跨M执行。在GMP模型中有三种队列来存放G分别是全局队列、P的本地队列和wait队列用于存放io阻塞就绪态的G图中未展示。每个P都有一个对应本地队列访问本地队列时可以接近无锁化。当P为M获取可调度的G时会优先从自己的本地队列中进行获取其次从全局队列中获取最后从wait队列中获取。如果一个G在调度过程中新创建了一个G那么这个新G会优先投递到当前P的本地队列中如果本地队列已满则投递到全局队列中。
调度器P获取可调度的G的流程如下
优先尝试从当前P的本地队列获取可调度的G。尝试从全局队列获取可调度的G。尝试从wait队列获取io阻塞就绪的G。尝试从其他P的本地队列窃取一半的G补充到当前P的本地队列防止不同P的闲忙差异过大work-stealing机制。
说明一下
由于存在work-stealing机制因此P的本地队列的访问也不是完全无锁的只能说接近无锁化。上述说到的只是获取可调度的G的主要流程实际实现时还有更多的细节。比如P每进行61次调度后会先尝试从全局队列中获取一个G进行调度避免造成全局队列中的G的饥饿问题。 GOMAXPROCS 在GMP模型中G只有被P调度才得以执行因此P的数量决定了G的最大并行数量。通过runtime包中的GOMAXPROCS函数可以获取和设置P的数量。如下
package mainimport (fmtruntime
)func main() {cpuNum : runtime.NumCPU() // 获取本地机器的逻辑CPU数fmt.Printf(cpuNum %d\n, cpuNum) // cpuNum 6runtime.GOMAXPROCS(4) // 设置可同时执行的最大CPU数num : runtime.GOMAXPROCS(0) // 获取可同时执行的最大CPU数fmt.Printf(num %d\n, num) // num 4
}说明一下
runtime包中的NumCPU函数用于获取本地机器的逻辑CPU数。runtime包中的GOMAXPROCS函数用于设置可同时执行的最大CPU数并返回先前的设置。如果设置的值小于1则不会更改当前的值设置的值超过CPU核数无意义。从Go1.5开始GOMAXPROCS默认设置为CPU的核数并且可以根据需要自动调整并发执行的并行度无需再手动设置。 协程的生命周期 Go中协程的生命周期大致由如下几种状态组成
_Gidle表示该协程刚刚创建但还未进行初始化。_Gdead表示该协程已经完成初始化但还未被使用。_Grunnable表示该协程已经被放入运行队列但还未被调度。_Grunning表示该协程正在被调度。_Gsyscall表示该协程正在执行系统调用。_Gwaiting表示该协程处于挂起状态需要等待被唤醒。_Gdead表示该协程刚刚执行完毕。
状态转换如下 说明一下
当协程在调度过程中执行到系统调用代码时其状态就会由_Grunning切换为_Gsyscall并在系统调用结束后根据实际情况恢复为_Grunning或_Grunnable状态。协程在调度过程中可能因为某些原因而陷入阻塞比如等待锁资源就绪或等待channel条件就绪等这是协程的状态会由_Grunning切换为_Gwaiting并在协程被唤醒后恢复为_Grunnable状态。除了上述常见的协程状态外协程还有一些其他的状态比如_Gcopystack表示该协程正处于栈扩容流程中Go协程的栈空间大小可动态扩缩_Greempted表示协程被抢占后的状态。 协程的调度流程 GMP模型中存在三种类型的协程
普通的g用户通过go关键字创建的协程也就是GMP模型中需要被调度的G。g0特殊的调度协程每个M都有一个g0其主要负责对普通的g进行运行调度。monitor g全局监控协程monitor g会越过P直接与一个M进行绑定不断轮询对所有P的执行状况进行监控如果发现满足抢占调度的条件则会从第三方的角度出手干预主动发起抢占调度。
在创建M时Go运行时系统会为每个M初始化一个g0g0的调度流程如下
找到一个可被调度执行的G。将这个G的状态切换为_Grunning并通过调用gogo函数将执行权交给G。执行G的代码逻辑直到某些条件达成使得调度结束。G调度结束后通过调用mcall函数将执行权交还给g0并更新G的状态。
示意图如下 调度类型 GMP模型中的调度类型大致可分为如下四类
主动调度用户通过调用runtime包中的Gosched函数可以让当前G主动让出执行权并将其投递到全局队列中等待下一次调度。被动调度G在调度过程中因为某些原因而陷入阻塞而导致调度终止比如等待锁资源就绪或等待channel条件就绪等。正常调度G的代码逻辑被正常执行完毕调度终止。抢占调度在G执行系统调用的情况下如果满足了抢占调度的条件那么monitor g会强行将当前的P和M进行解绑让解绑后的P重新寻找一个空闲的M进行绑定进而可以继续调度其他的G而解绑后M则继续执行系统调用。
触发前三种调度类型中的任意一种都会导致当前G的调度终止此时M的执行权将由普通的g交还给g0。示意图如下 上图说明
g0在调度普通的g时会先通过findRunnable函数找到一个可被调度的G然后通过execute函数更新对应G和P的状态信息最后通过gogo函数将执行权交给G进行G的调度。G在调度过程中如果因为主动调度、被动调度或正常调度导致调度终止那么会先调用mcall函数将执行权交还给g0然后通过调用对应的函数更新G的状态信息并完成G和M解绑等操作然后开启新一轮的调度。gosched_m函数对应的是主动调度该函数会先将G的状态由_Grunning切换为_Grunnable然后将G和M解绑并将其投递到全局队列中最后开启新一轮的调度。park_m函数对应的是被动调度该函数会先将G的状态由_Grunning切换为_Gwaiting然后将G和M解绑最后开启新一轮的调度。goexit0函数对应的是正常调度该函数会先将G的状态由_Grunning切换为_Gdead然后将G和M解绑最后开启新一轮的调度。
关于被动调度
当因被动调度陷入阻塞的G对应的条件就绪时会由导致条件就绪的G执行goready函数将其唤醒唤醒时会先将G的状态由_Gwaiting切换为_Grunnable然后将其添加到唤醒者的P的本地队列中。比如某个G在申请锁时由于锁资源不就绪而陷入阻塞此时这个G会被放在锁对应的资源等待队列中当另一个持有锁的G在被调度的过程中执行释放锁操作时就会执行goready函数唤醒该锁对应的资源等待队列中的G并将其添加到自己的P的本地队列中。在调度唤醒者时M的执行权在普通的g手中而被唤醒者的状态切换操作以及G的投递操作需要由g0执行因此在goready函数中会先将执行权交还给g0并在执行唤醒操作后再重新获得执行权这里的执行权交接是通过systemstack函数完成的。goready函数在将唤醒的G添加到唤醒者的P的本地队列中时如果P的本地队列已满则会将唤醒的G以及P的本地队列中一半的G放回到全局队列中帮助当前的P缓解执行压力。
关于抢占调度
在G需要执行系统调用之前会先调用reentersyscall函数保存当前G的执行环境并将G和P的状态更新为对应的系统调用状态最后解除P和当前M之间的绑定因为M即将进入系统调用而导致短暂不可用。与M解除绑定关系的P会被添加到当前M的oldp容器中后续M执行完系统调用后会优先寻找该P重新建立绑定关系。在G执行系统调用期间如果P的本地队列不为空或者当前没有空闲的M和P或者G执行系统调用的时间超过10ms则monitor g会将当前M的oldp容器中的P的状态置为空闲并让其与其他空闲的M也可能新创建一个M进行绑定进而可以继续调度其他的G而当前的M仍然继续执行系统调用。当M执行完系统调用后会通过exitsyscall函数尝试寻找P进行绑定。如果此时M的oldp容器中的P仍然可用则重新与该P建立绑定关系并将G的状态重新置为_Grunning继续执行后续的代码逻辑。如果原先的P已经不可用则将G的状态置为_Grunnable并解除G和M的绑定关系尝试从全局P队列中寻找一个可用的P进行绑定如果找到了则在绑定对应的P后继续调度该G否则将该G投递到全局队列并让当前的M陷入沉睡直到被唤醒后再继续发起调度。
协程间共享变量 协程间共享变量 在协程之间共享变量是常见的需求以便协程之间能够进行数据交换和协同工作。为了保证共享资源的并发安全通常需要引入互斥锁对共享资源进行保护。
例如下面程序中启动了4个协程进行抢票在抢票过程中需要并发访问全局变量tickets代码中通过加锁的方式保证了tickets变量的并发安全。如下
package mainimport (fmtsynctime
)var (tickets 1000 // 共享资源mtx sync.Mutex // 互斥锁
)func ByTicket(id int) {for {mtx.Lock() // 加锁if tickets 0 {mtx.Unlock() // 解锁break}time.Sleep(time.Microsecond) // 模拟抢票过程的耗时tickets--fmt.Printf(goroutine %d get a ticket, tickets %d\n, id, tickets)mtx.Unlock() // 解锁}
}func main() {// 启动4个协程进行抢票for i : 0; i 4; i {go ByTicket(i)}for {if tickets 0 {break}}fmt.Printf(tickets sold out...tickets %d\n, tickets)
}说明一下
Mutex是sync包中的互斥锁类型用于保护共享资源的并发访问该类型提供了Lock和Unlock两个方法分别用于加锁和解锁。
通道channel
基本介绍 基本介绍 通道channel是Go中用于协程间通信和数据交换的机制其提供了一种安全、同步和高效的方式来传递数据以实现协程之间的通信和协同工作。channel本质是一个队列遵守先进先出FIFO的原则。channel本身是线程安全的多协程可以通过channel直接发送和接收数据显式的加锁解锁操作。
channel的示意图如下 channel的定义方式 channel的定义方式 在定义channel时通过make创建指定类型以及容量的channel。如下
package mainimport (fmtunsafe
)func main() {// make channelvar intChan make(chan int, 10)fmt.Printf(intChan type %T\n, intChan) // intChan type chan intfmt.Printf(intChan len %d\n, len(intChan)) // intChan len 0fmt.Printf(intChan cap %d\n, cap(intChan)) // intChan cap 10fmt.Printf(intChan size %d\n, unsafe.Sizeof(intChan)) // intChan size 8
}说明一下
channel是引用类型其定义后需要先通过make函数分配内存空间然后才能使用。在使用make函数为channel分配内存空间时其第一个参数表示channel的类型第二个参数表示channel的容量第二个参数若省略则默认为0。通过len函数可以获取channel中元素的数量通过cap函数可以获取channel的容量。channel中仅包含一个指向底层队列的指针属于引用类型因此channel类型变量的大小为8字节。channel中只能存放对应类型的数据如果想让channel存放任意类型的数据可以指定channel中存放的元素类型为interface{}。
channel的读写 channel的读写 channel的读写
通过channel - data的方式向channel中写入数据在写入数据时如果channel已满则写操作会被阻塞直到channel中有数据被读走再执行写操作。通过data : -channel的方式从channel中读取数据在读取数据时如果channel为空则读操作会被阻塞直到有数据写入channel中再执行读操作。
例如下面程序中定义了一个容量为5的channel并启动了一个协程不断向该channel中写入数据而在主协程中每隔1秒从该channel中读取一次数据。如下
package mainimport (fmttime
)func WriteNum(intChan chan int) {num : 0for {intChan - num // 向channel中写入数据fmt.Printf(write a num: %d\n, num)num}
}func ReadNum(intChan chan int) {for {time.Sleep(time.Second)num : -intChan // 从channel中读取数据fmt.Printf(read a num: %d\n, num)}
}func main() {intChan : make(chan int, 5)go WriteNum(intChan)ReadNum(intChan)
}在上述代码中由于向channel中写入数据的过程中没有进行任何休眠操作因此程序运行后channel立马被写满了此时对channel的写操作将会被阻塞直到channel中的数据被主协程读走才能再次执行写操作因此后续对channel的写操作也被同步为每秒一次。程序运行结果如下 说明一下
将channel的容量指定为0意味着channel中不能存储任何数据此时该channel将成为一个无缓冲通道。对无缓冲通道的写操作将会被阻塞直到有另一个协程准备对channel进行读操作反之亦然因此无缓冲通道是一种强制同步的机制。
channel的关闭 channel的关闭 在Go中通过内建函数close可以关闭指定的channelchannel关闭后不能再对其进行写操作否则会触发panic异常但仍可以从该channel中读取数据。如下
package mainimport fmtfunc main() {charChan : make(chan int, 10)for i : 0; i 10; i {charChan - a i}close(charChan) // 关闭channelfor {ch, ok : -charChanif !ok {break}fmt.Printf(read a char: %c\n, ch)}
}运行程序后可以看到channel虽然被关闭了但仍然可以读取channel中的数据。如下 说明一下
通过-channel的方式读取channel中的数据将会得到两个值第一个值是从channel中读取到的数据第二个值表示本次对channel进行的读操作是否成功如果channel已关闭并且channel中没有数据可读那么第二个值将会返回false否则为true。
channel的遍历方式 channel的遍历方式 在Go中可以通过for range循环的方式对channel中的元素进行遍历其特点如下
for range循环的每次迭代会从channel中读取一个数据并将该值赋给指定的变量。如果channel中没有数据可读取for range会阻塞等待直到有数据可读或channel关闭。channel被关闭后for range可以继续从channel中读取数据当所有数据都被读取后会自动结束迭代。
使用案例如下
package mainimport fmtfunc main() {charChan : make(chan int, 10)for i : 0; i 10; i {charChan - a i}close(charChan) // 关闭channelfor value : range charChan {fmt.Printf(read a char: %c\n, value)}
}说明一下
在对channel进行读操作时要确保有协程会对channel进行对应的写操作否则会造成死锁deadlock。如果去掉上述代码中关闭channel的操作那么for range循环在读取完channel中的数据后不会自动结束迭代而会继续进行读操作但此时没有任何协程会再对该channel进行写操作因此会造成死锁deadlock。
只读/只写channel 只读/只写channel 在Go中通过-chan type和chan- type的方式可以将channel声明为只读或只写。如下
package mainimport (fmttime
)func WriteNum(intChan chan- int) { // 只写channelnum : 0for {intChan - num // 向channel中写入数据fmt.Printf(write a num: %d\n, num)num}
}func ReadNum(intChan -chan int) { // 只读channelfor {time.Sleep(time.Second)num : -intChan // 从channel中读取数据fmt.Printf(read a num: %d\n, num)}
}func main() {intChan : make(chan int, 5)go WriteNum(intChan)ReadNum(intChan)
}说明一下
对只读的channel进行写操作或对只写的channel进行读操作都会产生报错。由于WriteNum函数中只会对intChan进行写操作而ReadNum函数中只会对intChan进行读操作这时为了避免误操作可以分别将WriteNum和ReadNum函数的intChan参数声明为只写和只读的channel。
channel最佳案例 题目要求统计1-300000中有多少个素数 为了快速统计出素数的个数使用多个Go协程并发进行素数判断具体的解决思路如下
启动一个生产者协程负责将1-300000的数字写入到intChan中作为数据源。启动多个消费者协程负责从intChan中读取数据进行素数判断并将素数写入到primeChan中。主协程负责不断读取primeChan中的数据统计素数的个数。
为了让主协程能够判断primeChan中的素数是否已经读取完毕需要借助一个exitChan
生产者协程在生产完数据后关闭intChan使得各个消费者协程能够判断intChan中的数据是否消费完毕并在数据消费完毕后写入一个结束标志到exitChan中。启动一个匿名协程负责从exitChan中读取结束标志当读取到的结束标志个数等于消费者的个数时表明所有消费者协程已经退出这时关闭primeChan和exitChan。当primeChan被关闭并且primeChan中的数据已经读取完时则说明所有素数已经统计完毕。
示意图如下 代码如下
package mainimport fmtfunc Producer(numChan chan- int) {for num : 1; num 300000; num {numChan - num}close(numChan)
}func IsPrime(num int) bool {for i : 2; i num-1; i {if num%i 0 {return false}}return true
}func Consumer(numChan -chan int, primeChan chan- int, exitChan chan- bool) {for {num, ok : -numChanif !ok {break}if IsPrime(num) {primeChan - num}}exitChan - true
}func main() {numChan : make(chan int, 300000)primeChan : make(chan int, 300000)exitChan : make(chan bool, 6)// 生产者协程go Producer(numChan)// 消费者协程for i : 0; i 6; i {go Consumer(numChan, primeChan, exitChan)}// 匿名协程go func() {for i : 0; i 6; i {-exitChan}close(primeChan)close(exitChan)}()// 主协程count : 0for {_, ok : -primeChanif !ok {break}count}fmt.Printf(prime count %d\n, count) // prime count 25998
}select语句 select语句 在Go中select语句用于实现非阻塞的通信。其特点如下
select语句可以同时监听多个channel的操作它会选择一个已经就绪的操作并执行相应的分支代码。如果有多个操作就绪select语句会随机选择其中一个操作执行如果没有操作就绪则会执行default分支。
使用案例如下
package mainimport fmtfunc main() {intChan : make(chan int, 10)stringChan : make(chan string, 10)for i : 0; i 10; i {intChan - istringChan - fmt.Sprintf(hello select%d, i)}label:for {select {case num : -intChan:fmt.Printf(read intChan: %d\n, num)case str : -stringChan:fmt.Printf(read stringChan: %s\n, str)default:fmt.Printf(no data now...\n)break label}}
}运行代码后可以看到当intChan和stringChan中都有数据时select语句会随机对一个channel进行读操作并在两个channel中的数据都被读取完后通过执行default分支中的break语句跳出for循环。运行结果如下