Go 并发编程Goroutine的实现示例

 更新时间:2024年12月16日 11:16:27   作者:比猪聪明  
Go语言中的并发编程主要通过Goroutine和Channel来实现,本文就来介绍一下Go 并发编程的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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

进程

进程是一个程序在一个数据集中的一次动态执行过程,进程一般由程序,数据集,进程控制块三部分组成

线程

线程也叫轻量级进程,他是一个基本的CPU执行单元,也就是程序执行过程中的最小单元,由线程ID,程序计数器,寄存器集合和堆栈共同组成的,一个进程可以包含多个线程

协程

协程是一种用户态的轻量级线程,又称微线程,协程的调度完全由用户控制

一、主Goroutine

封装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函数

二、Goroutine

GO中使用Goroutine来实现并发

Goroutine是与其他函数或方法同时运行的函数或方法,与线程相比创建goroutine的成本很小,他就是一段代码,一个函数入口,以及在堆上为其分配一个堆栈(初始大小为4k,会随着程序的执行自动增长删除)。

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

package main

import "fmt"

func main() {

	// 使用go关键字使用goroutine调用hello函数
	go hello()
	for i := 0; i < 150000; i++ {
		//fmt.Println("main-", i)
	}
}

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

/*
    此处代码可设置main协程for循环次数的大小观测go协程调用hello情况
*/
  • 当新的Goroutine开始时,Goroutine调用立即返回,与函数不同,go不等待Goroutine执行结束

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

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

三、runtime

  • 获取系统信息

  • schedule调度让出时间片,让别的goroutine先执行

  • Goexit   //终止当前的goroutine 

Go 语言的 runtime 包提供了与 Go 运行时环境交互的各种功能。这个包允许你控制和检查程序的运行时行为,包括但不限于:

  • 垃圾回收(Garbage Collection):可以手动触发垃圾回收,或者调整垃圾回收的策略。

  • 并发控制:提供了包括 Gosched() 在内的方法来控制 goroutine 的调度。

  • 程序退出:可以正常或非正常地退出程序。

  • 堆栈管理:可以获取当前 goroutine 的堆栈信息。

  • 环境变量:读取和设置环境变量。

  • 系统信号:处理操作系统信号。

  • CPU 信息:获取 CPU 的数量和相关信息。

  • 内存分配:可以手动分配和释放内存。

  • 性能监控:可以监控程序的 CPU 使用情况。

以下是一些 runtime 包中常用函数的简要说明:

  • runtime.GOMAXPROCS:设置最大可运行的操作系统线程数。
  • runtime.NumCPU:返回机器的 CPU 核心数。
  • runtime.NumGoroutine:返回当前运行的 goroutine 数量。
  • runtime.Gosched:让出 CPU 时间片,使得其他 goroutine 可以运行。
  • runtime.Goexit:退出当前的 goroutine。
  • runtime.KeepAlive:确保某个 goroutine 不会被垃圾回收。
  • runtime.SetFinalizer:为对象设置终结器,当垃圾回收器准备回收该对象时,会调用该终结器。
  • runtime.GC:强制运行垃圾回收器。 
package main

import (
	"fmt"
	"runtime"
)

func main() {

	//获取系统信息
	fmt.Println("获取GOROOT目录", runtime.GOROOT())
	fmt.Println("获取操作系统", runtime.GOOS)
	fmt.Println("获取CPU", runtime.NumCPU())

	//Goroutine 调度
	go func() {
		for i := 0; i < 100; i++ {
			fmt.Println("Goroutine---", i)
		}
	}()

	for i := 0; i < 100; i++ {
		//让出时间片,让别的Goroutine先执行,不一定可以让成功
		runtime.Gosched()
		fmt.Println("main---", i)
	}
}

runtime.Gosched() 是 Go 语言运行时库中的一个函数,它用于让出 CPU 时间片,让其他 goroutine(轻量级线程)有机会执行。这通常用于避免阻塞或减少阻塞的持续时间,尤其是在长时间运行的 goroutine 中,你可能会在适当的地方调用 Gosched 来让出 CPU,以避免长时间占用 CPU 导致其他 goroutine 饥饿。

以下是 runtime.Gosched() 函数的一些使用场景:

  • 避免饥饿:在长时间运行的循环中,如果确定当前 goroutine 可能不会被阻塞,可以调用 Gosched 来让出 CPU。

  • 控制执行顺序:在某些情况下,你可能希望控制 goroutine 的执行顺序,通过 Gosched 可以给其他 goroutine 运行的机会。

  • 减少 CPU 使用:在某些 I/O 密集型操作中,如果当前 goroutine 主要是等待 I/O 操作完成,调用 Gosched 可以让出 CPU,减少不必要的 CPU 使用。

  • 避免死锁:在某些复杂的 goroutine 调度中,如果担心死锁问题,可以在适当的地方调用 Gosched 来减少死锁的风险。 

