Goroutine同步

# Goroutine多線程同步

Goroutine是Go語言特有的并發(fā)體,是一種輕量級(jí)的線程孟岛,由go關(guān)鍵字啟動(dòng)。在真實(shí)的Go語言的實(shí)現(xiàn)中,goroutine和系統(tǒng)線程也不是等價(jià)的曲稼。盡管兩者的區(qū)別實(shí)際上只是一個(gè)量的區(qū)別索绪,但正是這個(gè)量變引發(fā)了Go語言并發(fā)編程質(zhì)的飛躍。

首先贫悄,每個(gè)系統(tǒng)級(jí)線程都會(huì)有一個(gè)固定大小的棧(一般默認(rèn)可能是2MB)瑞驱,這個(gè)棧主要用來保存函數(shù)遞歸調(diào)用時(shí)參數(shù)和局部變量。固定了棧的大小導(dǎo)致了兩個(gè)問題:一是對(duì)于很多只需要很小的椪梗空間的線程來說是一個(gè)巨大的浪費(fèi)唤反,二是對(duì)于少數(shù)需要巨大棧空間的線程來說又面臨棧溢出的風(fēng)險(xiǎn)鸭津。針對(duì)這兩個(gè)問題的解決方案是:要么降低固定的棧大小彤侍,提升空間的利用率;要么增大棧的大小以允許更深的函數(shù)遞歸調(diào)用逆趋,但這兩者是沒法同時(shí)兼得的盏阶。相反,一個(gè)Goroutine會(huì)以一個(gè)很小的棧啟動(dòng)(可能是2KB或4KB)闻书,當(dāng)遇到深度遞歸導(dǎo)致當(dāng)前椕澹空間不足時(shí),Goroutine會(huì)根據(jù)需要?jiǎng)討B(tài)地伸縮棧的大衅敲肌(主流實(shí)現(xiàn)中棧的最大值可達(dá)到1GB)砰盐。因?yàn)閱?dòng)的代價(jià)很小,所以我們可以輕易地啟動(dòng)成千上萬個(gè)Goroutine坑律。

Go的運(yùn)行時(shí)還包含了其自己的調(diào)度器楞卡,這個(gè)調(diào)度器使用了一些技術(shù)手段,可以在n個(gè)操作系統(tǒng)線程上多工調(diào)度m個(gè)Goroutine脾歇。Go調(diào)度器的工作和內(nèi)核的調(diào)度是相似的蒋腮,但是這個(gè)調(diào)度器只關(guān)注單獨(dú)的Go程序中的Goroutine。Goroutine采用的是半搶占式的協(xié)作調(diào)度藕各,只有在當(dāng)前Goroutine發(fā)生阻塞時(shí)才會(huì)導(dǎo)致調(diào)度池摧;同時(shí)發(fā)生在用戶態(tài),調(diào)度器會(huì)根據(jù)具體函數(shù)只保存必要的寄存器激况,切換的代價(jià)要比系統(tǒng)線程低得多作彤。運(yùn)行時(shí)有一個(gè)runtime.GOMAXPROCS變量,用于控制當(dāng)前運(yùn)行正常非阻塞Goroutine的系統(tǒng)線程數(shù)目乌逐。

在main.main函數(shù)執(zhí)行之前所有代碼都運(yùn)行在同一個(gè)goroutine竭讳,也就是程序的主系統(tǒng)線程中。因此浙踢,如果某個(gè)init函數(shù)內(nèi)部用go關(guān)鍵字啟動(dòng)了新的goroutine的話绢慢,新的goroutine只有在進(jìn)入main.main函數(shù)之后才可能被執(zhí)行到。

## 基于原子操作的同步

