一.并发
并发意味着程序在运行时有多个执行上下文,对应着多个调用栈。每一个进程在运行时,都有自己的调用栈和堆,有一个完整的上下文,而操作系统在调度进程的时候,会保存被调度进程的上下文环境,等该进程获得时间片后,再恢复该进程的上下文到系统中。从整个操作系统层面来说,多个进程是可以并发的。
二.协程、进程、线程及goruntine
协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度
协程的概念并不是与线程对应的,而是和函数调用 call/return对应(eg:golang中把goruntine当作一个以函数为单位的执行单元)。它们的区别在于协程允许一个函数有多个入口、出口(逻辑上的),并且在切换到另一个函数执行时,允许使用一个新的context(包括调用栈)。正是有了这个机制基础,再加上CPU支持了保护模式,操作系统就可以接着实现进程、线程了。
进程可看作为分配资源的基本单位,比如new出了一块内存,就是操作系统将一块物理内存映射到我们的进程地址空间上(进程创建必须分配一个完整的独立地址空间),这块内存就属于这个进程,进程内的所有线程都可以访问这块内存,其他进程就访问不了,其他类型的资源也是同理。所以进程是分配资源的基本单位,也是我们说的隔离。线程作为独立运行和独立调度的基本单位,进而我们可以认为线程是进程的一个执行流,独立执行它自己的程序代码。线程上下文一般只包含CPU上下文及其他的线程管理信息,线程创建的开销主要取决于为线程堆栈的建立而分配内存的开销,这些开销并不大。线程还分为系统级别和用户级线程,用户级别线程对引起阻塞的系统调用的调用会立即阻塞该线程所属的整个进程,而内核实现线程则会导致线程上下文切换的开销跟进程一样大,所以经常的折衷的方法是轻量级进程(Lightweight)。在 Linux 中,一个线程组基本上就是实现了多线程应用的一组轻量级进程。线程的作用就在于充分使用硬件CPU,也就是我们说的并行。
我们一般将协程理解为用户态轻量级线程,是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户的程序自己调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的CPU控制权切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
以上说的协程和golang中的协程是不一样的。一个是Coroutine,另一个是goruntine,是不一样的。golang语言作者也说,“Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine“。 Go 协程意味着并行,协程一般来说不是这样的;
Go 协程通过通道来通信而协程通过让出和恢复操作来通信;而且Go 协程比协程更强大。因为Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,Golang 有自己的调度器,工作方式基本上是协作式,而不是抢占式,但也不是完全的协作式调度,比如在系统调用的函数入口处会有抢占。当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU 转让出去,让其他 goroutine 能被调度并执行,也就是说 Golang 从语言层面支持了协程。简单的说就是golang自己实现了协程并叫做goruntine。
goruntine的优势在于并行和非常低的资源使用,体现在内存消耗方面和切换(调度)开销方面,每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少,只有2KB,而线程则需要8MB;线程切换涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等;而goroutine 只有三个寄存器的值修改 - PC(程序计数器) / SP(堆栈指针) / DX(数据寄存器)。
二. go func 并发
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
goroutine 语法格式:
go 函数名( 参数列表 )
代码实例:
执行以上代码,输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行
三. 通道(channel)
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
- chan只有一种运算符<-,放在chan前面就是从chan接收数据,放在chan后面就是向chan发送数据,没有->运算符;
- go语言内建函数make,close用于创建和删除chan,内建函数len和cap用于获取chan的缓冲数据数量和缓冲总容量
ch <- v //把v发送给通道
v:= <-ch. //从ch接受数据并把数据赋值给v
声明一个通道,通道在使用前必须先创建: ch := make(chan int)
注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
实例:
执行以上代码,输出的结果是没有固定先后顺序。因为它们是4个 goroutine 在执行,使用通道发送数据
三. 通道(channel)的一些小例子
go chan报错:fatal error: all goroutines are asleep - deadlock!
示例代码:
执行结果:
上述问题解决方案:
看上面main函数中,先开了一个通道channel c,然后马上对这个c内写入数据。
然后调协程test3在c中取数据,结果运行时报错:
fatal error: all goroutines are asleep - deadlock!
这究竟是为啥子呢?
首先我们这里通过make(chan int),开辟的通道是一种无缓冲通道,所以当对这个缓冲通道写的时候,会一直阻塞等到某个协程对这个缓冲通道读(这个与典型的生产者消费者有点不一样,当队列中“内容”已经满了,生产者再生往里放东西才会阻塞,而这里我讲c<-'A’理解为生产,他却是需要等到某个协程读了再能继续运行)。
main函数的执行在go语言中本身就是一个协程的执行,所以在执行到c<-'A’的时候,执行main函数的协程将被阻塞,也就是说main函数被阻塞了,此时不能在继续往下执行了,所以go test3()这个函数无法执行到了,就无法读到c中的内容了,所以整个程序阻塞,发生了死锁。
我们该怎么解决这个问题呢?我们阻塞的原因是main函数不能继续向下执行了,所以go test3执行不到,所以那我们在main函数中另开一个协程,让他对c进行写(对c进行写的过程不在main函数对应的协程中做),修改后的代码:



