首頁  >  文章  >  後端開發  >  go語言中並發圖文教程

go語言中並發圖文教程

尚
轉載
2019-11-26 11:49:372086瀏覽

go語言中並發圖文教程

正如程式設計和物件導向一樣,一個好的程式模式需要有一個極其簡潔的內核,還有在此之上豐富的外延,可以解決現實世界中各種各樣的問題。本文以GO語言為例,解釋其中內核、外延。

並發模式之內核

這種並發模式的核心只需要協程和通道就夠了。其中協程負責執行程式碼,通道負責在協程之間傳遞事件。

go語言中並發圖文教程

並發程式設計一直以來都是非常困難的工作。要想編寫一個良好的並發程序,我們不得不了解線程, 鎖,semaphore,barrier甚至CPU更新高速緩存的方式,而且他們個個都有怪脾氣,處處是陷阱。筆者除非萬不得以,絕不會自己操作這些底層 並發元素。一個簡潔的並發模式不需要這些複雜的底層元素,只需協程和通道就夠了。

協程是輕量級的執行緒。在過程式編程中,當呼叫一個過程的時候,需要等待其執行完才回傳。而呼叫一個協程的時候,不需要等待其執行完,就會立即回傳。

協程十分輕量,Go語言可以在一個行程中執行有數以十萬計的協程,依舊保持高效能。而對於普通的平台,一個行程有數千個線程,其CPU會忙於上下文切換,效能急劇下降。隨意創建線程可不是一個好主意,但是我們可以大量使用的協程。

通道是協程之間的資料傳輸通道。通道可以在眾多的協程之間傳遞數據,具體可以值也可以是個引用。通道有兩種使用方式。

協程可以試圖向通道放入數據,如果通道滿了,會掛起協程,直到通道可以為他放入數據為止。

協程可以試圖向通道索取數據,如果通道沒有數據,會掛起協程,直到通道返回資料為止。

如此,通道就可以在傳遞資料的同時,控制協程的運作。有點像事件驅動,也有點像阻塞佇列。這兩個概念非常的簡單,各個語言平台都會有相對應的實作。在Java和C上也各有函式庫可以實現兩者。

go語言中並發圖文教程

只要有協程和通道,就可以優雅的解決並發的問題。不必使用其他和並發有關的概念。那如何用這兩把利刃解決各式各樣的實際問題呢?

並發模式之外延

協程相較於線程,可以大量建立。打開這扇門,我們拓展新的用法,可以做生成器,可以讓函數返回“服務”,可以讓循環並發執行,還能共享變數。但是出現新 的用法的同時,也帶來了新的棘手問題,協程也會洩漏,不恰當的使用會影響性能。以下會逐一介紹各種用法和問題。示範的程式碼用GO語言寫成,因為其簡潔明 了,而且支援全部功能。

1、生成器

有的時候,我們需要有一個函數能不斷產生資料。比方說這個函數可以讀文件,讀網絡,生成自增長序列,產生隨機數。這些行為的特徵是,函數的已知一些變量,如檔案路徑。然後不斷調用,返回新的資料。

go語言中並發圖文教程

下面產生隨機數字為例,讓我們做一個會並發執行的隨機數產生器。

// 函数rand_generator_1 ,返回 int
funcrand_generator_1() int {
         return rand.Int()
}
//        上面是一个函数,返回一个int。假如rand.Int()这个函数调用需要很长时间等待,那该函数的调用者也会因此而挂起。所以我们可以创建一个协程,专门执行rand.Int()。


// 函数rand_generator_2,返回通道(Channel)
funcrand_generator_2() chan int {
         // 创建通道
         out := make(chan int)
         // 创建协程
         go func() {
                  for {
                           //向通道内写入数据,如果无人读取会等待
                            out <- rand.Int()
                   }
         }()
         return out
} 
funcmain() {
         // 生成随机数作为一个服务
         rand_service_handler :=rand_generator_2()
         // 从服务中读取随机数并打印
         fmt.Printf("%d\n",<-rand_service_handler)
}

上面的這段函數就可以並發執行了rand.Int()。有一點值得注意到函數的回傳可以理解為一個「服務」。但我們需要取得隨機數據時候,可以隨時向這個 服務取用,他已經為我們準備好了相應的數據,無需等待,隨要隨到。

如果我們呼叫這個服務不是很頻繁,一個協程就夠滿足我們的需求了。但如果我們需要大量訪問,怎麼辦?我們可以用下面介紹的多工技術,啟動若干生成器,然後再整合成一個大的服務。

