关于go:Go协程揭秘轻量并发与性能的完美结合

48次阅读

共计 8557 个字符,预计需要花费 22 分钟才能阅读完成。

Go 协程为并发编程提供了弱小的工具,联合轻量级、高效的特点,为开发者带来了独特的编程体验。本文深入探讨了 Go 协程的基本原理、同步机制、高级用法及其性能与最佳实际,旨在为读者提供全面、深刻的了解和利用领导。

关注公众号【TechLeadCloud】,分享互联网架构、云服务技术的全维度常识。作者领有 10+ 年互联网服务架构、AI 产品研发教训、团队治理教训,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收 AI 产品研发负责人。

1. Go 协程简介

Go 协程(goroutine)是 Go 语言中的并发执行单元,它比传统的线程轻量得多,并且是 Go 语言并发模型中的外围组成部分。在 Go 中,你能够同时运行成千上万的 goroutine,而不必放心惯例操作系统线程带来的开销。

什么是 Go 协程?

Go 协程是与其余函数或办法并行运行的函数或办法。你能够认为它相似于轻量级的线程。其次要劣势在于它的启动和进行开销十分小,相比于传统的线程来说,能够更无效地实现并发。

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)
        fmt.Println("Hello!")
    }
}

func main() {go sayHello() // 启动一个 Go 协程
    for i := 0; i < 5; i++ {time.Sleep(150 * time.Millisecond)
        fmt.Println("Hi!")
    }
}

输入:

Hi!
Hello!
Hi!
Hello!
Hello!
Hi!
Hello!
Hi!
Hello!

处理过程:
在下面的代码中,咱们定义了一个 sayHello 函数,它在一个循环中打印“Hello!”五次。在 main 函数中,咱们应用 go 关键字启动了 sayHello 作为一个 goroutine。尔后,咱们又在 main 中打印“Hi!”五次。因为 sayHello 是一个 goroutine,所以它会与 main 中的循环并行执行。因而,输入中“Hello!”和“Hi!”的打印程序可能会变动。

Go 协程与线程的比拟

  1. 启动开销:Go 协程的启动开销远小于线程。因而,你能够轻松启动成千上万个 goroutine。
  2. 内存占用:每个 Go 协程的堆栈大小开始时很小(通常在几 KB),并且能够依据须要增长和放大,而线程通常须要固定的、较大的堆栈内存(通常为 1MB 或更多)。
  3. 调度:Go 协程是由 Go 运行时零碎而不是操作系统调度的。这意味着 Go 协程之间的上下文切换开销更小。
  4. 安全性:Go 协程为开发者提供了简化的并发模型,配合通道(channels)等同步机制,缩小了并发程序中常见的谬误。

示例代码:

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan int) {
    for {fmt.Printf("Worker %d received data: %d\n", id, <-ch)
    }
}

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

    for i := 0; i < 3; i++ {go worker(i, ch) // 启动三个 Go 协程
    }

    for i := 0; i < 10; i++ {
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
}

输入:

Worker 0 received data: 0
Worker 1 received data: 1
Worker 2 received data: 2
Worker 0 received data: 3
...

处理过程:
在这个示例中,咱们启动了三个工作 goroutine 来从同一个通道接收数据。在 main 函数中,咱们发送数据到通道。每当通道中有数据时,其中一个工作 goroutine 会接管并解决它。因为 goroutines 是并发运行的,所以哪个 goroutine 接收数据是不确定的。

Go 协程的外围劣势

  1. 轻量级:如前所述,Go 协程的启动开销和内存应用都远远小于传统线程。
  2. 灵便的调度:Go 协程是协同调度的,容许用户在适当的机会进行工作切换。
  3. 简化的并发模型:Go 提供了多种原语(如通道和锁),使并发编程变得更加简略和平安。

总的来说,Go 协程为开发者提供了一个高效、灵便且平安的并发模型。与此同时,Go 的规范库提供了丰盛的工具和包,进一步简化了并发程序的开发过程。


2. Go 协程的根本应用

