小工具      在线工具  汉语词典  css  js  c++  java

goroutine和channel

Go,golang,goroutine,channel 额外说明

收录于:152天前

进程与线程

进程是操作系统中程序的执行进程,是系统中资源分配的基本单位。

线程是进程的执行实例,是程序的最小执行单元,是比进程更小的独立运行单元。

一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行。一个程序至少有一个进程,一个进程至少有一个线程。

并发和并行

多线程的程序在单核上运行就是并发。
多线程程序咋在多核上运行就是并行。

并行发生在同一时刻,并发发生在同一时间间隔。

程序控制多线程

在应用程序中,应用程序的启动和运行相当于一个进程(进程运行在操作系统上); App中各种按钮的操作就相当于线程,线程之间可以互相通信。

在Go语言中,主程序相当于一个进程,每个方法相当于一个线程。每个线程可以相互通信,但不同进程之间不能通信。 (协程是轻量级线程)

在Go语言中,协程有独立的栈空间和共享的程序堆空间。调度由开发人员控制。协程是轻量级线程。

Go语言中多线程(协程)是通过goroutine来实现的。但默认是单线程执行。 Go程序的执行顺序总是先执行源文件的init函数并且总是单线程执行,并且按照包依赖的顺序执行。调用其他包时,会递归执行,仍然从init函数开始,最后从主包顺序开始。实施。

var starttime int
var endtime int

func main() {
    
	test()

	for i := 0; i < 5; i++ {
    
		fmt.Println("main~", strconv.Itoa(i))
		//time.Sleep(time.Second)
	}
	endtime = time.Now().Second()
	fmt.Println(endtime - starttime)
}

func test() {
    
	//开始时间
	starttime = time.Now().Second()
	for i := 0; i < 5; i++ {
    
		fmt.Println("test", strconv.Itoa(i))
		//time.Sleep(time.Second)
	}
}

在这里插入图片描述
可以看到单线程的情况是顺序执行,依然很块即使是以秒为单位,也可以忽略不计,在10000次的循环才1秒。

在Go语言中通过go关键字开启goroutine非常的方便,开启线程后就不在是单线程执行程序了,新起的线程会使用另一个处理器支持,其他程序任何在主线程中执行,这样多个线程就并发执行了。

使用多线程时,请确保main的执行时间比其他线程长。否则,主线程将在其他线程完成执行之前停止。这就像一场马拉松。如果你跑得太慢,你不会跑到比赛结束,却发现什么都没有了。

主线程的执行时间必须比其他线程长。以下是错误情况:

func main() {
    
	go test()
	fmt.Println(endtime - starttime)
}

在这里插入图片描述
主线程没有程序,很快结束,新线程还没开始执行,所以啥也没输出。在主线程休眠一秒钟,如下:

func main() {
    
	go test()

	time.Sleep(time.Second)
	fmt.Println(endtime - starttime)
}

在这里插入图片描述
休眠后才有打印,而且看时间差还是负数。

协程

主线程是物理线程,直接应用到CPU上是重量级的,而goroutine则是轻量级的。

MPG是goroutine的调度模型。 MPG:m是操作系统的主线程,p是协程执行所需的上下文,G是协程。

对于调度来说,多个程序在一个CPU上运行是并发,在不同CPU上运行是并行;

线程(协程通讯)

不同线程之间的通信方式有两种:全局变量锁定和通道管道。

全局变量锁定
如果不对全局变量加锁,线程都在写入会造成数据错误,因此Go语言不支持无锁写入数据,通过加互斥所解决问题。

import (
	"container/list"
	"fmt"
	"time"
)

var lis = list.New()

func main() {
    
	//test(10)

	for i := 10; i <= 20; i++ {
    
		go test(i)
	}

	time.Sleep(time.Second * 2)

	for i := lis.Front(); i != nil; i = i.Next() {
    
		fmt.Println(i.Value)
	}
}

func test(n int) {
    
	res := 1
	for i := 1; i < n; i++ {
    
		res = res * i
	}
	lis.PushBack(res)
}

在上面的程序中,通过不同线程执行循环并插入到单列表中list.List,根据单链表的特性,使用尾插法依次插入。但是由于是多线程,那个先执行完谁先插入,另外碰到同一时间完成,或者上一个还未插入完就要执行下一个插入的情况时就会出错。(单链表使线程安全的,这里模拟错误。)

import (
	"container/list"
	"fmt"
	"sync"
	"time"
)

var lis = list.New()

// 声明一个全局锁
// Mutex是互斥的
var lock sync.Mutex

func main() {
    
	//test(10)

	for i := 10; i <= 20; i++ {
    
		go test(i)
	}

	time.Sleep(time.Second * 2)

	//加锁
	lock.Lock()
	for i := lis.Front(); i != nil; i = i.Next() {
    
		fmt.Println(i.Value)
	}
	//解锁
	lock.Unlock()
}