呼叫生成器,可以傳回一個「服務」。可以用在持續取得數據的場合。用途很廣泛,讀取數據,產生ID,甚至定時器。這是一個非常簡潔的思路,將程     序並發化。

2、多路復用

多路復用是讓一次處理多個佇列的技術。 Apache使用處理每個連線都需要一個進程,所以其並發效能不是很好。而Nginx使用多工的技術,讓一 個進程處理多個連接,所以並發效能比較好。

同樣,在協程的場合,多重化也是需要的,但又有所不同。多路復用可以將若干個相似的小服務整合成一個大服務。

go語言中並發圖文教程

那么让我们用多路复用技术做一个更高并发的随机数生成器吧。

// 函数rand_generator_3 ,返回通道(Channel)
         funcrand_generator_3() chan int {
         // 创建两个随机数生成器服务
         rand_generator_1 := rand_generator_2()
         rand_generator_2 := rand_generator_2()
         //创建通道
         out := make(chan int)
          //创建协程
         go func() {
                   for {
                           //读取生成器1中的数据,整合
                           out <-<-rand_generator_1
                   }
         }()
         go func() {
                   for {
                            //读取生成器2中的数据,整合
                            out <-<-rand_generator_2
                   }
         }()
         return out
}

上面是使用了多路复用技术的高并发版的随机数生成器。通过整合两个随机数生成器,这个版本的能力是刚才的两倍。虽然协程可以大量创建,但是众多协程还是会争抢输出的通道。

Go语言提供了Select关键字来解决,各家也有各家窍门。加大输出通道的缓冲大小是个通用的解决方法。

 多路复用技术可以用来整合多个通道。提升性能和操作的便捷。配合其他的模式使用有很大的威力。

3、Future技术

Future是一个很有用的技术,我们常常使用Future来操作线程。我们可以在使用线程的时候,可以创建一个线程,返回Future,之后可以通过它等待结果。  但是在协程环境下的Future可以更加彻底,输入参数同样可以是Future的。

go語言中並發圖文教程

调用一个函数的时候,往往是参数已经准备好了。调用协程的时候也同样如此。但是如果我们将传入的参 数设为通道,这样我们就可以在不准备好参数的情况下调用函数。这样的设计可以提供很大的自由度和并发度。函数调用和函数参数准备这两个过程可以完全解耦。 下面举一个用该技术访问数据库的例子。

//一个查询结构体
typequery struct {
         //参数Channel
         sql chan string
         //结果Channel
         result chan string
}
//执行Query
funcexecQuery(q query) {
         //启动协程
         go func() {
                   //获取输入
                   sql := <-q.sql
                   //访问数据库,输出结果通道
                   q.result <- "get" + sql
         }()
}
funcmain() {
         //初始化Query
         q :=
                   query{make(chan string, 1),make(chan string, 1)}
         //执行Query,注意执行的时候无需准备参数
         execQuery(q)
         //准备参数
         q.sql <- "select * fromtable"
         //获取结果
         fmt.Println(<-q.result)
}

上面利用Future技术,不单让结果在Future获得,参数也是在Future获取。准备好参数后,自动执行。Future和生成器的区别在 于,Future返回一个结果,而生成器可以重复调用。还有一个值得注意的地方,就是将参数Channel和结果Channel定义在一个结构体里面作为 参数,而不是返回结果Channel。这样做可以增加聚合度,好处就是可以和多路复用技术结合起来使用。

Future技术可以和各个其他技术组合起来用。可以通过多路复用技术,监听多个结果Channel,当有结果后,自动返回。也可以和生成器组合使用,生 成器不断生产数据,Future技术逐个处理数据。Future技术自身还可以首尾相连,形成一个并发的pipe filter。这个pipe filter可以用于读写数据流,操作数据流。

Future是一个非常强大的技术手段。可以在调用的时候不关心数据是否准备好,返回值是否计算好的问题。让程序中的组件在准备好数据的时候自动跑起来。

4、并发循环

循环往往是性能上的热点。如果性能瓶颈出现在CPU上的话,那么九成可能性热点是在一个循环体内部。所以如果能让循环体并发执行,那么性能就会提高很多。

go語言中並發圖文教程

要并发循环很简单,只有在每个循环体内部启动协程。协程作为循环体可以并发执行。调用启动前设置一个计数器,每一个循环体执行完毕就在计数器上加一个元素,调用完成后通过监听计数器等待循环协程全部完成。

