zoukankan      html  css  js  c++  java
  • 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函数执行的内容.....
    ")
    }
    
    func run(){
    	for i := 0; i <4 ; i++ {
    		fmt.Printf("goroutine运行中....当前数字:%d
    ", 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
    ", 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
    ", 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
    ", 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 
    ", 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~

    作者:罗天帅
    出处:http://www.cnblogs.com/luotianshuai/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
  • 相关阅读:
    web 服务器
    mysql
    Vue学习之路第二十篇:Vue生命周期函数-组件创建期间的4个钩子函数
    Vue学习之路第十九篇:按键修饰符的使用
    Vue学习之路第十八篇:私有过滤器的使用
    Vue学习之路第十七篇:全局过滤器的使用
    Vue学习之路第十六篇:车型列表的添加、删除与检索项目
    Vue学习之路第十五篇:v-if和v-show指令
    Vue学习之路第十四篇:v-for指令中key的使用注意事项
    Vue学习之路第十三篇:v-for指令
  • 原文地址:https://www.cnblogs.com/luotianshuai/p/14564089.html
Copyright © 2011-2022 走看看