所謂的原子操作就是并發(fā)編程中“最小的且不可并行化”的操作洛波。通常胰舆,如果多個(gè)并發(fā)體對(duì)同一個(gè)共享資源進(jìn)行的操作是原子的話骚露,那么同一時(shí)刻最多只能有一個(gè)并發(fā)體對(duì)該資源進(jìn)行操作。從線程角度看缚窿,在當(dāng)前線程修改共享資源期間棘幸,其它的線程是不能訪問該資源的。原子操作對(duì)于多線程并發(fā)編程模型來說倦零,不會(huì)發(fā)生有別于單線程的意外情況误续,共享資源的完整性可以得到保證。一般情況下扫茅,原子操作都是通過“互斥”訪問來保證的蹋嵌,通常由特殊的CPU指令提供保護(hù)。當(dāng)然诞帐,如果僅僅是想模擬下粗粒度的原子操作欣尼,我們可以借助于sync.Mutex來實(shí)現(xiàn):

```Go

import (

? ? ? ? "sync"

? ? ? ? )

var total struct {

? ? ? ? sync.Mutex

? ? ? ? value int

}

func worker(wg *sync.WaitGroup) {

? ? ? ? defer wg.Done()

? ? ? ? for i := 0; i <= 100; i++ {

? ? ? ? ? ? ? ? total.Lock()

? ? ? ? ? ? ? ? total.value += i

? ? ? ? ? ? ? ? total.Unlock()

? ? ? ? }}

func main() {

? ? ? ? var wg sync.WaitGroup

? ? ? ? wg.Add(2)

? ? ? ? go worker(&wg)

? ? ? ? go worker(&wg)

? ? ? ? wg.Wait()

? ? ? ? fmt.Println(total.value)}

```

在worker的循環(huán)中爆雹,為了保證total.value += i的原子性停蕉,我們通過sync.Mutex加鎖和解鎖來保證該語句在同一時(shí)刻只被一個(gè)線程訪問。對(duì)于多線程模型的程序而言钙态,進(jìn)出臨界區(qū)前后進(jìn)行加鎖和解鎖都是必須的慧起。如果沒有鎖的保護(hù),total的最終值將由于多線程之間的競爭而可能會(huì)不正確册倒。

用互斥鎖來保護(hù)一個(gè)數(shù)值型的共享資源蚓挤,麻煩且效率低下。標(biāo)準(zhǔn)庫的sync/atomic包對(duì)原子操作提供了豐富的支持驻子。我們可以重新實(shí)現(xiàn)上面的例子:

```Go

import (

? ? ? ? "sync"

? ? ? ? "sync/atomic")

var total uint64

func worker(wg *sync.WaitGroup) {

? ? ? ? defer wg.Done()

? ? ? ? var i uint64

? ? ? ? for i = 0; i <= 100; i++ {

? ? ? ? ? ? ? ? atomic.AddUint64(&total, i)

? ? ? ? }}

func main() {

? ? ? ? var wg sync.WaitGroup

? ? ? ? wg.Add(2)

? ? ? ? go worker(&wg)

? ? ? ? go worker(&wg)

? ? ? ? wg.Wait()

}

```

atomic.AddUint64函數(shù)調(diào)用保證了total的讀取灿意、更新和保存是一個(gè)原子操作,因此在多線程中訪問也是安全的崇呵。原子操作配合互斥鎖可以實(shí)現(xiàn)非常高效的單件模式缤剧。互斥鎖的代價(jià)比普通整數(shù)的原子讀寫高很多域慷,在性能敏感的地方可以增加一個(gè)數(shù)字型的標(biāo)志位荒辕,通過原子檢測標(biāo)志位狀態(tài)降低互斥鎖的使用次數(shù)來提高性能。

### 單例模式

基于sync.Once實(shí)現(xiàn)單例模式

```Go

? ? ? ? m? ? Mutex

? ? ? ? done uint32

}

func (o *Once) Do(f func()) {

? ? ? ? if atomic.LoadUint32(&o.done) == 1 {

? ? ? ? ? ? ? ? return

? ? ? ? }

? ? ? ? o.m.Lock()

? ? ? ? defer o.m.Unlock()

? ? ? ? if o.done == 0 {

? ? ? ? ? ? ? ? defer atomic.StoreUint32(&o.done, 1)

? ? ? ? ? ? ? ? f()

? ? ? ? }

}

```

