Go-Groutine

Groutine

多线程

普通方法调用 对比 多线程

**普通方法调用:**串行

main(){ // 串行执行 1/2/3
    test1()
    test2()
    test3()
}

多线程

main(){ // 4个线程同时执行:main test1  test2 test3 、交替的快速执行。
   go test1()
   go test2()
   go test3()
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-waWajU8Y-1678112368682)(2023.2.28 Groutine-学相伴.assets/image-20230228202428108.png)]

进程、线程、协程

**程序:**指令和数据的一个有序集合。本身没有任何含义,是一个静态的概念。

进程:QQ.exe 微信 … 一个个的程序、执行起来之后,开启一个进程。执行程序的一次执行过程,它是动态的概念

  • 系统资源分配的单位。

**线程:**一个进程中可以有多个线程,并行的,一个进程之中,至少要有一个线程。main 主线程

  • 线程是CPU调度和执行的单位。

  • 一个线程,直接执行就可以了

  • 多个线程:CPU如何调度执行。 一个CPU、也是可以跑多个线程的。

    • 并发
    • 并行
  • 在代码级别中的所谓多线程并发处理问题。模拟出来的

  • 真正的多线程是指的拥有多个CPU、多核

  • 如果是模拟出来的多线程、即在一个CPU的情况下,在同一个时间点,只能执行一个线程的代码

  • 因为执行的速度很快,所以就有了同时在执行的一种错句。

并行真的就最快吗?

  • 并行运行的组件,考虑到多个线程之间的通信问题,这种跨CPU通信问题是开销比较高的,并行并不一定快。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63VSFO5E-1678112368683)(2023.2.28 Groutine-学相伴.assets/image-20230228202116838.png)]

进程、线程、协程

进程(Process),线程(Thread), 协程(Coroutine, 也叫轻量级线程)

进程

进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序",它是CPU资源分配和调度的独立单位。

进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;

数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大

线程 :

线程是在进程之后发展出来的概念。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、 程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,**缺点是线程没有自己的系统资源,同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。**不过对于某些独占性资源存在锁机制,处理不当可能会产生”死锁"。

协程Goroutine

协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。

就好比是启动了一个函数,单次执行完毕它。不影响我们main线程的执行。

子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级”可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

补充点:Go语言流行的另一个原因,高并发的问题。高效!

Go语言对于并发的实现是靠协程,Goroutine

Goroutine

Go中使用Goroutine来实现并发concurrently

**Goroutine是Go语言特有的名词。**区别于进程Process,线程Thread, 协程Goroutine, 因为Go语言的创造者们觉得和他们是有所区别的,所以专门创造了Goroutine

Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以轻松并发运行数千个Goroutines

在go语言中使用 goroutine,在调用函数或者方法前面加上 go 关键字即可

// 方法调用
main(){
    test()
}
// goroutine 
main(){
   go test()
}

goroutine

package main

import "fmt"

func main() {
   // goroutine : 和普通方法调用完全不同,它是并发执行的,快速交替。
   go hello()
   for i := 0; i < 1000; i++ {
      fmt.Println("main - ", i)
   }
}

// hello函数
func hello() {
   for i := 0; i < 1000; i++ {
      fmt.Println("hello - ", i)
   }
}

我们需要了解Goroutine的规则

1、当新的Goroutine开始时, Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束

2、当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码

3、main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行

主Goroutine - mian

封装main函数的goroutine称为主goroutine。

主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随后,这个go程序的运行也会终止。

此后,主goroutine会 进行一系列的初始化工作,涉及的工作内容大致如下:

1、创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束
2、启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识.
3、执行main包中所引用包下的init函数
4、执行main函数

执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。

程序运行完毕后,主goroutine会结束自己以及当前进程的运行。

runtime包

runtime

package main

import (
	"fmt"
	"runtime"
	"time"
)

// 获取系统的信息runtime
func main() {

	// 终止程序 runtime.Goexit()
	go func() {
		fmt.Println("start")
		runtimetest()
		fmt.Println("end")
	}()

	time.Sleep(time.Second * 3)
}
func runtimetest() {
	defer fmt.Println("test defer")
	//return // 只是终止了函数
	runtime.Goexit() // 终止当前的 goroutine
	fmt.Println("test")
}

