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/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
  • 相关阅读:
    85. Maximal Rectangle
    120. Triangle
    72. Edit Distance
    39. Combination Sum
    44. Wildcard Matching
    138. Copy List with Random Pointer
    91. Decode Ways
    142. Linked List Cycle II
    异或的性质及应用
    64. Minimum Path Sum
  • 原文地址:https://www.cnblogs.com/luotianshuai/p/14564089.html
Copyright © 2011-2022 走看看