sync/atomic包對(duì)基本的數(shù)值類型及復(fù)雜對(duì)象的讀寫都提供了原子操作的支持犹褒。atomic.Value原子對(duì)象提供了Load和Store兩個(gè)原子方法抵窒,分別用于加載和保存數(shù)據(jù),返回值和參數(shù)都是interface{}類型叠骑,因此可以用于任意的自定義復(fù)雜類型李皇。

```Go

var config atomic.Value // 保存當(dāng)前配置信息// 初始化配置信息

config.Store(loadConfig())// 啟動(dòng)一個(gè)后臺(tái)線程, 加載更新后的配置信息

go func() {

? ? ? ? for {

? ? ? ? ? ? ? ? time.Sleep(time.Second)

? ? ? ? ? ? ? ? config.Store(loadConfig())

? ? ? ? }}()// 用于處理請(qǐng)求的工作者線程始終采用最新的配置信息

? ? ? ? for i := 0; i < 10; i++ {

? ? ? ? ? ? ? ? go func() {

? ? ? ? ? ? ? ? ? ? ? ? for r := range requests() {

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? c := config.Load()

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // ...

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }()

}

```

## 基于Channel的同步

Channel通信是在Goroutine之間進(jìn)行同步的主要方法。在無緩存的Channel上的每一次發(fā)送操作都有與其對(duì)應(yīng)的接收操作相配對(duì)宙枷,發(fā)送和接收操作通常發(fā)生在不同的Goroutine上(在同一個(gè)Goroutine上執(zhí)行2個(gè)操作很容易導(dǎo)致死鎖)疙赠。無緩存的Channel上的發(fā)送操作總在對(duì)應(yīng)的接收操作完成前發(fā)生.

```Go

var done = make(chan bool)

var msg string

func aGoroutine() {

? ? ? ? msg = "你好, 世界"

? ? ? ? done <- true

}

func main() {

? ? ? ? go aGoroutine()

? ? ? ? <-done

? ? ? ? println(msg)

}

```

對(duì)于這個(gè)程序雖然goroutine和main沒有嚴(yán)格的前后關(guān)系付材,但是由于done channel的返送需要發(fā)生在接收之后,這樣就能保證主線程不會(huì)在go沒有執(zhí)行完成之前結(jié)束圃阳⊙嵯危可保證打印出“hello, world”。該程序首先對(duì)msg進(jìn)行寫入捍岳,然后在done管道上發(fā)送同步信號(hào)富寿,隨后從done接收對(duì)應(yīng)的同步信號(hào),最后執(zhí)行println函數(shù)锣夹。若在關(guān)閉Channel后繼續(xù)從中接收數(shù)據(jù)页徐,接收者就會(huì)收到該Channel返回的零值。因此在這個(gè)例子中银萍,用close(c)關(guān)閉管道代替done <- false依然能保證該程序產(chǎn)生相同的行為变勇。但是對(duì)已經(jīng)關(guān)閉的channel發(fā)送數(shù)據(jù)會(huì)導(dǎo)致程序panic,這里建議不關(guān)閉贴唇,channel也可以實(shí)現(xiàn)不同goroutine之間的處理搀绣。

對(duì)于帶緩沖的Channel,對(duì)于Channel的第K個(gè)接收完成操作發(fā)生在第K+C個(gè)發(fā)送操作完成之前戳气,其中C是Channel的緩存大小链患。 如果將C設(shè)置為0自然就對(duì)應(yīng)無緩存的Channel,也即使第K個(gè)接收完成在第K個(gè)發(fā)送完成之前瓶您。因?yàn)闊o緩存的Channel只能同步發(fā)1個(gè)麻捻,也就簡化為前面無緩存Channel的規(guī)則:對(duì)于從無緩沖Channel進(jìn)行的接收,發(fā)生在對(duì)該Channel進(jìn)行的發(fā)送完成之前呀袱。我們可以根據(jù)控制Channel的緩存大小來控制并發(fā)執(zhí)行的Goroutine的最大數(shù)目:

```Go

var work = []func(){

? ? ? ? func() { println("1"); time.Sleep(1 * time.Second) },

? ? ? ? func() { println("2"); time.Sleep(1 * time.Second) },

? ? ? ? func() { println("3"); time.Sleep(1 * time.Second) },

? ? ? ? func() { println("4"); time.Sleep(1 * time.Second) },

? ? ? ? func() { println("5"); time.Sleep(1 * time.Second) },

}

func main() {

? ? ? ? for _, w := range work {

? ? ? ? ? ? ? ? go func(w func()) {

? ? ? ? ? ? ? ? ? ? ? ? limit <- 1

? ? ? ? ? ? ? ? ? ? ? ? w()

? ? ? ? ? ? ? ? ? ? ? ? <-limit

? ? ? ? ? ? ? ? }(w)

? ? ? ? }

? ? ? ? select{}

}

```

在循環(huán)創(chuàng)建Goroutine過程中贸毕,使用了匿名函數(shù)并在函數(shù)中引用了循環(huán)變量w,由于w是引用傳遞的而非值傳遞明棍,因此無法保證Goroutine在運(yùn)行時(shí)調(diào)用的w與循環(huán)創(chuàng)建時(shí)的w是同一個(gè)值油吭,為了解決這個(gè)問題,我們可以利用函數(shù)傳參的值復(fù)制來為每個(gè)Goroutine單獨(dú)復(fù)制一份w婉宰。

同樣的也可以同時(shí)10個(gè)后臺(tái)線程分別打痈璨颉:

```Go

func main() {

? ? ? ? done := make(chan int, 10) // 帶 10 個(gè)緩存

? ? ? ? // 開N個(gè)后臺(tái)打印線程

? ? ? ? for i := 0; i < cap(done); i++ {

? ? ? ? ? ? ? ? go func(){

? ? ? ? ? ? ? ? ? ? ? ? fmt.Println("你好, 世界")

? ? ? ? ? ? ? ? ? ? ? ? done <- 1

? ? ? ? ? ? ? ? }()

? ? ? ? }

? ? ? ? // 等待N個(gè)后臺(tái)線程完成

? ? ? ? for i := 0; i < cap(done); i++ {

? ? ? ? ? ? ? ? <-done

? ? ? ? }

}

```

對(duì)于這種要等待N個(gè)線程完成后再進(jìn)行下一步的同步操作有一個(gè)簡單的做法心包,就是使用sync.WaitGroup來等待一組事件:

```Go

func main() {

? ? ? ? var wg sync.WaitGroup

? ? ? ? // 開N個(gè)后臺(tái)打印線程

? ? ? ? for i := 0; i < 10; i++ {

? ? ? ? ? ? ? ? wg.Add(1)

? ? ? ? ? ? ? ? go func() {

? ? ? ? ? ? ? ? ? ? ? ? fmt.Println("你好, 世界")

? ? ? ? ? ? ? ? ? ? ? ? wg.Done()

? ? ? ? ? ? ? ? }()

? ? ? ? }

? ? ? ? // 等待N個(gè)后臺(tái)線程完成

? ? ? ? wg.Wait()

}

```

中wg.Add(1)用于增加等待事件的個(gè)數(shù),必須確保在后臺(tái)線程啟動(dòng)之前執(zhí)行(如果放到后臺(tái)線程之中執(zhí)行則不能保證被正常執(zhí)行到)。當(dāng)后臺(tái)線程完成打印工作之后痕惋,調(diào)用wg.Done()表示完成一個(gè)事件区宇。main函數(shù)的wg.Wait()是等待全部的事件完成。

循環(huán)創(chuàng)建結(jié)束后议谷,在main函數(shù)中最后一句select{}是一個(gè)空的管道選擇語句堕虹,該語句會(huì)導(dǎo)致main線程阻塞赴捞,從而避免程序過早退出。還有for{}赦政、<-make(chan int)等諸多方法可以達(dá)到類似的效果。因?yàn)閙ain線程被阻塞了桐愉,如果需要程序正常退出的話可以通過調(diào)用os.Exit(0)實(shí)現(xiàn)仅财。

### 生產(chǎn)者消費(fèi)者模型