在 Go 中,协程是构建并发程序的根底。创立协程非常简单,并且应用 go 关键字就能够启动。让咱们摸索一些根本用法和与之相干的示例。

创立并启动 Go 协程

启动一个 Go 协程只需应用 go 关键字,后跟一个函数调用。这个函数即能够是匿名的,也能够是预约义的。

示例代码:

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {time.Sleep(200 * time.Millisecond)
        fmt.Println(i)
    }
}

func main() {go printNumbers()  // 启动一个 Go 协程
    time.Sleep(1 * time.Second)
    fmt.Println("End of main function")
}

输入:

1
2
3
4
5
End of main function

处理过程:
在这个示例中,咱们定义了一个 printNumbers 函数,它会简略地打印数字 1 到 5。在 main 函数中,咱们应用 go 关键字启动了这个函数作为一个新的 Go 协程。主函数与 Go 协程并行执行。为确保主函数期待 Go 协程执行实现,咱们使主函数休眠了 1 秒钟。

应用匿名函数创立 Go 协程

除了启动预约义的函数,你还能够应用匿名函数间接启动 Go 协程。

示例代码:

package main

import (
    "fmt"
    "time"
)

func main() {go func() {fmt.Println("This is a goroutine!")
        time.Sleep(500 * time.Millisecond)
    }()
    fmt.Println("This is the main function!")
    time.Sleep(1 * time.Second)
}

输入:

This is the main function!
This is a goroutine!

处理过程:
在这个示例中,咱们在 main 函数中间接应用了一个匿名函数来创立 Go 协程。在匿名函数中,咱们简略地打印了一条音讯并使其休眠了 500 毫秒。主函数先打印其音讯,而后期待 1 秒来确保 Go 协程有足够的工夫实现执行。

Go 协程与主函数

值得注意的是,如果主函数(main)完结,所有的 Go 协程都会被立刻终止,不管它们的执行状态如何。

示例代码:

package main

import (
    "fmt"
    "time"
)

func main() {go func() {time.Sleep(500 * time.Millisecond)
        fmt.Println("This will not print!")
    }()}

处理过程:
在下面的代码中,Go 协程在打印消息前休眠了 500 毫秒。但因为主函数在此期间曾经完结,所以 Go 协程也被终止,因而咱们不会看到任何输入。

总结,Go 协程的根本应用非常简单和直观,但须要留神确保主函数在所有 Go 协程执行结束之前不会完结。


3. Go 协程的同步机制

在并发编程中,同步是确保多个协程可能无效、平安地共享资源或协同工作的要害。Go 提供了几种原语,帮忙咱们实现这一指标。

1. 通道 (Channels)

通道是 Go 中用于在协程之间传递数据和同步执行的次要形式。它们提供了一种在一个协程中发送数据,并在另一个协程中接收数据的机制。

示例代码:

package main

import "fmt"

func sendData(ch chan string) {ch <- "Hello from goroutine!"}

func main() {messageChannel := make(chan string)
    go sendData(messageChannel) // 启动一个 Go 协程发送数据
    message := <-messageChannel
    fmt.Println(message)
}

输入:

Hello from goroutine!

处理过程:
咱们创立了一个名为 messageChannel 的通道。而后启动了一个 Go 协程 sendData,将字符串"Hello from goroutine!" 发送到这个通道。在主函数中,咱们从通道接管这个音讯并打印它。

2. sync.WaitGroup

sync.WaitGroup是一个期待一组协程实现的构造。你能够减少一个计数来示意应期待的协程数量,并在每个协程实现时缩小计数。

示例代码:

package main

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

func worker(id int, wg *sync.WaitGroup) {defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers completed.")
}

输入:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed.

处理过程:
咱们定义了一个名为 worker 的函数,它模仿一个须要一秒钟能力实现的工作工作。在这个函数中,咱们应用 defer wg.Done() 来确保在函数退出时缩小 WaitGroup 的计数。在 main 函数中,咱们启动了 5 个这样的工作协程,每启动一个,咱们就应用 wg.Add(1) 来减少计数。wg.Wait()则会阻塞,直到所有工作协程都告诉 WaitGroup 它们已实现。

