【Golang】快速复习指南QuickReview(八)——goroutine
goroutine
是Golang
特有,类似于线程,但是线程是由操作系统进行调度管理,而goroutine
是由Golang
运行时进行调度管理的用户态的线程。
1.C#的线程操作
1.1 创建线程
static void Main(string[] args)
{
Thread thread = new Thread(Count);
thread.IsBackground = true;
thread.Start();
for (int i = 0; i < 10; i++)
Console.Write("x\n");
}
static void Count()
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i); ;
}
}
1.2 向线程传参
Thread
构造函数有两个参数ParameterizedThreadStart
与ThreadStart
public delegate void ParameterizedThreadStart(object? obj);
public delegate void ThreadStart();
没错,一个是无参委托,,一个是有参委托且参数类型为object
,因此我们用以创建线程的方法参数需为object
类型,然后在内部对object
类型参数做一个转换:
static void Main(string[] args)
{
Thread thread = new Thread(Count);
thread.IsBackground = true;
thread.Start(100);
for (int i = 0; i < 10; i++)
Console.Write("x\n");
}
static void Count(object times)
{
int count = (int)times;
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i); ;
}
}
当然最简单的还是直接使用lambda
表达式
static void Main(string[] args)
{
Thread thread = new Thread(()=>{
Count(100);
});
thread.IsBackground = true;
thread.Start();
for (int i = 0; i < 10; i++)
Console.Write("x\n");
}
static void Count(object times)
{
int count = (int)times;
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i); ;
}
}
注意:使用Lambda表达式可以很简单的给Thread传递参数,但是线程开始后,可能会不小心修改了被捕获的变量,这要多加注意。比如循环体中,最好创建一个临时变量。
1.3 线程安全与锁
从单例模式来看线程安全:
class Student
{
private static Student _instance =new Student();
private Student()
{
}
static Student GetInstance()
{
return _instance;
}
}
**单例模式,我们的本意是始终保持类实例化一次然后保证内存中只有一个实例。**上述代码中在类被加载时,就完成静态私有变量的初始化,不管需要与否,都会实例化,这个被称为
饿汉模式的单例模式
。这样虽然没有线程安全问题,但是这个类如果不使用,就不需要实例化。然后便有了下面的写法:需要时,才实例化
class Student
{
private static Student _instance;
private Student()
{
}
static Student GetInstance()
{
if (_instance == null)
_instance = new Student();
return _instance;
}
}
上述代码,调用时判断静态私有变量是否为空,然后再赋值。这个其实就有一个线程安全的问题:多线程调用GetInstance()
,当同时多个线程执行时,条件_instance == null
可能会同时都满足。这样_instance
就完成了多次实例化赋值操作,就引出了我们的锁Lock
private static Student _instance;
private static readonly object locker = new object();
private Student()
{
}
static Student GetInstance()
{
lock (locker)
{
if (_instance == null)
_instance = new Student();
}
return _instance;
}
- 第一个线程运行,就会加锁
Lock
- 第二个线程运行,首先检测到locker对象为"加锁"状态(是否还有线程在
lock
内,未执行完成),该线程就会阻塞等待第一个线程解锁 - 第一个线程执行完
lock
体内代码,解锁,第二个线程才会继续执行
上面看似已经完美了,但是多线程情况下,每次都要经历,检测(是否阻塞),上锁,解锁,其实是很影响性能,我们本来的目的是返回单一实例即可。**我们在检测locker
对象是否加锁之前,如果实例已经存在,那么后续工作是没必要做的。**所以就有了下面的 双重检验:
private static Student _instance;
private static readonly object locker = new object();
private Student()
{
}
static Student GetInstance()
{
if (_instance == null)
{
lock (locker)
{
if (_instance == null)
_instance = new Student();
}
}
return _instance;
}
1.4 Task
线程有两种工作类型:
-
CPU-Bound:计算密集型,花费大部分时间执行CPU密集型工作的操作,这种工作类型永远也不会让线程处在等待状态。
-
IO-Bound:I/O密集型,花费大部分时间等待某事发生的操作,一直等待着,导致线程进入等待状态的工作类型。比如通过http请求对资源的访问。
对于IO-Bound的操作,时间花费主要是在I/O
上,而在CPU
上几乎是没有花费时间。对于此,更推荐使用Task
编写异步代码,而对于CPU-Bound
和IO-Bound
的异步代码不是我们本篇的重点,博主将大概介绍一下Task的优势:
-
不再干等:: 以
HttpClient
使用异步代码请求GetStringAsync
为例,这显然是一个I/O-Bound
,最终会对操作系统本地网络库进行调用,系统API调用,比如发起请求的socket,但是这个时间长短并不是由代码决定,他取决硬件,操作系统,网络情况。控制权会返回给调用者,我们可以做其他操作,这让系统能处理更多的工作而不是等待 I/O 调用结束。直到await
去获得请求结果。 -
让调用者不再干等: 对于
CPU-Bound
,没有办法避免将一个线程用于计算,因为这毕竟是一个计算密集型的工作。但是使用Task
的异步代码(async
与await
)不仅可以与后台线程交互,还可以让调用者继续响应(可以并发执行其他操作)。同上,直到遇到await
时,异步方法都会让步于调用方。
2.Golang的goroutine
2.1 启动goroutine
Golang
中启动一个goroutine
没有C#
的线程那么麻烦,只需要在调用方法的前面加上关键字go
.
func main(){
go Count(100)
}
func Count(times int) {
for i := 0; i < times; i++ {
fmt.Printf("%v\n", i)
}
}
2.2 goroutine的同步
在C#
中的任务(Task
)可以使用Task.WhenAll
来等待Task
对象完成。而在Golang
中就看起来简单粗暴,它将使用sync
包的WaitGroup
,然后像个花名册一样登记造册:
- 启动 - 计数+1
- 执行完毕 - 计数-1
- 完成
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
//登记造册
wg.Add(2)
go Count(100)
go Count(100)
//等待所有登记的goroutine都结束
wg.Wait()
fmt.Println("执行完成")
}
func Count(times int) {
//执行完成
defer wg.Done()
for i := 0; i < times; i++ {
fmt.Printf("%v\n", i)
}
}
2.3 channel通道
一个goroutine
发送特定值到另一个goroutine
的通信机制就是channel
2.3.1 channel声明
channel
是引用类型
var 变量 chan 元素类型
chan 元素类型
跟struct
,interface
一样,就是一种类型。后面的元素类型限定了通道具体存储类型。
2.3.2 channel初始化
声明后的channel
是空值nil
,需要初始化才能使用。
var ch chan int
ch=make(chan int,5) //5为设定的缓冲大小,可选参数
2.3.3 channel操作
操作三板斧:
- send - 发送
ch<-100
箭头从值指向通道
- receive - 接收
value:=<-ch
i, ok := <-ch1 // 通道关闭后再取值ok=false
//或者
for i := range ch1 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
箭头从通道指向变量
- close - 关闭
close(ch)
2.3.3 缓冲与无缓冲
ch1:=make(chan int,5) //5为设定的缓冲大小,可选参数
ch2:=make(chan int)
无缓冲的通道,无缓冲的通道只有在接收值的时候才能发送值。
- 只往通道传值,不从通道接收,就会出现
deadlock
- 只从通道接收,不往淘到发送,也会发生阻塞
使用无缓冲通道进行通信将导致发送和接收的goroutine
同步化。因此,无缓冲通道也被称为同步通道。
有缓冲的通道,可以有效缓解无缓冲通道的尴尬,但是通道装满,上面的尴尬依然存在。
2.3.4 单向通道
限制通道在函数中只能发送或只能接收,单向通道粉墨登场,单向通道的使用是在函数的参数中,也没有引入新的关键字,只是简单的改变的箭头的位置:
chan<- int 只写不读
<-chan int 只读不写
函数传参及任何赋值操作中可以将双向通道转换为单向通道,反之,不行。
2.3.5 多路复用
package main
import "fmt"
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Printf("第%v次,x := <-ch,从通道中读取值%v", i+1, x)
fmt.Println()
case ch <- i:
fmt.Printf("第%v次,执行ch<-i", i+1)
fmt.Println()
}
}
}
第1次,执行ch<-i
第2次,x := <-ch,从通道中读取值0
第3次,执行ch<-i
第4次,x := <-ch,从通道中读取值2
第5次,执行ch<-i
第6次,x := <-ch,从通道中读取值4
第7次,执行ch<-i
第8次,x := <-ch,从通道中读取值6
第9次,执行ch<-i
第10次,x := <-ch,从通道中读取值8
Select
多路复用的规则:
- 可处理一个或多个channel的发送/接收操作。
- 如果多个
case
同时满足,select
会随机选择一个。 - 对于没有
case
的select{}
会一直等待,可用于阻塞main函数。
2.5 并发安全与锁
goroutine是通过channel通道进行通信。不会出现并发安全问题。但是,实际上还是不能完全避免操作公共资源的情况,如果多个goroutine
同时操作这些公共资源,可能就会发生并发安全问题,跟C#的线程一样,锁的出现就是为了解决这个问题:
2.5.1 互斥锁
互斥锁,这个就跟C#
的锁的机制是一样的,一个goroutine访问,另外一个就只能等待互斥锁的释放。同样需要sync
包:
sync.Mutex
var lock sync.Mutex
lock.Lock()//加锁
//操作公共资源
lock.Unlock()//解锁
2.5.2 读写互斥锁
互斥锁是完全互斥的,如果是读多写少,大部分goroutine
都在读,少量的goroutine
在写,这时并发读是没必要加锁的。使用时,依然需要sync
包:
sync.RWMutex
读写锁分为两种:
- 读锁
- 写锁。
import (
"fmt"
"sync"
)
var (
lock sync.Mutex
rwlock sync.RWMutex
)
rwlock.Lock() // 加写锁
//效果等同于互斥锁
rwlock.Unlock() // 解写锁
rwlock.RLock() //加读锁
//可读不可写
rwlock.RUnlock() //解读锁
2.6* sync.Once
goroutine
的同步我们使用过sync.WaitGroup
Add(count int)
计数器累加,在调用goroutine外部执行,由开发人员指定- Done() 计数器-1,在goroutine内部执行
- Wait() 阻塞 直至计数器为0
除此之外还有一个sync.Once
,顾名思义,一次,只执行一次。
func (o *Once) Do(f func()) {}
var handleOnce sync.Once
handleOnce.Do(函数)
sync.Once
其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的,并且初始化操作也不会被执行多次。
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
2.7* sync.Map
Golang
的map不是并发安全的。sync
包中提供了一个开箱即用的并发安全版map–sync.Map
。
开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map
内置了诸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
var m = sync.Map{}
m.Store("四川", "成都")
m.Store("成都", "高新区")
m.Store("高新区", "应龙南一路")
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
v, ok := m.Load("成都")
if ok {
fmt.Println(v)
}
fmt.Println()
value, loaded := m.LoadOrStore("陕西", "西安")
fmt.Println(value)
fmt.Println(loaded)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
//【取值与创建】
//存在就加载,不存在就添加
value1, loaded1 := m.LoadOrStore("四川", "成都")
fmt.Println(value1)
fmt.Println(loaded1)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
//【删除】
//加载并删除 key存在
value2, loaded2 := m.LoadAndDelete("四川")
fmt.Println(value2)
fmt.Println(loaded2)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
//加载并删除 key 不存在
value3, loaded3 := m.LoadAndDelete("北京")
fmt.Println(value3)
fmt.Println(loaded3)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
m.Delete("成都") //内部是调用的LoadAndDelete
//【遍历】
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
k=:四川,v:=成都
k=:成都,v:=高新区
k=:高新区,v:=应龙南一路
高新区
西安
false
k=:四川,v:=成都
k=:成都,v:=高新区
k=:高新区,v:=应龙南一路
k=:陕西,v:=西安
成都
true
k=:四川,v:=成都
k=:成都,v:=高新区
k=:高新区,v:=应龙南一路
k=:陕西,v:=西安
成都
true
k=:陕西,v:=西安
k=:成都,v:=高新区
k=:高新区,v:=应龙南一路
<nil>
false
k=:成都,v:=高新区
k=:高新区,v:=应龙南一路
k=:陕西,v:=西安
k=:高新区,v:=应龙南一路
k=:陕西,v:=西安
2.8 单例模式
综上,sync.Once
其实内部包含一个互斥锁和一个布尔值,这个布尔值就相当于C#单例模式下的双重检验的第一个判断。所以在golang
中可以利用sync.Once
实现单例模式:
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
2.9* 原子操作
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全。Golang
中原子操作由内置的标准库sync/atomic
提供。由于场景较少,就不做介绍,详细操作请自行查阅学习。
再次强调:这个系列并不是教程,如果想系统的学习,博主可推荐学习资源。
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2020-12-09-golang-goroutine/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。