并發(fā)編程最常見的例子就是生產(chǎn)者消費(fèi)者模式碗淌,該模式主要通過生產(chǎn)和消費(fèi)的能力來提高程序的整體處理數(shù)據(jù)的速度抖锥,簡單來說就是生成數(shù)據(jù)磅废,然后放到成果隊(duì)列中,消費(fèi)者從成果隊(duì)列中獲得數(shù)據(jù)竟趾,這樣將一個(gè)東西拆分解耦成為前置條件的產(chǎn)生和后置消費(fèi)的異步行為宫峦。當(dāng)成果隊(duì)列中沒有數(shù)據(jù)時(shí)导绷,消費(fèi)者就進(jìn)入饑餓的等待中;而當(dāng)成果隊(duì)列中數(shù)據(jù)已滿時(shí),生產(chǎn)者則面臨因產(chǎn)品擠壓導(dǎo)致CPU被剝奪的下崗問題钦购。

Go語言實(shí)現(xiàn)生產(chǎn)者消費(fèi)者并發(fā)很簡單:

```Go

// 生產(chǎn)者: 生成 factor 整數(shù)倍的序列

func Producer(factor int, out chan<- int) {

? ? ? ? for i := 0; ; i++ {

? ? ? ? ? ? ? ? out <- i*factor

? ? ? ? }

}// 消費(fèi)者

func Consumer(in <-chan int) {

? ? ? ? for v := range in {

? ? ? ? ? ? ? ? fmt.Println(v)

? ? ? ? }

}

func main() {

? ? ? ? ch := make(chan int, 64) // 成果隊(duì)列

? ? ? ? go Producer(3, ch) // 生成 3 的倍數(shù)的序列

? ? ? ? go Producer(5, ch) // 生成 5 的倍數(shù)的序列

? ? ? ? go Consumer(ch)? ? // 消費(fèi) 生成的隊(duì)列

? ? ? ? // 運(yùn)行一定時(shí)間后退出

? ? ? ? sig := make(chan os.Signal, 1)

? ? ? ? signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

? ? ? ? fmt.Printf("quit (%v)\n", <-sig)

}

```

我們開啟了2個(gè)Producer生產(chǎn)流水線押桃,分別用于生成3和5的倍數(shù)的序列导犹。然后開啟1個(gè)Consumer消費(fèi)者線程锡足,打印獲取的結(jié)果。我們可以讓main函數(shù)保存阻塞狀態(tài)不退出掰烟,只有當(dāng)用戶輸入Ctrl-C時(shí)才真正退出程序.我們這個(gè)例子中有2個(gè)生產(chǎn)者沐批,并且2個(gè)生產(chǎn)者之間并無同步事件可參考九孩,它們是并發(fā)的。因此煤墙,消費(fèi)者輸出的結(jié)果序列的順序是不確定的宪拥,這并沒有問題,生產(chǎn)者和消費(fèi)者依然可以相互配合工作脚作。

## 觀察者模式

觀察者模式也可以叫做發(fā)布訂閱模式缔刹,在對(duì)象間定義一對(duì)多的依賴關(guān)系校镐,且需要一個(gè)對(duì)象變化的時(shí)候多個(gè)對(duì)象做出相應(yīng)的響應(yīng)則可以使用觀察者模式。對(duì)于被觀察者來說魏烫,他并不關(guān)心誰觀察了自己,只需要發(fā)布自己發(fā)生了變化就可以了稀蟋,這也是觀察者的好處退客,不變的對(duì)象作為被觀察者链嘀,變的對(duì)象作為觀察者,使用注冊(cè)增加或者減少觀察者茫藏,但是對(duì)于被觀察者沒有影響霹琼。

```Go

// 定義消息類型

type MSG string

type ISubject interface {

? Registry(observer IObserver)? ? ? ? // 注冊(cè)觀察

? Remove(observer IObserver)? ? ? ? ? // 取消觀察

? Notify(msg MSG)? ? ? ? ? ? ? ? ? ? // 通知觀察者

}

type IObserver interface {

? Update(msg MSG)? ? ? ? ? ? ? ? ? ? // 當(dāng)觀察對(duì)象發(fā)生變化的時(shí)候進(jìn)行響應(yīng)

? //GetName() string

}

```