3. 互斥锁 (sync.Mutex)

当多个协程须要访问共享资源时(例如,更新一个共享变量),应用互斥锁能够确保同时只有一个协程能拜访资源,避免数据竞态。

示例代码:

package main

import (
    "fmt"
    "sync"
)

var counter int
var lock sync.Mutex

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

func main() {
    var wg sync.WaitGroup

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

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

输入:

Final Counter: 1000

处理过程:
咱们有一个全局变量 counter,咱们心愿在多个 Go 协程中并发地减少它。为了确保每次只有一个 Go 协程可能更新counter,咱们应用了互斥锁lock 来同步拜访。

这些是 Go 协程同步机制的一些根本办法。正确地应用它们能够帮忙你编写更平安、更高效的并发程序。


4. Go 协程的高级用法

Go 协程的高级用法波及更简单的并发模式、错误处理和协程管制。咱们将摸索一些常见的高级用法和它们的具体利用示例。

1. 选择器 (select)

select语句是 Go 中解决多个通道的办法。它容许你期待多个通道操作,执行其中一个能够进行的操作。

示例代码:

package main

import (
    "fmt"
    "time"
)

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

    go func() {time.Sleep(1 * time.Second)
        ch1 <- "Data from channel 1"
    }()

    go func() {time.Sleep(2 * time.Second)
        ch2 <- "Data from channel 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

输入:

Data from channel 1
Data from channel 2

处理过程:
咱们创立了两个通道 ch1ch2。两个 Go 协程别离向这两个通道发送数据,但它们的休眠工夫不同。在 select 语句中,咱们期待两个通道中的任何一个筹备好数据,而后进行解决。因为 ch1 的数据先达到,因而它的音讯首先被打印。

2. 超时解决

应用select,咱们能够轻松实现对通道操作的超时解决。

示例代码:

package main

import (
    "fmt"
    "time"
)

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

    go func() {time.Sleep(3 * time.Second)
        ch <- "Data from goroutine"
    }()

    select {
    case data := <-ch:
        fmt.Println(data)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout after 2 seconds")
    }
}

输入:

Timeout after 2 seconds

处理过程:
Go 协程会休眠 3 秒钟后再向 ch 发送数据。在 select 语句中,咱们期待这个通道的数据或 2 秒的超时。因为 Go 协程在超时之前没有发送数据,因而超时的音讯被打印。

3. 应用 context 进行协程管制

context包容许咱们共享跨多个协程的勾销信号、超时和其余设置。

示例代码:

package main

import (
    "context"
    "fmt"
    "time"
)

func work(ctx context.Context) {
    for {
        select {case <-ctx.Done():
            fmt.Println("Received cancel signal, stopping the work")
            return
        default:
            fmt.Println("Still working...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go work(ctx)

    time.Sleep(5 * time.Second)
}

输入:

Still working...
Still working...
Still working...
Received cancel signal, stopping the work

处理过程:
在这个示例中,咱们创立了一个带有 3 秒超时的 context。Go 协程work 会继续工作,直到接管到勾销信号或超时。通过 3 秒后,context的超时被触发,Go 协程接管到了勾销信号并进行工作。

这些高级用法为 Go 协程提供了弱小的性能,使得简单的并发模式和管制成为可能。把握这些高级技巧能够帮忙你编写更强壮、更高效的 Go 并发程序。


5. Go 协程的性能与最佳实际

Go 协程为并发编程提供了轻量级的解决方案。但为了充分利用其性能劣势并防止常见的陷阱,理解一些最佳实际和性能思考因素是很有必要的。

1. 限度并发数

尽管 Go 协程是轻量级的,但无节制地创立大量的 Go 协程可能会导致内存耗尽或调度开销增大。

示例代码:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
}

func main() {
    var wg sync.WaitGroup
    numWorkers := 1000

    for i := 1; i <= numWorkers; i++ {wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

输入:

Worker 1 started
Worker 2 started
...
Worker 1000 started
All workers done

处理过程:
这个示例创立了 1000 个工作 Go 协程。只管这个数字可能不会导致问题,但如果不加限度地创立更多的 Go 协程,可能会导致问题。

2. 防止竞态条件

多个 Go 协程可能会同时访问共享资源,导致不确定的后果。应用互斥锁(Mutex)或其余同步机制来确保数据的一致性。

示例代码:

package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()}

func main() {
    var wg sync.WaitGroup

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

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

输入:

Final counter value: 1000

处理过程:
咱们应用 sync.Mutex 确保在减少计数器时的互斥拜访。这确保了并发拜访时的数据一致性。

3. 应用工作池模式

工作池模式是创立固定数量的 Go 协程来执行工作的办法,防止适度创立 Go 协程。工作通过通道发送。

示例代码:

package main

import (
    "fmt"
    "sync"
)

func worker(tasks <-chan int, wg *sync.WaitGroup) {defer wg.Done()
    for task := range tasks {fmt.Printf("Worker processed task %d\n", task)
    }
}

func main() {
    var wg sync.WaitGroup
    tasks := make(chan int, 100)

    // Start 5 workers.
    for i := 0; i < 5; i++ {wg.Add(1)
        go worker(tasks, &wg)
    }

    // Send 100 tasks.
    for i := 1; i <= 100; i++ {tasks <- i}

    close(tasks)
    wg.Wait()}

输入:

Worker processed task 1
Worker processed task 2
...
Worker processed task 100

处理过程:
咱们创立了 5 个工作 Go 协程,它们从 tasks 通道中接管工作。这种模式能够管制并发数并重复使用 Go 协程。

遵循这些最佳实际不仅能够使你的 Go 协程代码更加强壮,而且还能够更无效地利用系统资源,进步程序的整体性能。


6. 总结

随着计算技术的提高,并发和并行成为了古代软件开发中的要害元素。Go 语言作为一个古代编程语言,通过其内置的 goroutine 为开发者提供了一种简洁而弱小的并发编程模式。但正如咱们在后面的章节中所看到的,了解其工作原理、同步机制、高级用法及性能与最佳实际是至关重要的。

从本文中,咱们不仅理解了 Go 协程的基础知识和工作原理,还探讨了一些对于如何最大限度地施展其性能的高级主题。要害的洞察包含:

  1. 轻量与高效:Go 协程是轻量级的线程,但它们在实现上的特点使其在大量并发场景下更为高效。
  2. 同步与通信 :Go 的哲学是“不通过共享内存来通信,而是通过通信来共享内存”。这反映在其弱小的channel 机制中,这也是防止许多并发问题的要害。
  3. 性能与最佳实际:了解并遵循最佳实际不仅能够确保代码的健壮性,而且还能够显著进步性能。

最初,尽管 Go 提供了弱小的工具和机制来解决并发,但真正的艺术在于如何正确地应用它们。正如咱们在软件工程中常常看到的那样,工具只是伎俩,真正的力量在于理解它们的工作原理并正确地利用它们。

心愿本文为您提供了对于 Go 协程的深刻、全面的意识,并为您的并发编程之旅提供了有价值的洞见和领导。正如在云服务、互联网服务架构和其余简单的零碎中常常能够看到的那样,真正把握并发是进步性能、扩展性和响应速度的要害。

关注公众号【TechLeadCloud】,分享互联网架构、云服务技术的全维度常识。作者领有 10+ 年互联网服务架构、AI 产品研发教训、团队治理教训,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收 AI 产品研发负责人。

如有帮忙,请多关注
集体微信公众号:【TechLeadCloud】分享 AI 与云服务研发的全维度常识,谈谈我作为 TechLead 对技术的独特洞察。
TeahLead KrisChang,10+ 年的互联网和人工智能从业教训,10 年 + 技术和业务团队治理教训,同济软件工程本科,复旦工程治理硕士,阿里云认证云服务资深架构师,上亿营收 AI 产品业务负责人。

正文完
 0