goroutine和channel

news2024/9/21 20:35:26

进程与线程

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

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

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

并发和并行

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

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

程序控制多线程

在应用中一个app的启动运行相当于一个进程,(进程运行在操作系统上);app内各种按钮的操作相当于线程,线程可以互相通讯。

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

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

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

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)
}

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

goroutine

主线程时物理线程,直接作用再cpu上时重量级的,goroutine是轻量级的。

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

对于调度来说,当前若干程序在一个cpu上运行就是并发,在不同cpu上运行就是并行;

线程(协程通讯)

不同的线程之间有两种通讯方式全局变量加锁,channel管道。

全局变量加锁
如果不对全局变量加锁,线程都在写入会造成数据错误,因此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来解决这些问题。

channel管道

单纯的并发是没有意义的,并发的线程应该是同一为主进程服务的,就需要数据的共享,数据共享时不同线程的数据的操作不同,因此容易造成错误。Go除了锁还提供了goroutine来解决这一问题。

channel是一个队列,数据总是先进先出,因此是线程安全的,多goroutine时,无需加锁。在goroutine操作时数据经过处理进入channel,该数据一直为本次的线程服务,除非线程结束放行数据。(先进先出,上一个不走下一个出不去)

  • 声明

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)

}

在这里插入图片描述

使用channel时,只能存放指定的数据类型;达到数据的容积后就不能在存放数据了;取出最后一个数后也不能在取数据否则会报错。

//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也支持接口类型。

  • 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的任务管理中能够看到这个进程。

在这里插入图片描述

在一个程序中mian函数为程序的入口,运行的所有程序构成一个进程,在一个进程中默认会有一个主要的线程,线程必须运行在进程中。(操作系统的线程与进程)。

每个线程都为进程服务,因此线程的数据必须是共享的,而且是线程安全的,在程序中全局变量为共享的变量,因此如果只用全局变量的话就要涉及安全锁的问题,而Go语言中设计的channel就是为了解决共享数据的问题,从上述程序中可以看出,在使用通道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~200000这些数。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/467343.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何在C中动态分配二维数组

在C语言中如何动态申请连续的二维数组。可以采用多申请一些指针&#xff0c;然后这一些指针分别指向后面数据区中对应的位置&#xff0c;如一个3*4的int类型数组&#xff0c;我们先申请大小为sizeof(int*) * 3 3 * 4 * sizeof(int)的一维数组设为arr。然后arr[0]存放指向arr …

OpenAirInterface通过docker build方式构建images

本文主要讲解如何通过build方式构建各个网元的image&#xff0c;因为直接pull的image无法对其进行更改&#xff0c;而build的镜像可以对其源代码进行编辑更改后生成镜像。 参考链接&#xff1a;OAI build iamges 1.获取正确的网络功能分支 此存储库仅包含教程和持续集成脚本…

有假币与求正数数组的最小不可组成和

一、编程题 1.有假币 链接&#xff1a;有假币__牛客网 (nowcoder.com) 居然有假币&#xff01; 现在猪肉涨了&#xff0c;但是农民的工资却不见涨啊&#xff0c;没钱怎么买猪肉啊。nowcoder这就去买猪肉&#xff0c;结果找来的零钱中有假币&#xff01;&#xff01;&#xff…

爪哇项目maven项目的打包方式

大家好 ~ 前言 现在都是使用idea中maven插件来打包项目&#xff0c;出现了很多问题很多类或者方法、依赖没有打入到包中&#xff0c;发生了很多问题&#xff0c;因此此文章将基于idea中的maven插件打包。 概念 打包分为小包和大包两种概念&#xff1a; 小包&#xff1a;只打…

Conda虚拟环境的复制和迁移

Conda虚拟环境的复制和迁移 在本机复制Conda虚拟环境 conda create --name snapshot --clone myenv相同操作系统之间复制环境 方法一&#xff1a;requirements.txt 这个方法不推荐&#xff0c;因为只会导出你使用pip安装的依赖包&#xff0c;不会导出虚拟环境所依赖的包&am…

SQL应用题分析

SQL应用题的一般考点&#xff1a; 基本表的查询&#xff08;单表、多表、嵌入、分组、排序&#xff09;、向表中插入数据、更新数据、建立视图 关系代数替换sql&#xff0c;使用关系代数来进行查询 基本上就这几点 SQL基本表的插入、更新和建立视图都是死板的&#xff0c;只…

前端开发:CSS中@import指令详解

前言 在前端开发过程中&#xff0c;关于CSS的使用也是非常的&#xff0c;尤其是样式相关的设置等操作。作为前端开发者关于import指令都会有所了解&#xff0c;尤其是在导入CSS样式的时候会用到&#xff0c;但是刚入行不久的前端开发者对应import指令会有所陌生。那么本文就来分…

项目管理:我该怎么安排下属的工作