具體實(shí)現(xiàn)可以參考如下:

```Go

package main

import "log"

type MSG string //這里簡單使用string作為msg實(shí)際上可以更加復(fù)雜

type ISubject interface {

? Registry(observer IObserver)

? Remove(observer IObserver)

? Notify(msg MSG)

}

type IObserver interface {

? Update(msg MSG)

? GetName() string // getName 不是必須但是為了方便我選擇使用map保存隊(duì)列

}

type Subject struct {

? Observers map[string]IObserver

}

func NewSubject() Subject {

? return Subject{

? ? ? Observers: make(map[string]IObserver),

? }

}

func (s *Subject) Registry(observer IObserver) {

? s.Observers[observer.GetName()] = observer

}

func (s *Subject) Remove(observer IObserver) {

? delete(s.Observers, observer.GetName())

}

func (s *Subject) Notify(msg MSG) {

? for _, v := range s.Observers {

? ? ? v.Update(msg)

? }

}

type Observer struct {

? Name string

}

func (o *Observer) Update(msg MSG) {

? log.Print(o.GetName(), ":")

? log.Printf(

? ? ? "%s", msg)

}

func (o *Observer) GetName() string {

? return o.Name

}

func main() {

? subject := NewSubject()

? o1 := Observer{Name: "observer 1"}

? o2 := Observer{Name: "observer 2"}

? subject.Registry(&o1)

? subject.Registry(&o2)

? subject.Notify("發(fā)生什么事啦售葡!")

}

```

這種方式會(huì)持有對(duì)象忠藤,很多時(shí)候其實(shí)不需要持有對(duì)象模孩,可以使用函數(shù)作為參數(shù)輸入進(jìn)行調(diào)用,將Update作為函數(shù)的call函數(shù)進(jìn)行調(diào)用诺祸,當(dāng)數(shù)據(jù)發(fā)生變化進(jìn)行call調(diào)用祭芦。

更加復(fù)雜的將通知這件事情作為生產(chǎn)內(nèi)容龟劲,將調(diào)用作為消費(fèi)手段實(shí)現(xiàn)異步調(diào)用轴或,就可以實(shí)現(xiàn)異步調(diào)用:

```Go

/**

* @Author:Dijiang

* @Description:

* @Date: Created in 11:25 2022/4/6

* @Modified By: Dijiang

*/

package main

import (

? "fmt"

? "log"

? "os"

? "os/signal"

? "syscall"

)

type MSG string //這里簡單使用string作為msg實(shí)際上可以更加復(fù)雜

type IObserver func()

var msgChan = make(chan IObserver, 5)

type ISubject interface {

? Registry(observer IObserver)

? Remove(observer IObserver)

? Notify(msg MSG)

}

type Subject struct {

? Observers []IObserver

? msgChan? chan func(MSG)

}

func NewSubject() Subject {

? return Subject{

? ? ? Observers: make([]IObserver, 0),

? }

}

func (s *Subject) Registry(observer IObserver) {

? s.Observers = append(s.Observers, observer)

}

func (s *Subject) Remove(observer IObserver) {

? //for i := 0; i < len(s.Observers); i++ {

? // if s.Observers[i] == observer {

? //? ? s.Observers = append(s.Observers[:i], s.Observers[i+1:]...)

? //? ? return

? // }

? //}

? log.Printf("can't find observer %v", observer)

}

func (s *Subject) Notify(msg MSG) {

? for _, v := range s.Observers {

? ? ? msgChan <- v

? }

? //println(len(msgChan))

}

type Observer struct {

? Name string

}

func (o *Observer) GetName() string {

? return o.Name

}

func main() {

? subject := NewSubject()

? o1 := func() {

? ? ? println("01:")

? }

? o2 := func() {

? ? ? println("02: ")

? }

? subject.Registry(o1)

? subject.Registry(o2)

? go func() {

? ? ? var v IObserver

? ? ? for {

? ? ? ? select {

? ? ? ? case v = <-msgChan:

? ? ? ? ? ? v()

? ? ? ? default:

? ? ? ? ? ? continue

? ? ? ? }

? ? ? }

? }()

? subject.Notify("發(fā)生什么事啦蚕愤!")

? subject.Notify("發(fā)生什么事啦!")

? subject.Notify("發(fā)生什么事啦悬嗓!")

? subject.Notify("發(fā)生什么事啦裕坊!")

? subject.Notify("發(fā)生什么事啦!")

? subject.Notify("發(fā)生什么事啦周瞎!")

? // 運(yùn)行一定時(shí)間后退出

? sig := make(chan os.Signal, 1)

? signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

? fmt.Printf("quit (%v)\n", <-sig)

}

```