package main

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

func main() {
	/*
		因为goroutine2延时了一定时间,如果goroutine1不让出CPU时间片那么必先执行完成
	*/
	go func() {
		for i := 0; i < 5; i++ {
			runtime.Gosched() // 让出 CPU 时间片
			fmt.Println("Goroutine 1:", i)
		}
	}()

	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(100 * time.Millisecond)
			fmt.Println("Goroutine 2:", i)
		}
	}()

	time.Sleep(3 * time.Second) // 等待两个 goroutine 执行完毕
}

请注意,过度使用 Gosched 可能会导致性能下降,因为频繁的调度会消耗额外的 CPU 资源。因此,应该在仔细考虑后,根据实际需要来使用 Gosched

查看协程数&CUP数

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	var wg sync.WaitGroup //创建并发组
	fmt.Printf("当前运行的goroutine数量: %d\n", runtime.NumGoroutine())
	//创建10个协程
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("第 %d 个协程在 running\n", id)
		}(i)
	}
	fmt.Printf("当前运行的goroutine数量: %d\n", runtime.NumGoroutine())
	wg.Wait()

	fmt.Printf("CPU核心数: %d\n", runtime.NumCPU())
}

四、互斥锁 

在并发编程中会遇到的临界资源安全问题,可以采用互斥锁的方式来解决,后面也可通过通过通道channel来解决

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

使用sync包下的锁解决临界资源安全问题(Mutex) 

package main

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

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

// 创建锁
var mutexs sync.Mutex

func main() {
	//三个窗口同时售票
	go saleTicket("售票口1")
	go saleTicket("售票口2")
	go saleTicket("售票口3")
	time.Sleep(time.Second * 15) //等待售票完

}

// 售票函数
func saleTicket(name string) {
	for {
		// 在检查之前上锁
		mutexs.Lock()
		if tickets > 0 {
			time.Sleep(time.Second)
			fmt.Printf("%s剩余的票数为:%d\n", name, tickets)
			tickets--
		} else {
			fmt.Println("票已售完")
			break
		}
		//操作结束后解锁
		mutexs.Unlock()
	}
}

sync包下的同步等待组(WaitGroup)

package main

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

var w sync.WaitGroup

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

	go test11()
	go test22()

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

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

}
func test11() {
	for i := 0; i < 5; i++ {
		time.Sleep(1 * time.Second)
		fmt.Println("test1--", i)
	}
	w.Done()
}
func test22() {
	defer w.Done()
	for i := 0; i < 5; i++ {
		fmt.Println("test2--", i)
	}
}

五、Channel通道 

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

 通道可以被认为是Goroutines通信的管道,类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收,GO语言中建议使用Channel通道来实现Goroutines之间的通信

GO从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信

每个通道都有与其相关的类型,类型是通道允许传输的数据类型(通道的零值为nil,nil通道没有任何用处,因此通道必须使用类似于map和切片的方法定义)

一个通道发送和接收数据默认是阻塞的,当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从通道中读取数据 

关闭通道

发送者可以通过关闭通道来通知接收方不会有更多的数据被发送到通道

close(ch)

 接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已关闭

v,ok := <- ch

当ok的值为true,表示成功的从通道中读取了一个数据value,通道关闭时仍然可以读(存)数据当ok的值为false,表示从一个封闭的通道读取数据,从闭通道读取的数据将是通道类型的零值 

缓冲通道

缓冲通道是指一个通道,带有一个缓冲区,发送到一个缓冲通道只有在缓冲区满时才被阻塞,类似的,从缓冲通道接收的信息只有在为空时才会被阻塞,可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小

package main

import (
	"fmt"
	"strconv"
	"time"
)

func main() {
	//定义通道可以写10个数据
	ch := make(chan string, 10)
	go test3(ch)

	for v := range ch {
		fmt.Println(v)
	}
}

func test3(ch chan string) {
	for i := 0; i < 5; i++ {
		time.Sleep(time.Second)
		fmt.Println("通道内写入数据", "tset--"+strconv.Itoa(i))
		ch <- "tset--" + strconv.Itoa(i)
	}
	close(ch)//如果不关闭协程,主协程的for循环一值阻塞,知道报错“fatal error: all goroutines are asleep - deadlock!”
}

定向通道

单向通道也就是定向通道,这些通道只能发送数据或者接收数据 

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)
	go writerOnly(ch)
	go readOnly(ch)

	time.Sleep(time.Second * 2)
}