在刚开始做项目经理的时候&#xff0c;分配任务时会局限于这样的心理&#xff1a;以前我们都是平级的同事&#xff0c;现在我比他们高一个级别了&#xff0c;我说的话他们会不会听啊。 在这个阶段&#xff0c;大部分项目经理都缺乏自信&#xff0c;觉得在项目成员心中威望不足…

Linux shell编程 条件语句if case

条件测试 test命令 测试表达式是否成立&#xff0c;若成立返回0&#xff0c;否则返回其他数值 格式1: test 条件表达式 格式2: [ 条件表达式 ]文件测试 [ 操作符 文件或者目录 ][ -e 1.txt ]#查看1.txt是否存在&#xff0c;存在返回0 echo $? #查看是上一步命令执行结果 0成…

15天学习MySQL计划-存储过程变量判断循环游标函数触发器(进阶篇)-第九天

15天学习MySQL计划-存储过程/变量/判断/循环/游标/函数/触发器&#xff08;进阶篇&#xff09;-第九天 存储过程 1.介绍 ​ 存储过程是事先经过编译并存储是数据库中的一段SQL语句的集合&#xff0c;调用存储过程可以简化应用开发人员的很多工作&#xff0c;减少数据在数据库…

SR常用数据集介绍

1.Download Link&#xff1a; 1.DIV2K: DIV2K Dataset 2.Set5:【实践与问题解决28】最全超分辨率&#xff08;SR&#xff09;数据集介绍以及多方法下载链接_超分辨率数据集_呆呆象呆呆的博客-CSDN博客 3.Set14:【实践与问题解决28】最全超分辨率&#xff08;SR&#xff09;数…

Java --- springboot2的静态资源配置原理

目录 一、静态资源配置原理 1.1、配置类只有一个有参构造器 1.2、资源处理的默认规则 1.3、欢迎页的处理规则 一、静态资源配置原理 springboot启动默认加载xxxAutoConfiguration(自动配置) springmvc功能的自动配置类&#xff0c;生效 Configuration(proxyBeanMethods …

Find My资讯|苹果iOS 17将增强Health应用、引入情绪追踪器

彭博社的马克・古尔曼表示&#xff0c;苹果计划在 iOS / iPadOS 17 系统中&#xff0c;为 iPhone / iPad 用户引入几项和健康相关的新功能。其中最值得关注的一款新应用&#xff0c;可以追踪用户的情绪。 苹果计划进一步增强 Health 应用&#xff0c;引入视力状况相关的功能。…

Python小姿势 - # Python网络爬虫之如何通过selenium模拟浏览器登录微博

Python网络爬虫之如何通过selenium模拟浏览器登录微博 微博登录接口很混乱&#xff0c;需要我们通过selenium来模拟浏览器登录。 首先我们需要安装selenium&#xff0c;通过pip安装&#xff1a; pip install selenium 然后我们需要下载一个浏览器驱动&#xff0c;推荐使用Ch…

【翻译一下官方文档】认识uniCloud云数据库(基础篇)

我将用图文的形式&#xff0c;把市面上优质的课程加以自己的理解&#xff0c;详细的把&#xff1a;创建一个uniCloud的应用&#xff0c;其中的每一步记录出来&#xff0c;方便大家写项目中&#xff0c;做到哪一步不会了&#xff0c;可以轻松翻看文章进行查阅。&#xff08;此文…

虹科活动丨2023第十三届药品质量安全大会精彩回顾

由中国社会科学院食品药品产业发展与监管研究中心、北京中培科检信息技术中心联合主办的2023第十三届&#xff08;春季&#xff09;全国药品质量安全大会暨展览会圆满结束&#xff0c;会上众多优秀企业齐聚一堂&#xff0c;分享独家产品&#xff0c;交流行业知识。 在生物科学…

vCenter(PSC)正常更改或重置administrator@vsphere.local用户的密码方法

1. 正常更改administratorvsphere.local用户密码 在vCenter界面中选择“菜单”下的“系统管理”&#xff0c;如下图所示&#xff1a; 然后在Single Sign On下的用户和组中&#xff0c;选择“vsphere.local”域&#xff0c;再对Administrator用户进行编辑&#xff0c;即可进行…

一图看懂 certifi 模块:证书路径, 资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 certifi 模块&#xff1a;证书路径, 资料整理笔记&#xff08;大全&#xff09; 摘要模块图及类关系图类关系图模块全展开【certifi】统计常量模块1 certifi.core 函数2 co…

【c语言】详解宏定义#define 各种使用方法

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ…

vue+Nodejs+Koa搭建前后端系统(三)--koa-generator项目优化修改

前言 计算机系统为Windows 10 专业版 修改package.json配置 原package.json文件中scripts字段的配置字段为&#xff1a; 在终端运行 npm run dev可能会报错 ‘.’ 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 解决方法就是修改package.json文件中dev…