• Golang并发(Go程、管道)


    基础

    • 并发:电脑同时听歌,看小说,打游戏。cpu根据时间片进行划分,交替执行这三个程序。我们可以感觉是同时产生的。
    • 并行:多个cpu(多核)上述动作同时执行
    • C语言:,实现并发过程使用的是多线程(C++的最小资源单元)
    • Golang:Golang中不是线程,而是Go程(goroutine),Go程是Golang原生支持的,每一个Go程占用的系统资源,远远小于线程,一个Go程大约需要4k到5k的内存资源,一个程序可以启动大量的Go程序。相同的资源下,线程启动几十个,那么Go程是可以启动成百上千个。Go程对于高并发,性能非常好
    • Golang启动Go程,只需要在函数前面加上关键字go即可
    • 启动多个子Go程它们会竞争cpu资源
    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	go func() {
    		count := 1
    		for {
    			fmt.Println("======>我是子go程:", count)
    			count++
    			time.Sleep(time.Second)
    		}
    	}()
    
    	count := 1
    	for {
    		fmt.Println("我是主go程:", count)
    		count++
    		time.Sleep(time.Second)
    	}
    }
    
    

    image

    return、exit、goexit区别

    • return:返回当前函数
    • exit:退出当前进程
    • goexit:提前退出当前go程

    return

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	go func() {
    		func(){
    			fmt.Println("这是子go程的内部函数")
    			return	// 只是返回当前函数,对于上一层的函数会继续执行
    		}()
    		fmt.Println("子go程结束")
    	}()
    
    	fmt.Println("这是主go程")
    	time.Sleep(5 * time.Second)
    	fmt.Println("over")
    }
    
    

    image

    exit

    package main
    
    import (
    	"fmt"
    	"os"
    	"time"
    )
    
    func main() {
    	go func() {
    		func(){
    			fmt.Println("这是子go程的内部函数")
    			os.Exit(-1)		// 退出进程
    		}()
    		fmt.Println("子go程结束")
    	}()
    
    	fmt.Println("这是主go程")
    	time.Sleep(5 * time.Second)
    	fmt.Println("over")
    }
    

    image

    goexit

    package main
    
    import (
    	"fmt"
    	"runtime"
    	"time"
    )
    
    func main() {
    	go func() {
    		func(){
    			fmt.Println("这是子go程的内部函数")
    			runtime.Goexit()	// 退出当前go程
    		}()
    		fmt.Println("子go程结束")
    	}()
    
    	fmt.Println("这是主go程")
    	time.Sleep(5 * time.Second)
    	fmt.Println("over")
    }
    

    image

    多go程通信(channel)

    • 但涉及到多go程时,c语言使用互斥量,上锁来保持资源同步,避免资源竞争问题
    • Golang也支持这种方式,但是go语言更好的解决方案是使用管道、通道
    • 使用通道不需要我们去进行加解锁
    • A往通道里面写数据,B从管道里面读数据,Golang自动帮我们做好了数据同步
    package main
    
    import "fmt"
    
    func main() {
    	// 使用一个管道,一定要make,同map一样,否则为nil
    	// 此时是无缓冲的管道,会写一个读一个,也称无缓存管道
    	numChan := make(chan int)
    	// 如果创建的管道给入容量,那么将会批量写入,也称有缓冲通道
    	// numChan := make(chan int, 10)
    
    	// 创建两个go程,父母写数据,儿子读数据。
    	// 这是go程“儿子”,从管道中读取数据
    	go func() {
    		for i := 0; i < 50; i++ {
    			data := <-numChan
    			fmt.Println("<----这是go程“儿子”,读取数据:", data)
    		}
    	}()
    
    	go func() {
    		for i := 0; i < 20; i++ {
    			// 这是go程“妈妈”,写入20个数据
    			numChan <- i
    			fmt.Println("---->这是go“妈妈”,写入数据:", i)
    		}
    	}()
    
    	for i := 20; i < 50; i++ {
    		// 这是go程“爸爸”,写入30个数据
    		numChan <- i
    		fmt.Println("---->这是go程”爸爸“,写入数据:", i)
    	}
    }
    

    image

    管道的注意点

    管道nil

    如果管道没有使用make分配空间,那么管道默认为nil,读取写入都会阻塞

    package main
    
    import "fmt"
    
    func main() {
    	var numChan chan int
    	numChan <- 1
    	fmt.Println("numChan", <- numChan)
    }
    

    image

    管道死锁

    当管道读写次数不一致的时候,如果阻塞在主go程,那么程序会崩溃,如果阻塞在子go程,那么会出现内存泄露

    package main
    
    import "fmt"
    
    func main() {
    	numChan := make(chan int, 10)
    	
    	// 写入数据到管道
    	go func() {
    		for i := 0; i < 50; i++ {
    			numChan <- i
    			fmt.Println("写入数据:", i)
    		}
    	}()
        
        // 读,当主程序被管道阻塞时,那么程序将锁死崩溃
    	for i := 0; i < 60; i++ {
    		fmt.Println("numChan:", <-numChan)
    	}
    }
    

    image

    for range遍历管道

    for range是不知道管道是否写完,所以会一直等待,一直等待就会导致死锁

    package main
    
    import "fmt"
    
    func main() {
    	numChan := make(chan int, 10)
    	go func() {
    		for i := 0; i < 50; i++ {
    			numChan <- i
    			fmt.Println("写入数据:", i)
    		}
    	}()
    
    	// 遍历管道时,只返回值,不返回坐标
    	for val := range numChan{
    		fmt.Println("读取数据:",val)
    	}
    }
    

    image

    在写入端,将管道关闭,for range遍历关闭管道(nil)时,会退出就不会导致死锁

    package main
    
    import "fmt"
    
    func main() {
    	numChan := make(chan int, 10)
    	go func() {
    		for i := 0; i < 50; i++ {
    			numChan <- i
    			fmt.Println("写入数据:", i)
    		}
    		// 手动关闭管道
    		close(numChan)
    	}()
    
    	for val := range numChan{
    		fmt.Println("读取数据:",val)
    	}
    }
    

    image

    判断管道是否已经关闭

    我们如何知道一个管道的状态,如果已经关闭了,读没事,会返回零值,如果再写入的话会有崩溃风险
    有没有类似于map读取的那种方式val, ok := numMap[0]的这种ok-idiom方式知道呢

    package main
    
    import "fmt"
    
    func main() {
    	numChan := make(chan int, 10)
    	go func() {
    		for i := 0; i < 10; i++ {
    			numChan <- i
    			fmt.Println("写入数据:", i)
    		}
    		// 手动关闭管道
    		close(numChan)
    	}()
    
    	for {
    		val, ok := <-numChan
    		if ok {
    			fmt.Println("读取数据:", val)
    		} else {
    			fmt.Println("管道已经关闭")
    			break
    		}
    	}
    }
    

    image

    单向通道

    • numChan := make(chan int, 10)双向通道,既可以读,也可以写
    • 单向通道:这样的设计是为了明确语义,一般用于函数参数
      • 单向读通道:var numChanReadOnly <- chan int
      • 单向写通道:var numChanWriteOnly chan <- int
    • 双向管道可以赋值给同类型的单向管道,但单向通道不能赋值给同类型的双向通道
    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	// 生产者消费者模型(、producer)
    	// C语言:数组+锁,thread1:写,thread2:读
    	// Golang:goroutine + channel
    
    	// 在主函数中创建一个双向通道
    	numChan := make(chan int, 10)
    	// 双向管道可以赋值给同类型的单向管道
    
    	// 将numChan,传递给producer,负责生产
    	go producer(numChan)
    
    	// 将numChan,传递给consumer 负责消费
    	go consumer(numChan)
    
    	time.Sleep(5 * time.Second)
    
    }
    
    // 生产者,提供一个只写通道
    func producer(write chan<- int) {
    	for i := 0; i < 50; i++ {
    		write <- i
    		// 写管道中不允许读操作
    		// data <- write
    		fmt.Println("向管道中写入数据:", i)
    	}
    }
    
    // 消费者,提供一个只读通道
    func consumer(read <-chan int) {
    	// 读通道不允许写入操作
    	// read <- 12
    	for val := range read {
    		fmt.Println("向管道中写入数据:", val)
    	}
    }
    
    

    image

    管道监听(select)

    当程序中有多个channel协同工作,chan1,chan2,某一时刻,chan1chan2触发了,程序要做出处理,使用select来监听多个通道,当管道被触发时(写入数据、读取数据、关闭管道),select语法与switch case很像,但是所有的分支条件必须是管道io

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	// 启动一个go程,负责监听两个channel
    
    	chan1 := make(chan int)
    	chan2 := make(chan int)
    
    	go func() {
    		for {
    			select {
    			case val := <-chan1:
    				fmt.Println("从chan1读取数据成功:", val)
    			case val2 := <-chan2:
    				fmt.Println("从chan2读取数据成功:", val2)
    			}
    		}
    	}()
    
    	go func() {
    		for i := 0; i < 10; i++ {
    			chan1 <- i
    			time.Sleep(time.Second)
    		}
    	}()
    
    	go func() {
    		for i := 0; i < 10; i++ {
    			chan2 <- i
    			time.Sleep(time.Second)
    		}
    	}()
    
    	for{
    
    	}
    }
    

    image

    管道总结

    • 当管道写满了,写阻塞
    • 当缓冲区读完了,读阻塞
    • 如果管道没有使用make分配空间,管道默认nil
      • 从nil管道读取/写入数据,都会阻塞(不会崩溃)
    • 从一个已经close的管道读取/写入数据时,会返回零值(不会崩溃)
    • 一个管道,如果重复关闭,程序会崩溃
    • 关闭管道的动作,一定要在写管道的操作方执行,不应该放在读端,否则继续写会崩溃
    • 读写通道次数一定要对等
      • 否则在多个go程中,会出现资源泄露
      • 在主go程中,会出现程序崩溃(deadlock)
  • 相关阅读:
    三、一元线性回归--python数据分析--财政收入和国内生产总值之间的线性关系
    三、多因数方差分析==python数据分析--研究一个班三组不同性别的同学(分别接受了三种不同的教学方法)在数学成绩上面是否有显著差异
    二、单因数方差分析--python数据分析---分析四种不同的猪饲料 对 猪体重增加的作用有无不同。
    cda课程--python数据分析与挖掘之---朴素贝叶斯的应用!
    一、独立样本T检验--python数据分析--两种玉米产量是否有差距?
    git
    win7安装
    【日常小问题】windows系统操作技巧
    【OpenGL】如何绘制Shadow
    【图形学】图形管道
  • 原文地址:https://www.cnblogs.com/insipid/p/15354526.html
Copyright © 2020-2023  润新知