func test(n int) {
    
	res := 1
	for i := 1; i < n; i++ {
    
		res = res * i
	}

	//加锁
	lock.Lock()
	lis.PushBack(res)
	//解锁
	lock.Unlock()
}

sync.Mutex是一个全局的互斥锁,用于对重要数据加锁,在上述代码中lis为全局数据,由于多线程操作需要对其加锁。

  • 写入时锁定
//加锁
	lock.Lock()
	lis.PushBack(res)
	//解锁
	lock.Unlock()
  • 阅读时锁定
	//加锁
	lock.Lock()
	for i := lis.Front(); i != nil; i = i.Next() {
    
		fmt.Println(i.Value)
	}
	//解锁
	lock.Unlock()

全局变量加锁时使用了time.Sleep(time.Second * 2)使所有数据写入完毕,但是实际上主线程是不知道程序写入完毕,在实际中可以会出现一边读一边写入的问题,如果还没写完就在读就会出现脏读,幻读,重复都等问题。因此读写都是需要加锁。

全局锁是不完美了,主线程并不知道全部写入需要多长时间,所以无法设置等待时间(等数据写完),若线程很多,对全局变量读写会很复杂,Go提供了新的通讯方式管道channel来解决这些问题。

通道管

纯粹的并发是没有意义的。并发线程应该服务于同一个主进程,这需要数据共享。数据共享时,不同线程的数据操作不同,因此很容易导致错误。除了锁之外,Go还提供了goroutine来解决这个问题。

Channel是一个队列,数据总是先进先出,所以是线程安全的。当有多个goroutine时,不需要加锁。 goroutine运行过程中,数据被处理并进入通道。数据将始终为当前线程服务,除非线程结束并释放数据。 (先进先出,前一个不走,后一个也出不去)

  • 陈述

var 变量名 chan 数据类型
channel是引用类型,必须初始化才能写入数据,管道也是有类型的。声明管道后必须通过make关键字初始化才可以使用,否则会报错。

var intchan chan int

intchan = make(chan int, 3)

在这里插入图片描述

  • 数据输入

管道通过<-符号插入数据

intchan <- 10
num := 100
intchan <- num
  • 体积

channel放入数据后会有两个长度,容积cap和长度len,分别表示定义的chan的大小和当前chan存储的元素的个数。

//查看容量和数据长度
println(cap(intchan))
println(len(intchan))

管道的体积在定义时就已声明,以后不会改变。然而,存储数据的长度会随着元素的存储和取出而改变。

  • 管道价值
item1 := <-intchan

管道的取值和管道存数据使用的符号一致<-,知识顺序反转,表示取出数据。

item1 := <-intchan

println(item1)
print(len(intchan))

管道是一种队列数据结构,数据以先进先出的方式存储。

func test2() {
    
	var intChan chan int

	intChan = make(chan int, 5)

	var aList []int
	aList = append(aList, 1, 2, 3, 4, 5)
	//print(aList[0])

	intChan <- aList[0]
	intChan <- aList[3]
	intChan <- aList[4]

	a := <-intChan
	b := <-intChan
	c := <-intChan

	fmt.Printf("set data %d-%d-%d,but get data %d-%d-%d", aList[0], aList[3], aList[4], a, b, c)

}

在这里插入图片描述

使用通道时,只能存储指定的数据类型;达到数据量后无法存储数据;获取到最后一个数字后就不能再检索数据,否则会报错。

//map类型的数据

func mapChan() {
    
	var mapChan chan map[string]string

	mapChan = make(chan map[string]string, 5)

	a := map[string]string{
    
		"1": "北极",
		"2": "南极",
	}

	mapChan <- a

	var b map[string]string
	b = <-mapChan
	print(b["1"])
}
//结构体类型

func structChan() {
    

	structChan := make(chan Person, 3)

	per := Person{
    
		1,
		"xiaoxu",
		"男",
		18,
	}

	structChan <- per

	a := <-structChan

	fmt.Print(a)

}

除了上述两种主要数据类型外,channel还支持接口类型。

  • 通道遍历和关闭

chanen也是可以关闭的,Go语言提供了内置函数close来关闭管道。

在这里插入图片描述

close(structChan)

遍历过程中需要关闭管道,否则会报死锁错误。

func bainli() {
    
	var intChan = make(chan int, 101)
	for i := 0; i < 10; i++ {
    
		intChan <- i
	}

	close(intChan)

	for item := range intChan {
    
		fmt.Println(item)
	}
}

管道应用程序,使用管道读取数据。

func main() {
    

	intchan := make(chan int, 5)
	
	go writeData(intchan)
	go readData(intchan)

	time.Sleep(time.Second * 2)

}

func writeData(intChan chan int) {
    
	for i := 0; i < 10; i++ {
    
		intChan <- i
	}
	close(intChan)
}

