每个进程只有一个执行上下文,一个调用栈一个堆,操作系统在调度进程时,会保存被调度进程的上下文环境,等待该进程获得时间片后,再恢复进程上下文。
并发能更客观的表现问题模型 (图形界面与后台处理)
并发能充分利用CPU核心的优势,提高程序的执行效率
并发能充分利用CPU与其他硬件设备固有的异步性(中断触发)
多进程,操作系统层面的并发,开销最大的方式。好处是简单,进程间互不影响,坏处是开销大,所有进程都是由内核管理的
多线程,在大部分操作系统上都属于系统层面的并发,最有效,开销比进程小,在高并发模式下,效率会有影响
基于回调的非阻塞/异步IO,高并发情况下多线程会很快耗尽服务器的内存和CPU,通过事件驱动的方式使系统运转,坏处是编程复杂,把流程做了分割,对问题本身的反应不够自然
协程,本质上是用户态线程,不需要操作系统进行抢占式调度,而在真正实现中寄存于线程中,因此系统开销极小。提高任务的并发性,避免多线程缺点。优点是编程简单,结构清晰,缺点是需要语言支持。
内存共享系统,线程之间通过共享内存的方式,加锁
消息传递系统,线程之间通过消息通信,发送消息时对状态进行复制,并且在消息传递的边界上交出这个状态的所有权。由于复制,性能并不优越。
协程可以轻松创建上百万而不会导致系统资源衰竭,线程或进程最多不能超过1万
多数语言只提供轻量级线程创建,销毁和切换能力,任何同步IO操作都会阻塞并发执行的轻量级线程
go语言的goroutine,在任何系统调用时都会出让CPU给其他goroutine
定义一个函数,通过go关键字调用,这次调用就会在一个新的gotoutine中并发执行,当被调用的函数返回时,这个goroutine自动结束,如果这个函数有返回值,这个返回值会被丢弃
工程上,有两种常见的并发通信模型:共享数据和消息
设计上遵循的通信原则:不要通过共享来通信,而是通过通信来共享
go语言可以支持共享内存和锁,但是他提供消息机制
消息机制认为每个并发单元是字包含的独立的个体,并且都有自己的变量,不同并发单元不共享变量,每个并发单元的输入和输出只有一种就是消息
channel是goroutine间的通信方式
channel是进城内通信方式,通过channel传递对象过程和调用函数时传递参数行为比较一致
channel是类型相关,一个channel职能传递一种类型,需要在声明channel时指定
通过ch<-把值写入channel,这个写入操作在<-ch在读取之前是阻塞的,这样就可以确保所有channel执行完毕之后才返回。
var chanName chan ElementType ElementType是指channel传递元素的类型
var ch chan int int类型的channel
var m map[string] chan bool 声明一个map,元素是bool型的channel
ch := make(chan int) 声明并初始化一个int型的channel
ch <- value 写入,向channel写入数据会导致程序阻塞,直到有其他goroutine从这个channel读取数据
value := <- ch 从channel读取数据,如果之前没有写入数据也会阻塞
select用法和switch相似,每个选择条件用case语句描述,case的每一条语句里必须是一个IO操作
如果多个case都满足条件,选取哪个先执行是随机的
dafault,当监听的channel都没有准备好时,默认执行的,select不再阻塞等待channel
可以指定channel的缓冲区大小,通过make(chan int, 1024)第二个参数
缓冲写满之前,不会阻塞
读取和常规非缓冲channel相同,但可以使用for和range来读取
for i := range c {
........
}
不能永久等待,否则可能出现死锁
go语言没有直接提供超时处理机制,但可以利用select机制
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9)
timeout <- true
}()
select {
case <- ch:
case <- timeout:
// 一直没有从ch中读取到数据,但从timeout中读取到数据
}
这是在go语言中避免channel通信超时最有效办法
go语言中channel是原生类型,自身也可以通过channel传递
利用这个特定来实现管道:
type PipeData struct {
value int
handler func(int) int
next chan int
}
func handle(queue chan *PipeData) {
for data := range queue {
data.next <- data.handler(data.value)
}
}
将一个chanel变量来那个传递给一个函数时,可以通过将其指定为单向channel,从而限制该函数中对此channel的操作
var ch1 chan int 常规channel
var ch2 char<- float64 单向写channel
var ch3 <-cha int 单向读channel
channel初始化和类型转换
ch4 := makee(chan int)
ch5 := <-chan int(ch4)
ch6 := chan<- int(ch4)
单向channel的意义和c中的const类似,最小权限原则,避免没必要的使用泛滥问题
close(ch)
通过x, ok := <- ch来判断一个ch是否已经关闭
应该在生产者的地方关闭channel,而不是消费者,否则容易引起panic
当前版本的go编译器还不能智能的发现和利用多核的优势,有可能gotoutine都运行在一个cpu上,一个goroutine执行时,其他的goroutine等待
go语言升级之前,可以设置环境变量GOMAXPROCS来控制使用多少个CPU
或者在代码中启动goroutine之前通过runtime.GOMAXPROCS(CPUNUM)来设置
runtime包还提供了NumCPU来获取核心数
每个goroutine可以主动出让时间片,通过runtime包的函数Gosched()
Goexit,退出当前goroutine,但是defer还会继续调用
即使成功使用了channel有时也难以避免在goroutine之间共享数据
同步锁
sync.Mutex和sync.RWMutex
Lock()和RLock() 对应的Unlock()和RUnlock()
例子
var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock()
}
全局唯一性操作
Once类型来保证全局唯一性操作
func setup() {
}
var once sync.Once
once.Do(setup)
Do方法可以保证在全局范围内只调用指定函数一次,而且其他goroutine在调用到此语句时,将会先备阻塞,直至全局唯一的调用结束
sync包包含一个atomic子包,提供一些基础数据类型的原子操作函数
func CompareAndSwapUint64(val *uint64, old, new uint64)(swapped bool)