//建立计数器
sem :=make(chan int, N);
//FOR循环体
for i,xi:= range data {
         //建立协程
    go func (i int, xi float) {
        doSomething(i,xi);
                   //计数
        sem <- 0;
    } (i, xi);
}
// 等待循环结束
for i := 0; i < N; ++i {
 <-sem }

上面是一个并发循环例子。通过计数器来等待循环全部完成。如果结合上面提到的Future技术的话,则不必等待。可以等到真正需要的结果的地方,再去检查数据是否完成。

通过并发循环可以提供性能,利用多核,解决CPU热点。正因为协程可以大量创建,才能在循环体中如此使用,如果是使用线程的话,就需要引入线程池之类的东西,防止创建过多线程,而协程则简单的多。

5、ChainFilter技术

前面提到了Future技术首尾相连,可以形成一个并发的pipe filter。这种方式可以做很多事情,如果每个Filter都由同一个函数组成,还可以有一种简单的办法把他们连起来。

go語言中並發圖文教程

由于每个Filter协程都可以并发运行,这样的结构非常有利于多核环境。下面是一个例子,用这种模式来产生素数。

// Aconcurrent prime sieve
packagemain
// Sendthe sequence 2, 3, 4, ... to channel &#39;ch&#39;.
funcGenerate(ch chan<- int) {
         for i := 2; ; i++ {
                  ch<- i // Send &#39;i&#39; to channel &#39;ch&#39;.
         }
}
// Copythe values from channel &#39;in&#39; to channel &#39;out&#39;,
//removing those divisible by &#39;prime&#39;.
funcFilter(in <-chan int, out chan<- int, prime int) {
         for {
                   i := <-in // Receive valuefrom &#39;in&#39;.
                   if i%prime != 0 {
                            out <- i // Send&#39;i&#39; to &#39;out&#39;.
                   }
         }
}
// Theprime sieve: Daisy-chain Filter processes.
funcmain() {
         ch := make(chan int) // Create a newchannel.
         go Generate(ch)      // Launch Generate goroutine.
         for i := 0; i < 10; i++ {
                   prime := <-ch
                   print(prime, "\n")
                   ch1 := make(chan int)
                   go Filter(ch, ch1, prime)
                   ch = ch1
         }
}

上面的程序创建了10个Filter,每个分别过滤一个素数,所以可以输出前10个素数。   

Chain-Filter通过简单的代码创建并发的过滤器链。这种办法还有一个好处,就是每个通道只有两个协程会访问,就不会有激烈的竞争,性能会比较好

6、共享变量

协程之间的通信只能够通过通道。但是我们习惯于共享变量,而且很多时候使用共享变量能让代码更简洁。比如一个Server有两个状态开和关。其他仅仅希望获取或改变其状态,那又该如何做呢。可以将这个变量至于0通道中,并使用一个协程来维护。

go語言中並發圖文教程

下面的例子描述如何用这个方式,实现一个共享变量。

//共享变量有一个读通道和一个写通道组成
typesharded_var struct {
         reader chan int
         writer chan int
}
//共享变量维护协程
funcsharded_var_whachdog(v sharded_var) {
         go func() {
                   //初始值
                   var value int = 0
                   for {
                            //监听读写通道,完成服务
                            select {
                            case value =<-v.writer:
                            case v.reader <-value:
                            }
                   }
         }()
}
funcmain() {
         //初始化,并开始维护协程
         v := sharded_var{make(chan int),make(chan int)}
         sharded_var_whachdog(v)
         //读取初始值
         fmt.Println(<-v.reader)
         //写入一个值
         v.writer <- 1
         //读取新写入的值
         fmt.Println(<-v.reader)
}

这样,就可以在协程和通道的基础上实现一个协程安全的共享变量了。定义一个写通道,需要更新变量的时候,往里写新的值。再定义一个读通道,需要读的时候,从里面读。通过一个单独的协程来维护这两个通道。保证数据的一致性。

一般来说,协程之间不推荐使用共享变量来交互,但是按照这个办法,在一些场合,使用共享变量也是可取的。很多平台上有较为原生的共享变量支持,到底用那种 实现比较好,就见仁见智了。另外利用协程和通道,可以还实现各种常见的并发数据结构,如锁等等,就不一一赘述。

7、协程泄漏

协程和内存一样,是系统的资源。对于内存,有自动垃圾回收。但是对于协程,没有相应的回收机制。会不会若干年后,协程普及了,协程泄漏和内存泄漏一样成为 程序员永远的痛呢?

一般而言,协程执行结束后就会销毁。协程也会占用内存,如果发生协程泄漏,影响和内存泄漏一样严重。轻则拖慢程序,重则压垮机器。

