Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go 并发Goroutine

Go 并发编程Goroutine的实现示例

作者:比猪聪明

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情况
*/

三、runtime

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

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

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() 函数的一些使用场景:

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

否则

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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

您可能感兴趣的文章:
阅读全文