12goroutine

在 Go 语言中,goroutine 是一种轻量级的用户态线程,由 Go 运行时(runtime)管理。goroutine 的创建和销毁成本非常低,因此可以轻松地实现并发编程。

1. 什么是 Goroutine?

Goroutine 是 Go 语言中的并发执行单元。与操作系统线程相比,goroutine 更加轻量级,创建和销毁的开销更小。Go 运行时会在逻辑处理器(P)上调度 goroutine,使其在多个操作系统线程上高效地运行。

2. 如何创建 Goroutine?

要在 Go 中创建一个 goroutine,只需在函数调用前加上关键字 go。例如:

package main

import (
	"fmt"
	"time"
)

func sayHello() {
	fmt.Println("Hello, world!")
}

func main() {
	go sayHello() // 创建一个新的 goroutine 来执行 sayHello 函数
	time.Sleep(1 * time.Second) // 等待 1 秒,确保 goroutine 有机会执行
}

此时我们并不知道 goroutine什么时候执行完成,上例中我们使用睡眠,让程序停止,等待携程。

使用 sync.WaitGroup

sync.WaitGroup ,用于等待一组 goroutine 完成。它通过计数器来跟踪正在执行的 goroutine 数量,当计数器归零时,表示所有 goroutine 已完成。

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 在函数结束时调用 Done,减少计数器

    fmt.Printf("Worker %d 开始工作\n", id)
    time.Sleep(time.Second) // 模拟工作耗时
    fmt.Printf("Worker %d 完成工作\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // 每启动一个 goroutine,计数器加一
        go worker(i, &wg)
    }

    wg.Wait() // 等待所有 goroutine 完成
    fmt.Println("所有工作已完成")
}

3. Goroutine 的调度

12goroutine_第1张图片

Go 语言的运行时包含一个复杂的调度器,用于在多个操作系统线程上调度 goroutine。调度器使用 M:N 模型,将 M 个 goroutine 映射到 N 个操作系统线程上。调度器会根据程序的运行情况动态地将 goroutine 分配给不同的线程。

Go 运行时负责将 goroutine 调度到一组操作系统线程上执行。默认情况下,Go 运行时会根据系统的 CPU 核心数和程序的负载动态调整使用的线程数量。这种自动管理机制使得开发者无需手动干预线程的创建和管理。

然而,在某些特定场景下,如需要限制资源使用、可能需要手动设置或控制 Go 程序使用的最大线程数。

 设置 GOMAXPROCS

GOMAXPROCS 是 Go 运行时的一个重要环境变量和函数,用于设置可以同时执行 Go 代码的最大 CPU 核心数。它并不直接限制操作系统线程的数量,但会影响 Go 运行时调度 goroutine 到线程上的方式。

使用 runtime.GOMAXPROCS 设置

您可以在程序启动时调用 runtime.GOMAXPROCS 来设置并行执行的 CPU 核心数:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // 获取当前的 GOMAXPROCS 值
    current := runtime.GOMAXPROCS(0)
    fmt.Println("当前 GOMAXPROCS:", current)

    // 设置 GOMAXPROCS 为 4
    previous := runtime.GOMAXPROCS(4)
    fmt.Println("之前的 GOMAXPROCS:", previous)
    fmt.Println("现在的 GOMAXPROCS:", runtime.GOMAXPROCS(0))
}

输出示例:​

当前 GOMAXPROCS: 8
之前的 GOMAXPROCS: 8
现在的 GOMAXPROCS: 4

说明:​

  • runtime.GOMAXPROCS(n) 设置可以同时执行 Go 代码的最大 CPU 核心数为 n,并返回之前的值。
  • 如果 n 小于 1,则不会改变当前的设置,只是返回当前的值。

默认值

从 Go 1.5 开始,GOMAXPROCS 的默认值被设置为运行程序的机器上的 CPU 核心数。因此,通常不需要手动设置,除非有特定需求。

4. 通信与同步

在并发编程中,通信和同步是非常重要的。Go 语言提供了多种机制来实现 goroutine 之间的通信和同步。