func readData(intChan chan int) {
    
	for {
    
		a, ok := <-intChan
		if ok {
    
			fmt.Println(a)
		} else {
    
			fmt.Println("读取完毕")
			break
		}
	}

}

在上面的管道中,数据读取

在这里插入图片描述

从管道中读取的数据和写入管道中的数据是不同的,但两边都必须存在。

线程应用

在goroutine和channel之后,实现并发需要将两者结合起来才能实现并行或者说并发。

判断10000一类的所有素数?并将素数相加

上述问题无并发操作的实现方案是通过for循环

go中通过goroutine的实现并发更快捷
package main

import (
	"fmt"
	"time"
)

func main() {
    
	intchan := make(chan int, 100)
	var sum int = 0
	//start := time.Now().UnixMilli()

	write(intchan, 80)
	read(intchan, sum)
	//end := time.Now().UnixMilli() - start
	//fmt.Println("执行时间(微秒)", end)
	// fmt.Println("last sum is :", sum)
	time.Sleep(time.Second * 3)

}

func write(dataChan chan int, n int) {
    
	for i := 1; i < n; i++ {
    
		dataChan <- i
		fmt.Println("写入数据", i)
	}
	close(dataChan)
}

func read(data chan int, sun int) {
    
	sun = 0
	for item := range data {
    
		if sushu(item) != 0 {
    
			sun += item
			fmt.Println("读取数据", item)

		} else {
    
			continue
		}

	}
}

func sushu(a int) int {
    
	for i := 2; i < a; i++ {
    
		if a%i != 0 {
    
			a = a
		} else {
    
			a = 0
		}
	}
	return a
}

线程必须为主线程或进程服务,否则线程就没有意义。

成功读取大素数

在这里插入图片描述

//将主函数不用多线程执行,并记录执行时间,改到基数
func main() {
    
	intchan := make(chan int, 100005)
	var sum int = 0
	start := time.Now().UnixMilli()

	write(intchan, 100000)
	read(intchan, sum)
	end := time.Now().UnixMilli() - start
	fmt.Println("执行时间(微秒)", end)
	// fmt.Println("last sum is :", sum)
	time.Sleep(time.Second * 10)

}

没有线程的执行时间如下:

在这里插入图片描述

记录函数修改的执行情况,对于线程来说,通过记录线程的启动时间和主线程的启动时间进行比较来完成资源调度。分别记录各个线程的执行时间和主线程的执行时间。

package main

import (
	"fmt"
	"time"
)

func main() {
    
	start := time.Now().UnixMilli()
	intchan := make(chan int, 100005)
	var sum int = 0

	fmt.Println("主线程开始时间", time.Now().UnixMilli())
	go write(intchan, 100000)
	go read(intchan, sum)

	mix := time.Now().UnixMilli() - start
	fmt.Println("执行时间(微秒)", mix)
	time.Sleep(time.Second * 2)

}

func write(dataChan chan int, n int) {
    
	fmt.Println("写入线程开始时间", time.Now().UnixMilli())
	start := time.Now().UnixMilli()
	for i := 1; i < n; i++ {
    
		dataChan <- i
		//fmt.Println("写入数据", i)
	}
	close(dataChan)
	end := time.Now().UnixMilli()
	fmt.Println("写入线程执行时间", end-start)
}

func read(data chan int, sun int) {
    
	fmt.Println("读取线程开始时间", time.Now().UnixMilli())
	start := time.Now().UnixMilli()
	sun = 0
	for item := range data {
    
		if sushu(item) != 0 {
    
			sun += item
			//fmt.Println("读取数据", item)

		} else {
    
			continue
		}

	}
	end := time.Now().UnixMilli()
	fmt.Println("读取线程执行时间", end-start)
}

func sushu(a int) int {
    
	for i := 2; i < a; i++ {
    
		if a%i != 0 {
    
			a = a
		} else {
    
			a = 0
		}
	}
	return a
}

在这里插入图片描述
对于改造后的函数执行100000个素数查找,由上图可知读取线程和写入线程同时开启,它们的执行时间各有不同,根据打印显示主线程执行时间为1微妙,写入时间为1微妙,读取的长一点为1670微妙,由于主线程休眠了2秒因此读取线程也正常完成。与单线程的执行时间相比,延长了近5倍。

进程是面向操作系统的,线程是面向程序的。

在上述程序中让主函数休眠2分钟time.Sleep(time.Second * 120),在windows的任务管理中能够看到这个进程。

在这里插入图片描述

在程序中,main函数是程序的入口点。所有正在运行的程序构成一个进程。在一个进程中,默认会有一个主线程,并且该线程必须运行在进程中。 (操作系统的线程和进程)。

