Go 多线程操作

1,697 阅读3分钟

Goroutine

Go 中使用 Goroutine 来实现并发 (能够简易的实现多线程)

Goroutine 是 Go的特有名词, 区别于进程Process, 线程Thread.

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

如何使用?🧐 在调用函数或者方法前面加上go关键字即可~

package main

import "fmt"

func main() {
	// goroutine 并发执行
	go hello()
	for i := 0; i < 100; i++ {
		fmt.Println("main-", i) // 打印效果是交替执行~
	}
}

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

Goroutine的相关规则

  1. 当新的Goroutine开始时, Goroutine调用就会返回. 与函数不同, go 不会等待Goroutine执行结束
  2. 当Goroutine调用, 且其返回的任何返回值都被忽略之后, go立即执行下一行代码
  3. 如果main的Goroutine终止, 那么程序将被终止

主Goroutine -> main

背后做的事情, 首先是设定每一个goroutine 所能申请栈空间的最大尺寸。

32位计算机 -> 250MB. 64位计算机 -> 1GB。如果申请的空间大于这个限制就会报出栈溢出 -> panic 恐慌, 随后 go程序会终止

然后会进行一系列初始化工作

  1. 创建一个特殊的defer语句, 用于主Goroutine退出时做一些必要的处理
  2. 启动专用于在后台清扫内存垃圾的Goroutine, 并设置GC可用的标识
  3. 执行main包中所引用包里的init函数
  4. 执行main函数~

runtime包

runtime包的使用 -> 能够获取系统信息

package main

import (
	"fmt"
	"runtime"
)

// 获取系统信息 runtime.xxx()
func main() {
	// 获取goRoot目录 (常用于开源项目中~)
	fmt.Println("GoRoot Path:", runtime.GOROOT())
	// 获取操作系统 windows: "//" linux: '' -> 判断盘符
	fmt.Println("System:", runtime.GOOS)
	// 获取cpu数量 -> 可以试着进行程序优化
	fmt.Println("cpu num:", runtime.NumCPU())
	// runtime.Gosched() 礼让, 让出时间片 (Goroutine调度) 配合 Goroutine 使用
	// runtime.Goexit() 即终止当前的Goroutine
}

多线程会遇到的问题

临界资源的安全问题

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

package main

import (
	"fmt"
	"time"
)

func main() {
	// 临界资源
	a := 1
	go func() {
		a = 2
		fmt.Println("goroutine a:", a)
	}()
	a = 3
	time.Sleep(time.Second * 3) // 切换 cpu调度
	fmt.Println("main a:", a)
}

其中有一个多线程的经典问题 -> 售票问题

会出现临界资源争抢的问题, 那么该如何解决 ?

可以通过上锁来解决 -> 通过上锁的方式, 在某一时间段, 只能允许一个 goroutine来访问这个共享数据, 当前goroutine访问完毕, 解锁后, 其他的goroutine才能来访问

借助sync 包下的锁操作~

package main

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

// 定义全局变量
var ticket int = 20

// 定义锁 sync.Mutex 锁头 -> 提供上锁和解锁(这两个常用)
var mutex sync.Mutex

func main() {
	// 多线程资源争取会出现问题
	go saleTicket("sam")
	go saleTicket("lo")
	go saleTicket("a")
	go saleTicket("b")
	time.Sleep(time.Second * 3)
}

// 售票
// 为了解决多进程售票的安全问题, 可以在一个人进入(函数调用)的时候, 先对临界资源上锁, 修改完之后, 再把锁解开 (排队拿)
func saleTicket(name string) {
	for {
		mutex.Lock() // 拿到共享资源之前先上锁 !
		if ticket > 0 {
			time.Sleep(time.Millisecond)
			fmt.Println(name, "进来买票了, 剩余票数", ticket)
			ticket--
		} else {
			fmt.Println("票已卖完")
			break
		}
		mutex.Unlock() // 完成操作 就解锁
	}
}

但实际上, Go并发编程中流传这样一句经典的话: 不要以共享内存的方式去通信, 而要以通信的方式去共享内存

共享内存方式 -> 锁, 通信方式 -> chan(通道)

同步等待组的使用和介绍

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(2) // WaitGroup为2, 即有2个线程去执行
	// wg.Add() 判断还有几个线程去执行, 计数
	// wg.Done() 告知线程已经结束 -> 线程-1
	// 同步等待组
	go test1()
	go test2()
	fmt.Println("main_start")
	wg.Wait() // 等到 wg = 0, 才会继续向下执行
	fmt.Println("main_end")
}
func test1() {
	for i := 0; i < 10; i++ {
		fmt.Println("test1--", i)
	}
	wg.Done()
}

func test2() {
	defer wg.Done() // 这个与test1() 中的 wg.Done() 效果一致
	for i := 0; i < 10; i++ {
		fmt.Println("test2--", i)
	}
}

当然 sync包下有其他锁的使用. 而下一篇笔记是会介绍Go 使用通信的方式(chan 通道)去共享内存~