4.1 通道(Channel)

12goroutine_第2张图片

通道是 Go 语言中的一种类型安全的通信机制,用于在 goroutine 之间传递数据。通道有两种类型:无缓冲通道和有缓冲通道。

创建一个无缓冲通道:

ch := make(chan int)

创建一个有缓冲通道:

ch := make(chan int, 10) // 缓冲区大小为 10

发送数据到通道:

ch <- 42 // 将整数 42 发送到通道 ch

从通道接收数据:

value := <-ch // 从通道 ch 接收数据,并将其赋值给变量 value
单向通道

单向通道主要用于函数参数传递,以明确函数的意图,提高代码的可读性和安全性。

示例:发送数据的函数

func sendOnly(ch chan<- int, value int) {
    ch <- value
}

在上述函数中,参数 ch 被声明为 chan<- int,表示 ch 只能用于发送整数,不能用于接收数据。

示例:接收数据的函数

func receiveOnly(ch <-chan int) int {
    return <-ch
}

这里,参数 ch 被声明为 <-chan int,表示 ch 只能用于接收整数,不能用于发送数据。

使用通道实现生产者-消费者模式:

package main

import (
    "fmt"
    "sync"
)

func producer(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("生产者发送:", i)
    }
    close(ch) // 发送完毕后关闭通道
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for value := range ch { // 使用range接收,直到通道关闭
        fmt.Println("消费者接收:", value)
    }
    fmt.Println("消费者检测到通道已关闭")
}

func main() {
    ch := make(chan int)
    var wg sync.WaitGroup

    wg.Add(2)
    go producer(ch, &wg)
    go consumer(ch, &wg)

    wg.Wait()
    fmt.Println("所有操作完成")
}

输出示例:​

生产者发送: 0
消费者接收: 0
生产者发送: 1
消费者接收: 1
生产者发送: 2
消费者接收: 2
生产者发送: 3
消费者接收: 3
生产者发送: 4
消费者接收: 4
消费者检测到通道已关闭
所有操作完成

在这个示例中:

  • producer goroutine 向通道发送了 5 个整数,然后关闭了通道。
  • consumer goroutine 使用 for range 循环接收通道中的数据,直到通道被关闭。
注意事项
  1. 关闭通道的责任:只有发送方应该关闭通道。如果接收方尝试关闭通道,可能会导致运行时恐慌,尤其是在多个发送方的情况下。

  2. 避免重复关闭:确保通道只被关闭一次。可以通过使用 sync.Once 或其他同步机制来保证这一点。

  3. 接收方的处理:接收方应该能够正确处理通道关闭的情况。例如,使用 for range 循环可以自动检测通道是否已关闭。

  4. 缓冲通道:对于有缓冲的通道,关闭通道的行为与无缓冲通道类似,但接收方可以在缓冲区中的数据被消费完之前继续接收数据。

4.2(Select)简介

select 语句类似于 switch,但是用于处理多个通道操作。它允许程序等待多个通道操作中的任意一个完成,并执行对应的代码块。select 的基本语法:

select {
case <-ch1:
    // 处理来自 ch1 的数据
case ch2 <- value:
    // 向 ch2 发送数据
default:
    // 如果没有任何 case 可运行,则执行 default
}
Select 的基本用法

select 语句通常与多个通道操作一起使用,以处理并发任务。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    // 启动两个发送数据的 goroutine
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "来自 ch1 的消息"
    }()

    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- "来自 ch2 的消息"
    }()

    // 使用 select 等待任意一个通道有数据
    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

输出:​

来自 ch2 的消息

在这个例子中,ch2 先有数据可接收,因此先打印来自 ch2 的消息。

Select 的高级用法
默认分支(Default Case)

select 可以包含一个 default 分支,当没有任何 case 可运行时,执行 default 分支。这使得 select 成为一个非阻塞操作。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    // 尝试接收数据,如果 ch 没有数据,则执行 default
    select {
    case msg := <-ch:
        fmt.Println("接收到:", msg)
    default:
        fmt.Println("没有接收到数据")
    }
}