每个线程都为进程服务,因此线程的数据必须是共享的、线程安全的。程序中的全局变量是共享变量,所以如果只使用全局变量,就会涉及到安全锁问题,而 Go 语言中设计的通道就是为了解决共享数据的问题。从上面的程序可以看出,使用channel通道时,读和写是分开在线程上操作的,并且使用锁,而且也是线程安全的。

甚至为了开启更多的线程写入数据,读取数据更快,修改了写入方式,进行多线程分段写入。

func write(dataChan chan int, left, right int) {
    
	fmt.Println("写入线程开始时间", time.Now().UnixMilli())
	start := time.Now().UnixMilli()
	for i := left; i < right; i++ {
    
		dataChan <- i
		//fmt.Println("写入数据", i)
	}
	//close(dataChan)
	end := time.Now().UnixMilli()
	fmt.Println("写入线程执行时间", end-start)
}

//多线程写入
go write(intchan, 1, 100000)
go write(intchan, 100001, 200000)

管道是线程安全的。下一篇必须在上一篇写完之后再写。因此,无法判断线程写入数据的顺序,但唯一确定的是,写入完成后,一定是1到20万之间的数字。

从管道读取也是如此,因为管道是队列的数据结构,先入队列的必须出队才能读取下一个数据。因此,从多行读取数据也是安全的。

线程尽量使用deferrecover处理错误,以免程序故障。

//匿名函数处理错误
defer func ()  {
    
	err:= recover()
	if err != nil{
    
		fmt.Println("err",err)
	}
}()
. . .

相关推荐

额外说明

Vue3视频播放器组件Vue3-video-play入门教程

Vue3-video-play适用于 Vue3 的 hls.js 播放器组件 | 并且支持MP4/WebM/Ogg格式。 1、支持快捷键操作 2、支持倍速播放设置 3、支持镜像画面设置 4、支持关灯模式设置 5、支持画中画模式播放 6、支持全屏/网页全屏

额外说明

Java 从当前月份获取上个月

      SimpleDateFormat sd=new SimpleDateFormat("yyyy-MM"); try { String payoffYearMonth = "2018-06";

额外说明

Juc09_CompletableFuture概述、创建方法、常用API以及电商比价要求

文章目录 ①. CompletableFuture概述 ②. CompletableFuture创建方式 ③. CompletableFuture API ①. 获得结果和触发计算(get、getNow、join、complete) ②. 对计算结果进行

额外说明

NLP专栏简介:数据增强、智能标注、意图识别算法|多分类算法、文本分割、文本信息抽取、多模态信息抽取、可解释性分析、性能调优、模型压缩算法等

NLP专栏简介:数据增强、智能标注、意图识别算法|多分类算法、文本信息抽取、多模态信息抽取、可解释性分析、性能调优、模型压缩算法等 专栏链接:NLP领域知识+项目+码源+方案设计 订阅本专栏你能获得什么? 前人栽树后人乘凉,本专栏提供资料:数据增强、智能

额外说明

C语言:文件操作

文件操作 1. 为什么使用文件 2.文件的打开和关闭 2.1文件指针 2..2 文件的打开和关闭 3.文件的顺序读写 3.1 对比一组函数 4.文件的随机读写 4.1 fseek 4.2 ftell 4.3 rewind 5. 文本文件和二进制文件 6.

额外说明

数据库字段和JavaBean字段名不一致解决办法

数据库字段和JavaBean字段名不一致解决办法 数据库字段 u_name JavaBean字段 userName Mabtis–>Mapper解决 一、取别名 <select id="finfAll" resultType="com.bealei.sp

额外说明

MyBatis数据库操作

文章目录 前言 一、MyBatis的各种查询功能 1.查询一个实体类对象 2.查询一个List集合 3.查询单个数据 4.查询一条数据为map集合 5.查询多条数据为map集合 方法一 方法二 6.测试类 二、特殊SQL的执行 1.模糊查询 2.批量删除

额外说明

解决Windows系统目录Chakra.dll文件丢失找不到的问题

其实很多用户玩单机游戏或者安装软件的时候就出现过这种问题,如果是新手第一时间会认为是软件或游戏出错了,其实并不是这样,其主要原因就是你电脑系统的该dll文件丢失了或没有安装一些系统软件平台所需要的动态链接库,这时你可以下载这个Chakra.dll文件(挑

额外说明

WPBeginner成长加速器公司第一轮介绍

In August, we decided to do something that had never been done before in the WordPress community. We created the WP初学者成长加速器, fi

额外说明

实战总结,18种接口优化方案的总结

之前工作中,遇到一个504超时问题。原因是因为接口耗时过长,超过nginx配置的10秒。然后 真枪实弹搞了一次接口性能优化,最后接口从11.3s降为170ms。本文将跟小伙伴们分享接口优化的一些一般的方案。 1.批量思维:数据库的批量操作 优化前: //

ads via 小工具