// 只读,指只允许管道读入/写出数据
func writerOnly(ch chan<- int) {
	ch <- 10
}

// 只写,指指只允许管道读出/写入数据
func readOnly(ch <-chan int) {
	temp := <-ch
	fmt.Println(temp)
}

Select

  • 每个case都必须是一个通道的操作

  • 如果任意某个通信可以进行,他就执行,其他被忽略

  • 如果有多个case都可以运行,Select会随机公平的选出一个执行

否则

  • 如果有default子句,则执行该语句

  • 如果没有default字句,select将阻塞,直到某个通信可以运行,GO不会重新对channel或值进行求值

package main

import (
	"fmt"
	"time"
)

func main() {

	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(time.Second * 2)
		ch1 <- 100
	}()

	go func() {
		time.Sleep(time.Second * 2)
		ch2 <- 200
	}()

	select {
	case num1 := <-ch1:
		fmt.Println("ch1--", num1)
	case num2 := <-ch2:
		fmt.Println("ch2--", num2)
	}
    //没有default,则等待通道(阻塞),因为select{}本身是阻塞的
}

 利用通道解决临界资源安全问题

package main

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

// 定义全局chan,存储票总数
var totalTickets chan int
var wg sync.WaitGroup

func main() {

	// 初始化票数量:总票数10张
	totalTickets = make(chan int, 2)
	totalTickets <- 10

	wg.Add(3)

	go sell("售票口1")
	go sell("售票口2")
	go sell("售票口3")

	wg.Wait()

	fmt.Println("买完了,下班")

}

func sell(name string) {
	defer wg.Done()

	for { //for循环表示一直在卖,一直在营业
		residue, ok := <-totalTickets
		if !ok {
			fmt.Printf("%s: 关闭\n", name)
			break
		}
		if residue > 0 {
			time.Sleep(time.Second * 1)
			totalTickets <- residue - 1 //
			fmt.Println(name, "售出1张票,余票:", residue)
		} else {
			//进入此处时票已经买完了,因为for循环一进来售票窗口就检查是否还有票,假如最后卖完的是售票口3,那么销售窗口1跟2就会判断还有没有
			fmt.Printf("%s: 关闭\n", name)
			close(totalTickets)
			break
		}
	}
}

到此这篇关于Go 并发编程Goroutine的实现示例的文章就介绍到这了,更多相关Go 并发Goroutine内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Golang中List的实现方法示例详解

    Golang中List的实现方法示例详解

    最近决定复习下Go,所以下面这篇文章主要给大家介绍了关于Golang中List的实现方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-09-09
  • golang中的空接口使用详解

    golang中的空接口使用详解

    这篇文章主要介绍了golang中的空接口使用,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • Go标准库http server的优雅关闭深入理解

    Go标准库http server的优雅关闭深入理解

    这篇文章主要为大家介绍了Go标准库http server的优雅有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪关闭深入理解
    2024-01-01
  • go generate代码自动生成指南

    go generate代码自动生成指南

    这篇文章主要介绍了go generate代码自动生成指南,本文将探讨 go generate 命令的使用方法、原理以及一些实际应用场景,希望读者能够更好地理解和运用这个强大的工具
    2024-01-01
  • GO中的条件变量sync.Cond详解

    GO中的条件变量sync.Cond详解

    条件变量是基于互斥锁的,它必须基于互斥锁才能发挥作用,条件变量的初始化离不开互斥锁,并且它的方法有点也是基于互斥锁的,这篇文章主要介绍了GO的条件变量sync.Cond,需要的朋友可以参考下
    2023-01-01
  • 详解Go语言如何热重载和优雅地关闭程序

    详解Go语言如何热重载和优雅地关闭程序

    我们有时会因不同的目的去关闭服务,一种关闭服务是终止操作系统,一种关闭服务是用来更新配置,本文就来和大家简单讲讲这两种方法的实现吧
    2023-07-07
  • Go 分布式链路追踪实现原理解析

    Go 分布式链路追踪实现原理解析

    分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施,本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践,需要的朋友可以参考下
    2022-06-06
  • Go语言里的结构体文法实例分析

    Go语言里的结构体文法实例分析

    这篇文章主要介绍了Go语言里的结构体文法,实例分析了结构体文法的概念及使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • golang import自定义包方式

    golang import自定义包方式

    这篇文章主要介绍了golang import自定义包方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go基础教程系列之WaitGroup用法实例详解

    Go基础教程系列之WaitGroup用法实例详解

    这篇文章主要介绍了Go基础教程系列之WaitGroup用法实例详解,需要的朋友可以参考下
    2022-04-04

最新评论