這樣的簡單改造可以使得觀察者模式更加易用声诸。同時(shí)需要注意到很多的mvvm模式都可以說是這種的觀察者模式退盯,如vue和依賴定時(shí)刷新界面的mvvm模式。(但是個(gè)人感覺而言囤攀,mvvm難點(diǎn)在于數(shù)據(jù)劫持和樹狀依賴的構(gòu)建而不在于觀察者模式)

![](undefined)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末焚挠,一起剝皮案震驚了整個(gè)濱河市漓骚,隨后出現(xiàn)的幾起案子蝌蹂,更是在濱河造成了極大的恐慌,老刑警劉巖剃允,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齐鲤,死亡現(xiàn)場離奇詭異给郊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)统锤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門饲窿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唧席,你說我怎么就攤上這事嘲驾。” “怎么了徒仓?”我有些...
    開封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵掉弛,是天一觀的道長喂走。 經(jīng)常有香客問我芋肠,道長,這世上最難降的妖魔是什么奈惑? 我笑而不...
    開封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任睡汹,我火速辦了婚禮囚巴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晤斩。我一直安慰自己姆坚,他們只是感情好兼呵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著维苔,像睡著了一般懂昂。 火紅的嫁衣襯著肌膚如雪凌彬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天褐澎,我揣著相機(jī)與錄音工三,去河邊找鬼先鱼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛段审,可吹牛的內(nèi)容都是我干的寺枉。 我是一名探鬼主播绷落,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼砌烁,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼站宗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起管呵,我...
    開封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤捐下,失蹤者是張志新(化名)和其女友劉穎萌业,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體廓奕,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桌粉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年番甩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缘薛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宴胧。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恕齐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仪或,到底是詐尸還是另有隱情士骤,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站巨缘,受9級(jí)特大地震影響若锁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜靶病,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一娄周、第九天 我趴在偏房一處隱蔽的房頂上張望沪停。 院中可真熱鬧,春花似錦众辨、人聲如沸舷礼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至笋粟,卻和暖如春析蝴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尝盼。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工东涡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留倘待,地道東北人凸舵。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像渐苏,于是被迫代替她去往敵國和親琼富。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • go語言協(xié)程使用[vscode-webview://45b6830c-5e27-4be5-8359-1ea2a28...
    xcrossed閱讀 1,302評(píng)論 0 0
  • 目錄 1.go 各種代碼運(yùn)行 2.go 在線編輯代碼運(yùn)行 3.通過 Gob 包序列化二進(jìn)制數(shù)據(jù) 4.使用 ...
    楊言錫閱讀 1,118評(píng)論 0 1
  • 在goroutine執(zhí)行的過程中械蹋,需要進(jìn)行g(shù)oroutine的同步。Go語言提供了sync包和channel機(jī)制來...
    高稚商de菌閱讀 948評(píng)論 0 1
  • 開發(fā)go程序的時(shí)候郊艘,時(shí)常需要使用goroutine并發(fā)處理任務(wù)纱注,有時(shí)候這些goroutine是相互獨(dú)立的副渴,而有的時(shí)...
    駐馬聽雪閱讀 2,429評(píng)論 0 21
  • 相信大家在學(xué)習(xí)Go的過程中煮剧,都會(huì)看到類似這樣一句話:"與傳統(tǒng)的系統(tǒng)級(jí)線程和進(jìn)程相比,協(xié)程的最大優(yōu)勢在于其‘輕量級(jí)’...
    我愛張智容閱讀 1,381評(píng)論 0 1