func runtime1() {
	// 获取goRoot目录 : 找到指定目录,存放一些项目信息。
	fmt.Println("GoRoot Path:", runtime.GOROOT())
	// 获取操作系统  windows ,判断盘符字符。 “\\”  “/”
	fmt.Println("System:", runtime.GOOS)
	// 获取cpu数量 8, 可以尝试做一些系统优化,开启更大的栈空间。
	fmt.Println("Cpu num:", runtime.NumCPU())
}

func runtime2() {

	// goroutine是竞争cpu的  ,调度
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("goroutine", i)
		}
	}()

	for i := 0; i < 5; i++ {
		// gosched:礼让, 让出时间片,让其他的 goroutine 先执行
		// cpu是随机,相对来说,可以让一下,但是不一定能够成功
		// schedule
		runtime.Gosched()
		fmt.Println("main-", i)
	}
}

其他的方法,不常用,可以自行读源码分析。

多线程会遇到的问题

临界资源的安全问题

临界资源:指并发环境中多个进程、线程、协程共享的资源

在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。

//
func main() {
   // 临界资源:多个协程共享的变量,会导致程序结果位置
   a := 1

   go func() {
      a = 2
      fmt.Println("goroutine a:", a)
   }()

   a = 3
   time.Sleep(3 * time.Second)
   fmt.Println("main a:", a)
}

售票问题

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的

package main

import (
   "fmt"
   "time"
)

// 定义全局变量 票库存为10张
var ticket int = 10

func main() {
   // 单线程不存在问题,多线程资源争抢就出现了问题
   go saleTickets("张三")
   go saleTickets("李四")
   go saleTickets("王五")
   go saleTickets("赵六")

   time.Sleep(time.Second * 5)
}

// 售票函数
func saleTickets(name string) {
   for {
      if ticket > 0 {
         time.Sleep(time.Millisecond * 1)
         fmt.Println(name, "剩余票的数量为:", ticket)
         ticket--
      } else {
         fmt.Println("票已售完")
         break
      }
   }
}

发现结果和预想的不同,多线程加入之后,原先单线程的逻辑出现了问题。

出现了临界资源安全问题。

sync包 - 锁

要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问

我们可以借助于sync包下的锁操作。 synchronization

但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。

共享内存的方式
锁:多个线程拿的是同一个钥匙,go语言不建议使用锁机制来解决。不要以共享内存的方式去通信

而要以通信的方式去共享内存 go语言更建议我们使用 chan 来解决安全问题。

在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多

package main

import (
	"fmt"
	"sync"
	"time"
)

// 定义全局变量 票库存为10张
var ticket int = 10

// 定义一个锁  Mutex 锁头
var mutex sync.Mutex

func main() {
	// 单线程不存在问题,多线程资源争抢就出现了问题
	go saleTickets("张三")
	go saleTickets("李四")
	go saleTickets("王五")
	go saleTickets("赵六")

	time.Sleep(time.Second * 5)
}

// 售票函数
func saleTickets(name string) {
	for {
		// 在拿到共享资源之前先上锁
		mutex.Lock()
		if ticket > 0 {
			time.Sleep(time.Millisecond * 1)
			fmt.Println(name, "剩余票的数量为:", ticket)
			ticket--
		} else {
			// 操作完毕后,解锁
			mutex.Unlock()
			fmt.Println("票已售完")
			break
		}
		// 操作完毕后,解锁
		mutex.Unlock()
	}
}

同步等待组

package main

import (
   "fmt"
   "sync"
   "time"
)

// waitgroup、

var wg sync.WaitGroup

func main() {
   // 公司最后关门的人   0
   // wg.Add(2) 判断还有几个线程、计数  num=2
   // wg.Done() 我告知我已经结束了  -1
   wg.Add(2)

   go test1()
   go test2()

   fmt.Println("main等待ing")
   wg.Wait() // 等待 wg 归零,才会继续向下执行
   fmt.Println("end")

   // 理想状态:所有协程执行完毕之后,自动停止。
   //time.Sleep(3 * time.Second)

}
func test1() {
   for i := 0; i < 10; i++ {
      time.Sleep(3 * time.Second)
      fmt.Println("test1--", i)
   }
   wg.Done()
}
func test2() {
   defer wg.Done()
   for i := 0; i < 10; i++ {
      fmt.Println("test2--", i)
   }
}

不要以共享内存的方式去通信,而要以通信的方式去共享内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值