Go语言学习之路-12-并发(1)-goroutine

概念回顾

进程/线程

进程是程序在操作系统中的一次执行过程,每次程序执行的时候操作系统都会给这个程序打一个标识:资源、ID,它是一个独立的单位
线程是进程的一个执行实体,是 CPU 调度和分派的基本单位

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行

并发/并行

拿两个任务来说:

并发:1个CPU,通过时间的切换来干两件事,同一时刻只有一件事情能做:9:10分任务1在占用CPU,那么任务2就的等待(下图1)
并行:2个CPU,两件事同时做各自用各自的CPU,同一时刻两件事情都在做(下图2)


go语言并发

Go 语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。

为什么是goroutine

资源占用少

  • Linux操作系统栈默认是8M, Go语言层面实现的goroutine会以一个很小的栈开始其生命周期,一般只需要2kb,资源占用很小
  • goroutine栈是动态的最大有1GB当然如果出现这种情况你的程序就有问题了一半情况下用不到
  • 基于上面的情况对于go程序来说,同时创建成百上千个goroutine是非常普遍的

调度更快

  • Linux线程会被操作系统内核调度,有一个硬件计时器需要不断的切换、从寄存器存取数据进行调度
  • Go在运行时包含了其自己的调度器,和操作系统的线程调度不同的是,Go调度器并不是用一个硬件计时器而是被Go语言本身进行调度的。他不需要进行硬件中断和内核调度而是go语言本身进行调度,相对更快

goroutine和线程的关系

goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务启动合理的线程,并合理地分配给每个 CPU。

所以你只需要把任务进行封装,然后调用goroutine对外暴露的函数: go 就可以快速的启动一或者N个goroutine,来帮你完成并发工作

使用goroutine

通过go关键字来调用函数就启用了一个goroutine

go func()

创建goroutine

package main

import (
	"fmt"
	"time"
)

var input string

func main() {
	// 启动一个goroutine,当他运行完逻辑后会正常退出
	go run()
	fmt.Printf("这是main函数执行的内容.....\n")
}

func run(){
	for i := 0; i <4 ; i++ {
		fmt.Printf("goroutine运行中....当前数字:%d\n", i)
		time.Sleep(time.Second)
	}
	fmt.Println("run 函数执行完毕退出!!")
}

现在能做什么

并发获取数据

使用并发前

  1. 比如我有一个获取网站信息的代码
  2. 每次请求花费5秒 "func square"
package main

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

var wg sync.WaitGroup

func main() {
	for i := 1; i < 21; i++ {
		fmt.Printf("%v\n", square(i))
	}
}


func square(i int)(result int){
	time.Sleep(time.Second * 5)
	return i * i
}

使用并发后

package main

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

var wg sync.WaitGroup

func main() {
	for i := 1; i < 21; i++ {
		go func(num int) {
			fmt.Printf("%v\n", square(num))
		}(i)
	}
}


func square(i int)(result int){
	time.Sleep(time.Second * 5)
	return i * i
}

有什么问题

上面这个压测实例,如果不在主函数(main函数)最后增加,下面这一行就会出现问题

	// 这里必须等待几秒钟
	// 因为main函数不会等待goroutine的执行才推出
	// 这个后面我们在并发控制的时候去说如何优化
	time.Sleep(time.Second * 6)

运行上面的函数看看会出现下面的问题: 问题goroutine还没执行完程序就退出了

goroutine还没执行完程序就退出了解决方法

sync.WaitGroup 等goroutine运行完在退出(监工)

之前在没有使用WaitGroup的时候,main函数就想当于老板,老板一离开公司,goroutine就全不干活了
sync.WaitGroup就像老板请的监工,没当来一个goroutine就记录1次,当它干完活在记录一次,直到所有的goroutine都干完活,才下班走人

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	for i := 1; i < 21; i++ {
		go func(num int) {
			fmt.Printf("%v\n", square(num))
			wg.Done()

		}(i)
		wg.Add(1)
	}

	wg.Wait()
}


func square(i int)(result int){
	return i * i
}

sync.WaitGroup 有3个方法(var wg sync.WaitGroup 创建一个wg)

  • wg.Add(1) 每当启动一个goroutine就+1,**知晓当前有多少个goroutine**
  • wg.Done() 在goroutine的运行函数内,最后执行完后 -1,**就知道有多少个goroutine运行完毕了**
  • wg.Wait() 在主main函数内等待,所有的goroutine运行完毕后在退出

上面就是sync.WaitGroup的主要方法和作用

并发的问题

sync.Wait解决了goroutine整体状态退出的问题

并发操作一个变量(把结果进行汇总)出现互相覆盖的问题,看栗子

  • 把并发的结果汇总,写入到一个变量内
  • 然后在进行操作
package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	// 第1步: 创建一个共享变量
	ret := []int{}

	for i := 1; i < 21; i++ {
		// 第2步: 并发请求并把结果写入到共享变量
		go func(num int) {
			ret = append(ret, square(i))
			wg.Done()

		}(i)
		wg.Add(1)
	}

	wg.Wait()
	// 第3步: 把结果进行汇总操作
	fmt.Printf("平方Ret结果是:%v \n", ret)
}


func square(i int)(result int){
	return i * i
}

结果: 平方Ret结果是:[9 144 144 144 144 169 441 441 441] # 这个结果时错误的

当前并发的问题

通过上面的例子我们可以看出来

  1. 存在互相竞争、相互覆盖的问题,所有goroutine都拿到了这个变量然后同时写入,(相互覆盖)最后一个写入的是大家看到的结果

go语言中给的解决方案是:通过通信来解决它

  1. 我不关注goroutine的执行顺序
  2. goroutine也不直接操作最终的变量
  3. goroutine内把执行完的结果通过channel发消息,发出去后,有专门接收的步骤去处理它

这样就可以用生产者消费者模型来处理并发数据,并发请求的相当于生产者,我们在启动一个消费者来把生产的数据进行整理

看下一篇channel~

posted @ 2021-06-05 09:14  天帅  阅读(92)  评论(0编辑  收藏  举报