从零开始学习Go语言中的并发编程
Go语言是一种高效,快速的编程语言,它的并发编程能力非常强大,可以轻易地实现多线程编程,同时还有很多与其它语言不同的特性。本篇文章将着重讲解Go语言中的并发编程,包括基本语法、锁机制、通道和协程,以帮助初学者更好地了解Go语言中的并发编程。
一、基本语法
在Go语言中,使用goroutine来实现并发,一个goroutine就是一个轻量级的线程,它与线程的区别在于,goroutine会自动地进行调度,而不是由操作系统进行控制。goroutine使用关键字go来启动,示例如下:
```go
package main
import "fmt"
func main() {
go sayHello()
fmt.Println("Waiting for goroutine...")
}
func sayHello() {
fmt.Println("Hello, world!")
}
```
在这个例子中,我们在main()函数中启动了一个名为sayHello()的goroutine,这个goroutine会输出Hello, world!,同时main()函数会继续往下执行,输出Waiting for goroutine...。需要注意的是,如果不使用类似于time.Sleep()等方法来让程序进入等待状态,程序可能不会等到goroutine执行完毕而直接退出。
二、锁机制
在并发编程中,锁机制是保证线程安全的重要手段,Go语言提供了两种锁机制:sync.Mutex和sync.RWMutex。
1. sync.Mutex
sync.Mutex是最基本的一种锁,它只有两个方法:Lock()和Unlock()。示例如下:
```go
package main
import (
"fmt"
"sync"
)
var mutex sync.Mutex
var count int
func main() {
for i := 0; i < 10; i++ {
go add()
}
fmt.Scanln()
}
func add() {
mutex.Lock()
defer mutex.Unlock()
count++
fmt.Println("Count:", count)
}
```
在这个例子中,我们定义了一个全局变量count和一个Mutex对象mutex,add()函数中的mutex.Lock()锁住了资源,防止其它goroutine对其进行修改,而defer mutex.Unlock()则是在函数执行完毕后解锁资源。
2. sync.RWMutex
sync.RWMutex是读写锁,它分为读锁和写锁,读锁可以多个goroutine同时获取,但写锁只能有一个goroutine获取,下面是sync.RWMutex的示例:
```go
package main
import (
"fmt"
"sync"
)
var rwMutex sync.RWMutex
var count int
func main() {
for i := 0; i < 10; i++ {
go add()
}
fmt.Scanln()
}
func add() {
rwMutex.Lock()
defer rwMutex.Unlock()
count++
fmt.Println("Count:", count)
}
```
在这个例子中,我们使用了sync.RWMutex对count进行写操作,只允许一个goroutine进行写操作。如果count只需要进行读操作,我们可以使用rwMutex.RLock()和rwMutex.RUnlock(),允许多个goroutine同时获取读锁。下面是一个读写锁的示例代码:
```go
package main
import (
"fmt"
"sync"
)
var rwMutex sync.RWMutex
var count int
func main() {
for i := 0; i < 10; i++ {
go read(i)
}
for i := 0; i < 5; i++ {
go write(i)
}
fmt.Scanln()
}
func read(id int) {
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read goroutine %d starts\n", id)
fmt.Printf("Read goroutine %d reads count as %d\n", id, count)
}
func write(id int) {
rwMutex.Lock()
defer rwMutex.Unlock()
fmt.Printf("Write goroutine %d starts\n", id)
count++
fmt.Printf("Write goroutine %d writes count as %d\n", id, count)
}
```
在这个例子中,我们使用了sync.RWMutex对count进行读写操作,其中有10个goroutine读取count,5个goroutine写入count。
三、通道
通道是Go语言中非常重要的一个机制,它可以用于不同goroutine之间进行通信,从而实现数据的传输和同步。通道可以是无缓冲的,也可以是有缓冲的。
1. 无缓冲通道
无缓冲通道在传输数据时,发送方和接收方必须同时准备好,否则就会阻塞。示例如下:
```go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
fmt.Println("Start goroutine...")
time.Sleep(2 * time.Second)
ch <- 1
fmt.Println("End goroutine...")
}()
fmt.Println("Waiting...")
<-ch
fmt.Println("Done.")
}
```
在这个例子中,我们定义了一个无缓冲通道ch,并在一个新的goroutine中向ch发送了数值1。在main()函数中,我们等待接收ch中的数据,如果接收到了1,则输出Done.。
2. 有缓冲通道
有缓冲通道可以在一定程度上解决无缓冲通道的问题,当通道中有数据时,接收方可以立即接收数据。示例如下:
```go
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
```
在这个例子中,我们定义了一个有缓冲通道ch,并向ch中发送了数值1、2和3。接着,我们分别接收了ch中的数据。需要注意的是,如果有缓冲通道的缓存已满,继续发送数据会导致程序阻塞。
四、协程
协程是Go语言中比较重要的机制,它可以让我们更加方便地实现并发编程。协程与goroutine的区别在于,协程具有更大的灵活性,可以在多个线程中共享,而goroutine只能在单一线程中运行。
示例如下:
```go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
fmt.Println("Start goroutine 1...")
time.Sleep(2 * time.Second)
ch <- 1
fmt.Println("End goroutine 1...")
}()
go func() {
fmt.Println("Start goroutine 2...")
time.Sleep(3 * time.Second)
ch <- 2
fmt.Println("End goroutine 2...")
}()
fmt.Println("Waiting...")
res1 := <-ch
res2 := <-ch
fmt.Println("Result:", res1, res2)
fmt.Println("Done.")
}
```
在这个例子中,我们定义了两个协程,分别向通道ch中发送数值1和2。在main()函数中,我们等待接收ch中的数据,并输出结果。需要注意的是,协程的执行顺序是不确定的,因此输出结果也不一定是1和2。
总结
Go语言中的并发编程是非常重要的,使用goroutine、锁机制、通道和协程可以很方便地实现多线程编程。在编写并发程序时,也需要注意避免常见的问题,如竞态条件和死锁等。本篇文章主要介绍了Go语言中的并发编程,适合初学者入门学习。