goroutineGolang特有,类似于线程,但是线程是由操作系统进行调度管理,而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构造函数有两个参数ParameterizedThreadStartThreadStart

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-BoundI/O密集型,花费大部分时间等待某事发生的操作,一直等待着,导致线程进入等待状态的工作类型。比如通过http请求对资源的访问。

对于IO-Bound的操作,时间花费主要是在I/O上,而在CPU上几乎是没有花费时间。对于此,更推荐使用Task编写异步代码,而对于CPU-BoundIO-Bound的异步代码不是我们本篇的重点,博主将大概介绍一下Task的优势:

  • 不再干等:: 以HttpClient使用异步代码请求GetStringAsync为例,这显然是一个I/O-Bound,最终会对操作系统本地网络库进行调用,系统API调用,比如发起请求的socket,但是这个时间长短并不是由代码决定,他取决硬件,操作系统,网络情况。控制权会返回给调用者,我们可以做其他操作,这让系统能处理更多的工作而不是等待 I/O 调用结束。直到await去获得请求结果。

  • 让调用者不再干等: 对于CPU-Bound,没有办法避免将一个线程用于计算,因为这毕竟是一个计算密集型的工作。但是使用Task的异步代码(asyncawait)不仅可以与后台线程交互,还可以让调用者继续响应(可以并发执行其他操作)。同上,直到遇到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 元素类型structinterface一样,就是一种类型。后面的元素类型限定了通道具体存储类型。

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会随机选择一个
  • 对于没有caseselect{}会一直等待,可用于阻塞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内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

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提供。由于场景较少,就不做介绍,详细操作请自行查阅学习。

再次强调:这个系列并不是教程,如果想系统的学习,博主可推荐学习资源。