天天看點

【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].