天天看点

【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

类型

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

中比较原始,像个花名册一样登记造册:

  • 启动 - 计数+1
  • 执行完毕 - 计数-1
  • 完成

要使用

sync

包:

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/

本文版权归作者共有,未经许可禁止转载,否则保留追究法律责任的权利,若有需要请联系[email protected]

微信公众号

扫描下方二维码关注个人微信公众号,实时获取更多干货

【Golang】快速复习指南QuickReview(八)——goroutine

同步更新至:http://www.randyfield.cn/

出处:http://www.cnblogs.com/RandyField/

本文版权归作者和博客园共有,未经许可禁止转载,否则保留追究法律责任的权利,若有需要请联系[email protected].