输出:​

没有接收到数据
超时控制

结合 time.After,可以实现超时控制,防止 select 永远阻塞。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    // 启动一个发送数据的 goroutine,但会延迟3秒
    go func() {
        time.Sleep(3 * time.Second)
        ch <- "消息"
    }()

    // 设置超时时间为2秒
    select {
    case msg := <-ch:
        fmt.Println("接收到:", msg)
    case <-time.After(2 * time.Second):
        fmt.Println("接收超时")
    }
}

输出:​

接收超时
非阻塞接收和发送

通过在 select 中添加 default 分支,可以实现非阻塞的发送和接收。

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 1)

    // 非阻塞发送
    select {
    case ch <- 1:
        fmt.Println("发送成功")
    default:
        fmt.Println("发送失败,通道已满或无接收方")
    }

    // 非阻塞接收
    select {
    case msg := <-ch:
        fmt.Println("接收到:", msg)
    default:
        fmt.Println("接收失败,通道为空")
    }
}

输出:​

发送成功
接收到: 1

如果将通道缓冲区设置为0,并尝试非阻塞发送,可能会失败:

ch := make(chan int) // 无缓冲通道

// 非阻塞发送,没有接收方准备好,发送失败
select {
case ch <- 1:
    fmt.Println("发送成功")
default:
    fmt.Println("发送失败,通道已满或无接收方")
}

输出:​

发送失败,通道已满或无接收方
 Select 与管道的综合示例

下面是一个综合示例,展示如何使用 select 处理多个管道的发送和接收,以及超时控制。

package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- string, name string, delay time.Duration) {
    time.Sleep(delay)
    ch <- fmt.Sprintf("来自 %s 的消息", name)
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go producer(ch1, "生产者1", 2*time.Second)
    go producer(ch2, "生产者2", 1*time.Second)

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        case <-time.After(3 * time.Second):
            fmt.Println("接收超时")
        }
    }
}

输出:​

来自 生产者2 的消息
来自 生产者1 的消息

如果将 for 循环的条件改为 i < 1,并添加超时控制,可以观察到超时行为。

空 select 语句阻塞主线程以防止程序退出

在某些情况下,尤其是当所有的工作都在其他 goroutine 中进行时,主 goroutine 可能需要等待其他 goroutine 完成工作。这时可以使用一个空的 select 语句来阻塞主线程。

示例:等待其他 goroutine

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    fmt.Println("工作开始")
    time.Sleep(2 * time.Second)
    fmt.Println("工作完成")
    done <- true
}

func main() {
    done := make(chan bool)
    go worker(done)
    // 等待工作完成,可以通过接收 done 信号来实现
    // 但这里为了简单起见,使用空的 select 阻塞主线程(不推荐实际使用)
    // select {}
    
    // 更好的做法是使用 <-done 来等待
    <-done
    fmt.Println("主程序退出")
}

注意

  • 上述代码中,如果取消注释 select {},主线程将被永久阻塞,程序不会退出。但更好的做法是使用 <-done 来等待工作完成。

4.3 同步原语

Go 语言还提供了其他同步原语,如互斥锁(sync.Mutex)和读写锁(sync.RWMutex),用于保护共享资源。

使用互斥锁保护共享资源:

package main

import (
	"fmt"
	"sync"
)

var counter int
var mu sync.Mutex

func increment() {
	mu.Lock()
	counter++
	mu.Unlock()
}

func main() {
	wg := sync.WaitGroup{}

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}

	wg.Wait()
	fmt.Println("Counter:", counter)
}

sync.Mutex 提供了两个主要的方法:

  • Lock(): 获取锁。如果锁已被其他goroutine持有,则当前goroutine会被阻塞,直到锁被释放。
  • Unlock(): 释放锁,允许其他goroutine获取该锁。
读写锁(RWMutex)

读写锁是一种比互斥锁更细粒度的锁机制,适用于读多写少的场景。它允许多个goroutine同时读取共享资源,但在写操作时需要独占访问。这可以提高并发性能,减少锁竞争。

