赞
踩
sync包提供了基本的同步基元
,如互斥锁。除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些
。本包的类型的值不应被拷贝。
Locker接口提供了加锁和解锁的方法,对于可以加锁和解锁的对象需要实现这个接口。在sync包中有Mutex互斥锁和RWMutex读写锁实现了这个接口
。
package main import ( "fmt" "sync" ) func main() { var once sync.Once // Once对象 // 准备好sync.Once对象要执行的函数 onceBody := func() { fmt.Println("Only once") } done := make(chan bool) // 信道 // 开启10条协程 for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } // 主线程 for i := 0; i < 10; i++ { <-done fmt.Printf("Main --- %d\n", i + 1) } }
package main import ( "fmt" "sync" ) // 互斥锁 var mutex sync.Mutex func main() { // 使用互斥实现两个协程的交替打印 ctrl := make(chan bool, 1) // 信道 // 开启协程 go func(printChan <-chan bool) { for { if len(printChan) == 1 { mutex.Lock() // 加锁 <-printChan fmt.Println("gorountine...Print") mutex.Unlock() // 解锁 } } }(ctrl) // 主线程 for { if len(ctrl) == 0 { mutex.Lock() // 加锁 ctrl <- true fmt.Println("Main...Print") mutex.Unlock() // 解锁 } } }
package main import ( "fmt" "sync" "time" ) // 读写锁 var rwMutex sync.RWMutex // 全局资源 var SHARE int = 100 func main() { // 协程 go func() { for i := 1; i < 99999999999; i++ { rwMutex.Lock() // 这种加锁方式将会禁止其他线程进入 fmt.Printf("goruntine Log --> %d\n", SHARE) time.Sleep(time.Second * 2) // 睡眠时间到了之后才会去释放锁,其他线程才能拿到锁 rwMutex.Unlock() } }() // 主线程 for i := 1; i < 99999999999; i++ { rwMutex.Lock() fmt.Printf("Main Log --> %d\n", SHARE) rwMutex.Unlock() } }
package main import ( "fmt" "sync" "time" ) // 读写锁 var rwMutex sync.RWMutex // 全局资源 var SHARE int = 100 func main() { // 协程 go func() { for i := 1; i < 99999999999; i++ { rwMutex.RLock() // 这种加锁方式可以让其他线程拿到读锁 fmt.Printf("goruntine Log --> %d\n", SHARE) time.Sleep(time.Second * 2) // 加锁之后未解锁,此处睡眠两秒,其他线程仍然可以拿到这个锁,继续执行 rwMutex.RUnlock() } }() // 主线程 for i := 1; i < 99999999999; i++ { rwMutex.RLock() fmt.Printf("Main Log --> %d\n", SHARE) rwMutex.RUnlock() } }
package main import ( "fmt" "sync" "time" ) func main() { signal := 1 mutex := sync.Mutex{} cond := sync.NewCond(&mutex) for i := 0; i < 10; i++ { go func(int1 int) { cond.L.Lock() for signal == 1 { fmt.Printf("线程%d开始等待\n",int1 + 1) cond.Wait() // 等待通知 fmt.Printf("线程%d结束\n",int1 + 1) } cond.L.Unlock() }(i) } // 睡眠300毫秒,先让所有协程都进入for循环 time.Sleep(300 * time.Millisecond) //cond.Broadcast() //通知所有的线程,所有等待中的协程都会继续执行,不再等待 cond.Signal() // 只会通知一个线程,其他线程还会处于等待中 signal = 0 // 控制协程执行一次循环 // 等待一段时间,防止主线程执行完导致协程提前结束 time.Sleep(300 * time.Millisecond) }
package main import ( "fmt" //"fmt" //"net/http" "sync" //"time" ) func main() { var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } for _, url := range urls { // Increment the WaitGroup counter. wg.Add(1) // Launch a goroutine to fetch the URL. go func(url string) { // Decrement the counter when the goroutine completes. defer wg.Done() // Print the URL. fmt.Println(url) }(url) } // Wait for all HTTP fetches to complete. wg.Wait() }
串行、并发与并行
概念
串行
:我们都是先读小学,小学毕业后再读初中,读完初中再读高中。
并发
:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。
并行
:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。
进程、线程和协程
概念
进程
(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程
(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。
协程
(coroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。
并发模型
概念
业界将如何实现并发编程总结归纳为各式各样的并发模型,常见的并发模型有以下几种:
线程&锁模型 Actor模型 CSP模型 Fork&Join模型
Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式。
Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
Goroutine 是 Go
程序中最基本的并发执行单元
。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了,就是这么简单粗暴。
Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine
,从而让该函数或方法在新创建的 goroutine 中执行。
func run() {
XXX
}
go run()
匿名函数也支持使用go关键字创建 goroutine 去执行。
go func() {
}
一个 goroutine 必定对应一个函数/方法,可以创建多个 goroutine 去执行相同的函数/方法。
其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine
。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数
,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”
。
package main import ( "fmt" "time" ) func hello() { fmt.Println("hello") } func main() { go hello() fmt.Println("你好") // 防止主线程执行太快而结束导致协程未能执行。 time.Sleep(time.Second) }
上面使用的睡眠方式来处理显然是不合理的,可以使用sync包中的WaitGroup
来处理,改写如下:
package main import ( "fmt" "sync" ) // 声明全局等待组变量 var wg sync.WaitGroup func hello() { defer wg.Done() // 告知当前goroutine完成 fmt.Println("hello") } func main() { wg.Add(1) // 登记1个goroutine go hello() fmt.Println("你好") wg.Wait() // 阻塞等待登记的goroutine完成 }
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("hello", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 }
操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB
),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小
, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
操作系统内核在调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu周期。
区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。
在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型
。
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS 默认为 8
。Go语言中可以通过runtime.GOMAXPROCS函数设置当前程序并发时占用的 CPU逻辑核心数
。(Go1.5版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU 逻辑核心数
。)
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题
。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁
,这种做法势必造成性能问题。
Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信
。
如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制
。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则
,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel是一种引用类型,未初始化的通道类型变量其默认零值是nil
。
初始化channel
:
select 语句具有以下特点
。
可处理一个或多个 channel 的发送/接收操作
。
如果多个 case 同时满足,select 会随机选择一个执行
。
对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。