Go语言进阶 | 青训营笔记

110 阅读4分钟

Go语言进阶 | 青训营笔记

这是我参与「第五届青训营 」伴学笔记创作活动的第2天

一、Go 并发基本概念

Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。

在学习 并发 之前,我们需要先了解一些基础概念:

进程/线程

进程 基本上是一个正在执行的程序,它是操作系统中最小的资源分配单位,是系统进行资源分配和调度的一个独立单位。

线程 是进程的子集,也称为轻量级进程。

并发编程中进程和线程是不可忽略的两个概念,他们很好的完成了操作系统或者服务对于高并发的需求。一个进程可以有多个线程,这些线程由调度器独立管理。一个进程内的所有线程都是相互关联的。线程是操作系统中最小的调度单位。

image.png

并发/并行

并发:并发意味着程序在单位时间内是同时运行的,即多线程程序在单核心的 cpu 上运行。

并行:并行意味着程序在任意时刻都是同时运行的,即多线程程序在多核心的 cpu 上运行。

并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了(Golang 的并发通过切换多个线程达到减少物理处理器空闲等待的目的)

image.png

协程/线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

image.png

二、Goroutine 介绍

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。

使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine。

goroutine 语法格式:

go 函数名( 参数列表 )

例如:

go f1(x, y, z)

开启一个新的 goroutine:

f1(x, y, z)

goroutine 特性:

(1)go 的执行是非阻塞的,不会等待。

(2)go 后面的函数的返回值会被忽略。

(3)调度器不能保证 goroutin 的执行次序。

(4)没有父子 goroutin 的概念,所有的 goroutin 是平等地被调度和执行的。

(5)Go 程序在执行时会单独为 main 函数 goroutin ,遇到其他go关键字时再去创建其他的 goroutine。

(6)Go 没有暴露 goroutine id 给用户,所以不能在 goroutine 里面显式地操作另一个goroutine 不过 runtime 包提供了一些函数访问和设置 goroutin 的相关信息。

三、Channel

通道(channel)是用来传递数据的一个数据结构。

channel 是Go语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或多个 goroutine 之间传递消息。

image.png

定义一个 channel 时,也需要定义发送到 channel 的值的类型,注意,必须使用 make 创建 channel,例如:

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

四、Go并发安全之lock

Go语言中通过Groutine 启动一个Go协程,不同协程之间是并发执行的,就像C++/Java中线程之间线程安全是一个常见的问题。

Go中提供了MutexRWMutex ,来实现锁功能。前者就是正常的 Lock, Unlock ;后者提供了读写锁,在协程读写分离中提供了读读互不影响,读写互斥。

通过mutex 实现了变量访问的安全,如下测试代码:

func TestMutex(t *testing.T) {
  var counter int = 0
  var mu sync.RWMutex // rwmutex lock

  for i := 0;i < 10000; i ++{
    go func() {
      // defer mutex unlock
      // 如果协程异常退出,能够释放锁
      defer func() { 
        mu.Unlock()
      }()
      mu.Lock()
      counter ++
    }()
  }

  time.Sleep(time.Second * 1)
  t.Logf("Counter = %d", counter)
}

假如有更多的协程,Go语言提供了WaitGroup机制,就像是C++中的条件变量,主线程可以等待在条件变量上,当线程执行完可以通过条件变量唤醒主线程继续执行。

image.png

var wg sync.WaitGroup 声明一个WaitGroup变量。

通过wg.Add(1)wg.Done() 来检测线程的完成情况,wg.Wait() 表示主线程等待在wg变量上。

func TestWaitGroup(t *testing.T) {
  var counter int = 0
  var mu sync.RWMutex // rw lock, 读写互斥
  var wg sync.WaitGroup

  for i := 0;i < 5000; i ++{
    wg.Add(1) 
    go func() {
      defer func() {
        mu.Unlock()
      }()
      mu.Lock()
      counter ++
      wg.Done() // wg.Add(-1)
    }()
  }

  wg.Wait()
  t.Logf("Counter = %d", counter)
}

五、引用参考:

该文章部分内容来自于以下课程或网页:

字节内部课:Go 语言进阶与依赖管理、Go语言工程实践与测试

并发编程 | 百度百科

Go 并发 | 菜鸟教程 (runoob.com)

Go 分布式学习利器(18) | [51CTO博客]

Go语言并发简述 | C语言中文网