Go语言进阶—并发编程| 青训营笔记

85 阅读4分钟

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

GO语言进阶

并发编程

什么是并发编程?首先我们先了解一下并发与并行

并发(Concurrency): 所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。

并行(Parallelism): 并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。

简单的来说,并发是多线程程序在一个核的cpu上运行,并行是多线程程序在多个核的cpu上运行,他们之间的区别简单的理解就是一个处理器还是多个处理器来执行任务

Go语言是如何实现高并发

高并发初探 —— 进程、线程与协程

进程: 进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。

线程: 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。

协程: 协程,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态中执行)。

线程与协程: 线程属于内核态,轻量级线程,栈MB级别;协程属于用户态,线程跑多个协程,栈KB级别

Go语言中的协程 —— Goroutine

go中使用Goroutine来实现并发concurrently。

Goroutine是Go语言特有的名词。区别于进程Process,线程Thread,协程Coroutine,因为Go语言的创造者们觉得和他们是有所区别的,所以专门创造了Goroutine。

Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

如何使用Goroutines

在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。

上述代码快速打印hello goroutine :0 ~ hello goroutine :4

main函数是Go语言默认启动的协程,即不使用go关键字也会有协程在运行

Channel

Channel类型

分为无缓冲通道和有缓冲通道两种

创建Channel

无缓存通道:make(chan int) 有缓存通道:make(chan int,[缓冲大小])

以下代码是一个由三个协程组成的程序:第一个协程负责生成 0-9 的数字并通过 Channel 发送给第二个协程;第二个协程接收收到的数字,并将数字进行平方计算,然后将结果发送给主协程;主协程遍历接收到的结果并输出:

func CalSquare() {
    //创建无缓冲通道src Channel
    src := make(chan int)
    //创建大小为3的有缓冲通道 dest Channel
    dest := make(chan int, 3)
    
    //启动协程A 发送数字0~9
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    
    //协程B计算输入数字的平方
    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    
    //主协程输出最后的平方数
    for i := range dest {
        //复杂操作
        println(i)
    }
}

并发安全Lock

并发安全问题,下面我们来看一个代码例子

var (
    x  int64
    lock sync.Mutex
)

func addWithLock(){

    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock
    }
}

func addWithoutLock(){

    for i := 0; i < 2000; i++ {
        x += 1
    }
}


func Add(){

    x = 0
    for i := 0; i < 5; i++ {
       go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("WithoutLock:",x)
    
        for i := 0; i < 5; i++ {
       go addWithLock()
    }
    time.Sleep(time.Second)
    println("addWithLock:",x)

}

//最终结果是
//            WithoutLock: 8382
//            WithLock: 10000       

WaitGroup

在快速打印0-4的数字代码中使用了time.Sleep(time.Second),为了保证子协程执行完毕后主协程才会退出,改造 HelloGoRoutine 函数:

func HelloGoRoutine() {
    //声明了一个名为 wg 的 WaitGroup 变量;
    var wg sync.WaitGroup
    
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            //函数末尾再执行
            defer wg.Done()
            hello(j)
        }(i)
    }
    //通过调用 Wait 方法阻塞当前协程,这会使得协程陷入无限的等待
    wg.Wait()
}

依赖管理

演进过程

GOPATH => Go Vendor => Go Module

GOPATH: 项目代码直接依赖src下的代码,go get 下载最新版本的包到src目录下,但是存在无法实现package的多版本控制的弊端

GO Vendor: 项目名目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor,依赖寻址方式:vendor => GOPATH,通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖冲突的问题,存在无法控制依赖的版本和更新项目可能出现依赖冲突导致编译出错的问题

GO Module: 通过go.mod 文件管理依赖包版本,通过go get/go mod指令工具管理依赖包

依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod

测试

单元测试

规则: 所有测试文件以_test.go结尾,函数命名为func TestXxx(t *testing.T),初始化逻辑放到TestMain中

Mock测试

基准测试

项目实战

项目实战是开发一个简易的论坛后端,时间关系暂未完成

引用参考