优秀的编程知识分享平台

网站首页 > 技术文章 正文

对比着学 Go 语言-进阶:从基础开讲什么是并发?

nanyue 2024-12-01 01:43:06 技术文章 4 ℃

并发意味着程序在运行时有多个执行上下文,对应着多个调用栈。

每一个进程在运行时,都有自己的调用栈和堆,有一个完整的上下文。

从系统的角度讲,多个进程时可以并发的。

并发适用的场景有 4 种:

  • 同时响应图形用户界面和 IO 密集操作
  • 服务器面对大量用户请求
  • 发挥计算机硬件的能力
  • IO 操作阻塞程序

并发的优势有 3 条:

  • 表现问题模型
  • 提高程序的执行效率
  • 充分利用固件的异步性

实现并发的主流模型包括 4 种:

  • 多进程,传统并发模型
  • 多线程,传统并发模型
  • 基于回调的非阻塞/异步 IO,传统并发模型
  • 协程

传统并发模型的缺陷包括 2 条:

  • 共享内存的并发,容易在工程中遇到各种奇怪的故障和问题
  • 消息传递系统,复制操作在性能上不优越。

新的并发方式:协程

Python 中的并发方式叫 协程,用 yield 实现,Go 中把它叫做 goroutine , 也称作协程。Go 中从语言的角度就支持协程。

goroutine 由 Go 的运行时 runtime管理。

使用并发执行一个 Add 函数

func Add(x, y int) {
  z := x + y
  fmt.Println(z)
}

go Add(1, 1)

在函数调用前加上 go 关键字,这次调用就会在一个新的 goroutine 中并发执行。

当被调用的函数返回时,这个 goroutine 也自动结束了。

当这个函数有返回值,在并发执行时,这个返回值会被丢弃。

package main

import "fmt"

func Add(x, y int) {
  z := x + y
  fmt.Println(z)
}

func main() {
  for i := 0; i<10; i++ {
   		go Add(i,i) 
  }
}

想要并行执行 10 次 Add() 函数,可以通过在一个 for 循环中调用 10 次 Add()。但这样会出现协程还未开始执行,主程序就已经退出的情况。

Go 语言的程序机制是,程序从初始化 main package 并执行 main() 函数开始,当 main() 函数返回时,程序退出,且程序不等待其他 goroutine 结束。

解决的办法是使用 Go 的 channel:

package main

import "fmt"
import "sync"
import "runtime"

var counter int = 0
func Count(lock *sync.Mutex) {
     lock.Lock()
     counter++
     fmt.Println(counter)
     lock.Unlock()
}

func main() {
  lock := &sync.Mutex{}
  
  for i:=0; i<10; i++ {
      go Count(lock) 
  }
  
  for {
     lock.Lock()
     
     c := counter
     
     lock.Unlock()
    
     runtime.Gosched()
     if c>=10 {
        break 
     }
  }
}

Go 中获知所有 goroutine 的状态,使用的是并发通信。

区别于共享数据的并发通信模型,Go 采用的是消息的模型。

package main 

import "fmt"

func Count(ch chan int) {
    fmt.Println("Counting") // 3.执行加减计算
    ch <- 1 // 4.通过 ch <- 1 语句向对应的 channel 中写入数据 1,在这个 channel 被读取前,这个操作是阻塞的。
}

func main() {
     chs := make([]chan int, 10) // 1.定义一个包含 10 个 channel 的数组
     for i := 0; i<10; i++ { // 2.把数组中的每个 channel 分配给 10 个不同的 goroutine
         chs[i] = make(chan int) 
         go Count(chs[i])
     } // 5.在所有的 goroutine 启动完成后
  	
     for _, ch := range(chs) { 
         <-ch // 6.从 10 个 channel 中依次读取数据。在对应的 channel 写入数据前,这个操作也是阻塞的。
     }
}

这样,用 channel 实现了类似锁的功能,进而保证了所有 goroutine 完成后,主函数才返回。

channel 的声明形式:

var chanName chan ElementType

与一般的变量声明不同的地方仅仅是在类型之前加了 chan 关键字。ElementType 指定这个 channel 所能传递的元素类型。

var ch chan int // 声明一个传递类型为 int 的 channel

var m map[string] chan bool // 声明一个 map,元素是 bool 型的 channel

定义一个 channel,直接使用内置的函数 make():

ch := make(chan int) // 声明并初始化一个 int 型的 channel,名为 ch

channel 用法中,最常见的包括写入和读出。

ch <- value // 数据写入至 channel

向 channel 写入数据会导致程序阻塞,直到有其他 goroutine 从这个 channel 中读取数据。

value := <- ch

如果 channel 之前没有写入数据,那么从 channel 中读取数据也会导致程序阻塞,直到 channel 中被写入数据为止。

Tags:

最近发表
标签列表