go语言并发入门
本文中内容和代码基本都为原创,在结束部分有参考(或者说是学习总结而来的,毕竟概念没法原创)今天的课程和网上的相关介绍
当谈论 Go (Golang) 语言中的并发时,有三个主要方面需要考虑:goroutine、channel 和 sync 。这些是 Go 语言中实现并发的核心
-
Goroutine(协程): Goroutine 是 Go 语言中轻量级的执行单位。它们是由 Go 运行时管理的并发执行的函数或方法。相比于传统的线程,Goroutine 更轻量,启动和销毁代价较低,并且可以更高效地处理大量的并发任务。
使用 Goroutine 很简单,只需在函数或方法调用前加上
go关键字即可启动一个 Goroutine。例如:package main import ( "fmt" "strconv" "time" ) func work1(x int) { fmt.Println(strconv.Itoa(x) + ":process is starting ") time.Sleep(time.Duration(x) * time.Second) fmt.Println(strconv.Itoa(x) + ":process ends ") } func main() { go work1(1) go work1(2) go work1(3) go work1(4) time.Sleep(5 * time.Second) } 上面是我在go中进行的一个使用goroutine的并发例子,就是一个函数中,在他开始的时候,让它输出一个值,然后进行一段时间的阻塞,再让它结束的时候输出一下。
4:process is starting 2:process is starting 1:process is starting 3:process is starting 1:process ends 2:process ends 3:process ends 4:process ends 可以看到它成功的如预期了
Goroutine 之间的通信通常通过
channel实现,它是连接不同 Goroutine 的管道。 -
Channel(通道): Channel 是一种用于在 Goroutine 之间传递数据的通信机制。它可以让不同的 Goroutine 安全地交换数据,从而实现协调和同步。
在 Go 中,要通过
make函数来创建一个 Channel,指定通道中元素的类型。例如,创建一个整数类型的通道:ch := make(chan int)信道是带有类型的管道,你可以通过它用信道操作符
<-来发送或者接收值。ch <- v // 将 v 发送至信道 ch。 v := <-ch // 从 ch 接收值并赋予 v。信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给
make来初始化一个带缓冲的信道:ch := make(chan int, 100)信道如果你想像流一样的理解使用,go的channel有range和close两者,前者进行循环会对信道上的内容进行监听和接收值,后者是信道的发送者用来关闭信道的
监听者使用range进行监听的时候,可以有两个参数,后者是用来检测的,如信道是否没有值可以接收且信道已关闭,则其值为falsee
v, ok := <-ch我写了一个如下的代码来验证和测试channel的功能,不幸的是,它需要后面sync的waitgroups中的知识点,导致我一开始搞了好久才搞明白那里出了问题(遇到了死锁)
package main import ( "fmt" "strconv" "sync" ) // 我当时设想的这个代码要能正常实现功能还需要后面的sync.waitgroups来进行结束了的监测 func addi(st int, ed int, ch chan<- int, wg *sync.WaitGroup) { for i := st; i < ed; i++ { ch <- i } fmt.Println(strconv.Itoa(st) + "to" + strconv.Itoa(ed) + "done") wg.Done() } func demon(wg *sync.WaitGroup, ch chan int) { wg.Add(3) go addi(1, 1000, ch, wg) go addi(1001, 2000, ch, wg) go addi(2001, 3000, ch, wg) wg.Wait() defer func() { fmt.Println("all goroutine done") close(ch) }() } func sums(ch chan int, wg1 *sync.WaitGroup) { sumi := 0 for i := range ch { sumi += i } fmt.Println(sumi) wg1.Done() } func main() { ch := make(chan int, 5) wg := sync.WaitGroup{} wg1 := sync.WaitGroup{} wg1.Add(1) go demon(&wg, ch) go sums(ch, &wg1) wg1.Wait() } 输出如下:
2001 to 3000 done 1 to 1000 done 1001 to 2000 done all goroutine done Sum: 4495500 -
Sync(同步):
sync包提供了例如 Mutex、RWMutex、WaitGroup 等来帮助管理并发访问共享资源和实现同步。这些原语可以防止多个 Goroutine 同时访问共享资源,从而避免竞态条件和数据竞争。- Mutex(互斥锁):用于保护临界区,确保同一时间只有一个 Goroutine 可以进入临界区代码。防止多个 Goroutine 同时修改共享数据。
package main import ( "fmt" "sync" "time" ) func sets(mu *sync.Mutex, x *int) { (*mu).Lock() (*x) = (*x) + 1 (*mu).Unlock() } func main() { mu := sync.Mutex{} ans := 0 for i := 0; i < 1000; i++ { go sets(&mu, &ans) } time.Sleep(4 * time.Second) fmt.Println(ans) } -
RWMutex 全称为 "读写互斥锁"(Read-Write Mutex),它提供了读写锁的功能,可以在多个 Goroutine 之间有效地管理对共享资源的访问。
- 读锁定和写锁定互斥: 在任何时候,RWMutex 要么可以被多个 Goroutine 同时读取(共享访问),要么可以被单个 Goroutine 写入(互斥访问)。在写锁定状态下,任何其他 Goroutine 都无法获得读锁定或写锁定,直到写锁定被释放。
- 多读锁定优先: 在读锁定状态下,多个 Goroutine 可以同时获得读锁定,实现了多读单写的并发模型。只有在没有读锁定的情况下,写锁定才能获得。
RWMutex 提供了以下方法:
func (*RWMutex) Lock():获取写锁定。如果已经有其他 Goroutine 持有读锁定或写锁定,则调用 Lock() 的 Goroutine 将阻塞,直到它获得写锁定为止。func (*RWMutex) Unlock():释放写锁定。func (*RWMutex) RLock():获取读锁定。如果已经有其他 Goroutine 持有写锁定,则调用 RLock() 的 Goroutine 将阻塞,直到它获得读锁定为止。func (*RWMutex) RUnlock():释放读锁定。
(和数据库差不多)-
WaitGroup(等待组):用于等待一组 Goroutine 完成任务。它可以阻塞主 Goroutine,直到所有其他 Goroutine 完成工作。
WaitGroup中有三种操作,分别是wair,add,done
- ADD(x)计数器中增加x个计数
- DONE()表面当前的完成,计数器中-1
- WAIT()阻塞使其等待直到计数器为0
package main import ( "fmt" "math/rand" "sync" "time" ) func waiters(x string, wg *sync.WaitGroup) { fmt.Println(x + " come") time.Sleep(time.Duration(rand.Intn(5)) * time.Second) defer func() { }() wg.Done() fmt.Println(x + " go") } func main() { wg := sync.WaitGroup{} wg.Add(4) go waiters("alex", &wg) go waiters("wangcr", &wg) go waiters("yaossg", &wg) go waiters("dnk", &wg) wg.Wait() defer fmt.Println("all done") }
这三个方面共同组成了 Go 语言中并发的基础。使用 Goroutine 和 Channel 可以轻松实现并发任务的执行和数据交换,而 Sync 包中的原语可以保护共享资源和确保 Goroutine 之间的同步。这些特性使得 Go 在处理并发任务时非常强大和高效。