C和C++都是没有自动内存回收的程序设计语言,但只要有良好的编程习惯,就能解决规避问题。对于协程是一样的,只要有好习惯就可以了。

只有两种情况会导致协程无法结束。一种情况是协程想从一个通道读数据,但无人往这个通道写入数据,或许这个通道已经被遗忘了。还有一种情况是程想往一个通道写数据,可是由于无人监听这个通道,该协程将永远无法向下执行。下面分别讨论如何避免这两种情况。

对于协程想从一个通道读数据,但无人往这个通道写入数据这种情况。解决的办法很简单,加入超时机制。对于有不确定会不会返回的情况,必须加入超时,避免出 现永久等待。

另外不一定要使用定时器才能终止协程。也可以对外暴露一个退出提醒通道。任何其他协程都可以通过该通道来提醒这个协程终止。

1go語言中並發圖文教程

对于协程想往一个通道写数据,但通道阻塞无法写入这种情况。解决的办法也很简单,就是给通道加缓冲。但前提是这个通道只会接收到固定数目的写入。

比方说, 已知一个通道最多只会接收N次数据,那么就将这个通道的缓冲设置为N。那么该通道将永远不会堵塞,协程自然也不会泄漏。也可以将其缓冲设置为无限,不过这 样就要承担内存泄漏的风险了。等协程执行完毕后,这部分通道内存将会失去引用,会被自动垃圾回收掉。

funcnever_leak(ch chan int) {
         //初始化timeout,缓冲为1
         timeout := make(chan bool, 1)
         //启动timeout协程,由于缓存为1,不可能泄露
         go func() {
                   time.Sleep(1 * time.Second)
                   timeout <- true
         }()
         //监听通道,由于设有超时,不可能泄露
         select {
         case <-ch:
                   // a read from ch hasoccurred
         case <-timeout:
                   // the read from ch has timedout
         }
}

上面是个避免泄漏例子。使用超时避免读堵塞,使用缓冲避免写堵塞。

和内存里面的对象一样,对于长期存在的协程,我们不用担心泄漏问题。一是长期存在,二是数量较少。要警惕的只有那些被临时创建的协程,这些协程数量大且生 命周期短,往往是在循环中创建的,要应用前面提到的办法,避免泄漏发生。协程也是把双刃剑,如果出问题,不但没能提高程序性能,反而会让程序崩溃。但就像 内存一样,同样有泄漏的风险,但越用越溜了。

并发模式之实现

在并发编程大行其道的今天,对协程和通道的支持成为各个平台比不可少的一部分。虽然各家有各家的叫法,但都能满足协程的基本要求—并发执行和可大量创建。笔者对他们的实现方式总结了一下。

下面列举一些已经支持协程的常见的语言和平台。

go語言中並發圖文教程

GoLang 和Scala作为最新的语言,一出生就有完善的基于协程并发功能。Erlang最为老资格的并发编程语言,返老还童。其他二线语言则几乎全部在新的版本中加入了协程。

        令人驚訝的是C/C 和Java這三個世界上最主流的平台沒有在對協程提供語言層級的原生支援。他們都背負著厚重的歷史,無法改變,也無需改變。但他們還有其他的辦法可以使用協程。

Java平台有許多方法實現協程:

 · 修改虛擬機器:對JVM打補丁來實現協程,這樣的實作效果好,但是失去了跨平台的好處

 · 修改字節碼:在編譯完成後增強字節碼,或使用新的JVM語言。稍微增加了編譯的難度。

 · 使用JNI:在Jar套件中使用JNI,這樣容易使用,但不能跨平台。

 · 使用執行緒模擬協程:使協程重量級,完全依賴JVM的執行緒實作。

 其中修改字節碼的方式比較常見。因為這樣的實現辦法,可以平衡性能和移植性。最具代表性的JVM語言Scale就能很好的支援協程並發。流行的Java Actor模型類別庫akka也是用修改字節碼的方式實現的協程。

對於C語言,協程和執行緒一樣。可以使用各種各樣的系統呼叫來實現。協程作為一個比較高階的概念,實現方式實在太多,就不討論了。比較主流的實作有libpcl, coro,lthread等等。

對於C ,有Boost實現,還有一些其他開源函式庫。還有一門名為μC 語言,在C 基礎上提供了並發擴展。

可見這種程式設計模型在眾多的語言平台中已經得到了廣泛的支持,不再小眾。如果想使用的話,隨時可以加到自己的工具箱中。

更多go語言相關文章請關注go語言教學欄位。

以上是go語言中並發圖文教程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除