sync.RWMutex 提供了以下方法:

  • RLock(): 获取读锁。如果当前没有写锁被持有,多个读锁可以被同时获取。
  • RUnlock(): 释放读锁。
  • Lock(): 获取写锁。写锁是排他的,当写锁被持有时,其他所有的读锁和写锁都会被阻塞。
  • Unlock(): 释放写锁。

:在需要支持大量并发读取操作的场景下,读写锁比互斥锁更高效。

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    data      = make(map[string]string)
    rwMutex   sync.RWMutex
)

func readData(key string, wg *sync.WaitGroup) {
    defer wg.Done()
    rwMutex.RLock()
    value, exists := data[key]
    fmt.Printf("Read %s: %s (exists: %v)\n", key, value, exists)
    rwMutex.RUnlock()
}

func writeData(key, value string, wg *sync.WaitGroup) {
    defer wg.Done()
    rwMutex.Lock()
    data[key] = value
    fmt.Printf("Write %s: %s\n", key, value)
    rwMutex.Unlock()
}

func main() {
    // 初始化一些数据
    writeData("name", "Alice", &sync.WaitGroup{})
    writeData("age", "30", &sync.WaitGroup{})

    var wg sync.WaitGroup

    // 启动多个读goroutine
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go readData("name", &wg)
    }

    // 启动一个写goroutine
    wg.Add(1)
    go writeData("name", "Bob", &wg)

    // 再启动多个读goroutine
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go readData("name", &wg)
    }

    wg.Wait()
}

在上述示例中,多个goroutine可以同时读取data中的数据,而写操作则需要独占锁。这确保了在读取过程中数据的一致性,同时允许多个读操作并发执行,提高了性能。

4.4sync.Once

sync.Once 的内部实现保证了 Do 方法只会执行一次,即使在多个 goroutine 同时调用的情况下。

这种实现保证了高效且线程安全的单次执行。

  • ​**Do(f func())**: 执行传入的函数 f,确保该函数在整个程序生命周期内只被调用一次。如果有多个 goroutine 同时调用 Do,只有一个 goroutine 会执行 f,其他 goroutine 会被阻塞,直到 f 执行完毕。

  • sync.Once 内部维护了一个状态标志和一个同步机制(如互斥锁和条件变量),以确保:

  • 第一次调用 Do 时,执行传入的函数,并将状态标志设置为已执行。
  • 后续调用 Do 时,直接跳过函数执行,不会再次执行传入的函数。
package main

import (
    "fmt"
    "sync"
)

var (
    once     sync.Once
    instance *Singleton
)

type Singleton struct {
    Name string
}

func GetInstance() *Singleton {
    once.Do(func() {
        fmt.Println("Initializing Singleton...")
        instance = &Singleton{Name: "Unique Instance"}
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            singleton := GetInstance()
            fmt.Printf("Goroutine %d: %+v", id, singleton)
        }(i)
    }
    wg.Wait()
}

输出示例

Initializing Singleton...
Goroutine 0: &{Name:Unique Instance}
Goroutine 1: &{Name:Unique Instance}
Goroutine 2: &{Name:Unique Instance}
...

在上述示例中,无论有多少个 goroutine 调用 GetInstanceSingleton 的初始化代码只会执行一次。

5. 错误处理与恢复

在并发编程中,错误处理和恢复尤为重要。Go 语言提供了 defer 和 recover 机制来处理 panic 和恢复程序的执行。

package main

import (
	"fmt"
)

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered from panic:", r)
		}
	}()

	go func() {
		panic("Something went wrong!")
	}()

	select {} // 阻塞主 goroutine,防止程序退出
}

6. 总结

Go 语言的 goroutine 提供了一种简单而强大的并发编程模型。通过使用通道和同步原语,可以轻松地在 goroutine 之间进行通信和同步。同时,Go 语言的错误处理和恢复机制也有助于编写健壮的并发程序。

你可能感兴趣的:(go语言基础,golang,后端)