1. 程式人生 > >併發技術4:同步排程

併發技術4:同步排程

等待組

  • 在此之前,我們讓主協程等待子協程結束的方式都是睡眠,睡足子協程需要的時間,這種方式顯然是不理想的!
  • 等待組(sync.WaitGroup)的原理是:每增加一個子協程,就向等待組中+1,每結束一個協程,就從等待組中-1,主協程會阻塞等待直到組中的協程數等於0為止;
  • 這種方式可以令主協程恰好結束在最後一個子協程結束的時間點上,Perfect!

在這裡插入圖片描述

互斥鎖案例1

  • 在很多情境中,資料是不允許併發修改的;
  • 典型的案例如銀行賬戶,銀行卡在存取的過程中,存摺是不允許在同一時間進行存取操作的,例如卡剛剛取走500,在查詢餘額時恰好存摺又存入500,銀行卡在查詢餘額時會誤以為銀行並沒有扣款,這顯然是應該避免的!
  • 所以我們不允許銀行卡和存摺併發地執行存取操作,必須同步序列有先後地執行存取,這樣才不會帶來髒讀和幻讀;
  • 我們可以通過搶互斥鎖(sync.Mutex)的方式來強制存取操作同步;
  • 互斥鎖的原理是:對於有必要強制同步序列的任務,我們規定它只有得到互斥鎖才有執行權,而全域性只有一把互斥鎖,誰先搶到誰就獲得任務執行權,任務進行的過程中如果有其它協程想要得到執行權,它必須阻塞等待至當前任務協程釋放同步鎖;
  • 下面的例子中,銀行卡無論誰先搶到資源鎖,都立刻對同步鎖進行鎖定(mt.Lock()),在其存取操作沒有結束之前,另一方必須阻塞等待直至前者將互斥鎖釋放(mt.Unlock());
package main

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

func main() {

	//必須保證併發安全的資料
	type Account struct {
		money float32
	}

	var wg sync.WaitGroup
	account := Account{1000}
	fmt.Println(account)

	//資源互斥鎖(誰搶到鎖,誰先訪問資源,其他人阻塞等待)
	//全域性就這麼一把鎖,誰先搶到誰操作,其他人被阻塞直到鎖釋放
	var mt sync.Mutex

	//銀行卡取錢
	wg.Add(1)
	go func() {
		//拿到互斥鎖
		mt.Lock()

		//加鎖的訪問
		fmt.Println("取錢前:",account.money)
		account.money -= 500
		time.Sleep(time.Nanosecond)
		fmt.Println("取錢後:",account.money)
		wg.Done()

		//釋放互斥鎖
		mt.Unlock()
	}()

	//存摺存錢
	wg.Add(1)
	go func() {
		//拿到互斥鎖(如果別人先搶到,則阻塞等待)
		mt.Lock()

		fmt.Println("存錢前:",account.money)
		account.money += 500
		time.Sleep(time.Nanosecond)
		fmt.Println("存錢後:",account.money)
		wg.Done()

		//釋放互斥鎖
		mt.Unlock()
	}()

	wg.Wait()
}

互斥鎖案例2

  • 在上面的例子中,銀行卡和存摺的存取操作,必須強制同步,否則會形成資料的髒讀或幻讀;
  • 但如果是查詢上個月的銀行流水或者僅僅是查詢使用者名稱之類的只讀操作,則沒有強制同步的必要,完全可以併發執行;
  • 於是我們對上面的例子稍作修改,使得對銀行賬戶的強制同步僅限於存取操作,而對於其他操作則放開許可權令其可以被併發地執行;
  • 原理很簡單:沒有必要強制同步的任務,不去搶互斥鎖就是了——需要確保同步的任務就先搶鎖後執行,其餘的則不去搶鎖,直接執行;
package main

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

//必須保證併發安全的資料
type Account struct {
	name  string
	money float32

	//定義該資料的互斥鎖
	mt    sync.Mutex
}

//本方法不能被併發執行——併發安全的
func (a *Account) saveGet(amount float32) {
	//先將資源鎖起來
	a.mt.Lock()

	//執行操作
	fmt.Println("操作前:", a.money)
	a.money += amount
	fmt.Println("操作後:", a.money)
	<-time.After(3 * time.Second)

	//釋放資源
	a.mt.Unlock()
}

//本方法可以被併發執行——不是併發安全的,無此必要
func (a *Account) getName() string {
	return a.name
}

func main() {
	a := Account{name: "張全蛋", money: 1000}

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		//呼叫一個加鎖的方法(同步)
		a.saveGet(500)
		wg.Done()
	}()

	wg.Add(1)
	go func() {
		//呼叫一個加鎖的方法(同步)
		a.saveGet(-500)
		wg.Done()
	}()

	for i:=0;i<3 ;i++  {
		wg.Add(1)
		go func() {
			//呼叫一個普通的沒有訪問鎖的方法(非同步)
			fmt.Println(a.getName())
			wg.Done()
		}()
	}

	wg.Wait()
}

通過訊號量控制併發數

  • 控制併發數屬於常用的排程;
  • 我們的做法是:規定併發執行的任務都必須先在某個監視管道中進行註冊,而這個監視管道的快取能力是固定的,比如說5,那麼註冊在該管道中的併發能力就是5;
package main

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

/*訊號量:通過控制管道的“頻寬”(快取能力)控制併發數*/

func main() {

	//定義訊號量為5“頻寬”的管道
	sema = make(chan int, 5)

	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(index int) {
			ret := getPingfangshu(index)
			fmt.Println(index, ret)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

//該函式只允許5併發執行
var sema chan int
func getPingfangshu(i int) int {
	sema <- 1
	<-time.After(2 * time.Second)
	<- sema
	return i
}

[清華團隊帶你實戰區塊鏈開發]