匠心精神 - 良心品质腾讯认可的专业机构-IT人的高薪实战学院

咨询电话:4000806560

从零开始学习Go语言中的并发编程

从零开始学习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语言中的并发编程,适合初学者入门学习。