前段時(shí)間嘗試了一點(diǎn) Google 的 Go 語(yǔ)言,感覺其很多特性還是不錯(cuò)的。Go 語(yǔ)言旨在結(jié)合傳統(tǒng)編譯型的靜態(tài)語(yǔ)言和解釋型的動(dòng)態(tài)語(yǔ)言的優(yōu)點(diǎn),在其中找到一個(gè)平衡。從而打造一個(gè)既快速(編譯執(zhí)行),又方便編程的語(yǔ)言(動(dòng)態(tài)語(yǔ)言往往語(yǔ)法簡(jiǎn)單快捷)。同時(shí),Go 語(yǔ)言還具備豐富的特性以支持并發(fā)編程,這在現(xiàn)在多核非常普及的情況下,是很重要和強(qiáng)大的一個(gè)功能。
Go 語(yǔ)言的并發(fā)特性主要有 goroutine, channel 等。goroutine - 可以大致理解為一種輕量級(jí)的線程(或微線程),它是一種“分配在同一個(gè)地址空間內(nèi)的,能夠并行執(zhí)行的函數(shù)”。同時(shí),它是輕量級(jí)的,不需要像分配線程那樣分配獨(dú)立的?臻g。所以理論上講,我們可以很容易的分配很多個(gè) goroutine, 讓它們并發(fā)執(zhí)行,而其開銷則比多線程程序要小得多,從而可以讓程序支持比較大的并發(fā)性。
channel - 顧名思義,就是通道。通道的目的是用來傳遞數(shù)據(jù)。在一個(gè)通道上我們可以執(zhí)行數(shù)據(jù)的發(fā)送(Send)和接受(Receive)操作。對(duì)于非緩沖的 channel 而言,Receive 方法執(zhí)行時(shí),會(huì)判斷該通道上是否有值,如果沒有就會(huì)等待(阻塞),直到有一個(gè)值為止。同樣,在 channel 上有值,而尚未被一個(gè) Receiver 接受的時(shí)候,Send 方法也會(huì)阻塞,直到 Channel 變空。這樣,通過一個(gè)簡(jiǎn)單的機(jī)制就可以保證 Send 和 Receive 總是在不同的時(shí)間執(zhí)行的,而且只有 Send 之后才能 Receive. 這樣就避免了常規(guī)的多線程編程中數(shù)據(jù)共享的問題。正如 Go 語(yǔ)言的文檔一句話所說:
Do not communicate by sharing memory; instead, share memory by communicating.
不要通過共享內(nèi)存來溝通;而是通過溝通來共享內(nèi)存。
在常規(guī)的多線程編程里,我們總是定義好一些類變量,如果這些變量有可能被多個(gè)線程同時(shí)訪問,那么就需要加鎖。這樣帶來了一定的編程復(fù)雜性,如果代碼寫的稍有bug,則會(huì)導(dǎo)致讀/寫到錯(cuò)誤的值。
而通過 channel 來溝通,我們得到了一個(gè)更為清晰的溝通方式。兩個(gè)線程(或者 goroutine)要讀寫相同的數(shù)據(jù),則創(chuàng)建一個(gè)通道,雙方通過對(duì)這個(gè)通道執(zhí)行 Send / Receive 的操作來設(shè)值或取值即可,相對(duì)而言,比較不容易出錯(cuò)。
為了更好的理解這個(gè)原理,我嘗試了在 C# 中實(shí)現(xiàn)類似的功能。
相對(duì)于 goroutine, 我沒有去實(shí)現(xiàn)微線程,因?yàn)檫@需要更復(fù)雜的調(diào)度機(jī)制(打算接下來進(jìn)一步研究這方面)。我們可以暫時(shí)利用 Thread 來簡(jiǎn)單的模擬之。
而 Channel, 則用 Semaphone 控制同步的 Send / Receive 就可以了。
首先讓我們來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Channel,思想上面已經(jīng)說過了:
view sourceprint?01 /// <summary>
02 /// 先實(shí)現(xiàn)簡(jiǎn)單的沒有緩沖的 Channel.
03 /// </summary>
04 /// <typeparam name="T"></typeparam>
05 public class Channel<T>
06 {
07 T _value;
08
09 // 開始不能 Receive.
10 Semaphore _canReceive = new Semaphore(0, 1);
11
12 // 開始沒有值,可以 Send
13 Semaphore _canSend = new Semaphore(1, 1);
14
15 public T Receive()
16 {
17 // 等待有值
18 _canReceive.WaitOne();
19
20 T value = _value;
21
22 // 通知可以發(fā)送新的值了
23 _canSend.Release();
24
25 return value;
26 }
27
28 public void Send(T value)
29 {
30 // 如果是非緩沖的情況,則為阻塞式的,需要等待已有的值被一個(gè) Receiver 接受完,
31 // 才能發(fā)送新值,不能連續(xù) Send
32 _canSend.WaitOne();
33 _value = value;
34
35 // 通知可以接收了
36 _canReceive.Release();
37 }
38 }
接下來粗略的模擬實(shí)現(xiàn) goroutine 的語(yǔ)法:
view sourceprint?01 public static class GoLang
02 {
03 /// <summary>
04 /// 先簡(jiǎn)單的用線程來模擬 goroutine. 因?yàn)槭褂?channel 通信,所以
05 /// 不需考慮線程之間的數(shù)據(jù)共享/同步問題
06 /// </summary>
07 /// <param name="action"></param>
08 public static void go(Action action)
09 {
10 new Thread(new ThreadStart(action)).Start();
11 }
12
13 }
有了這些,我們可以寫一個(gè) test case 來驗(yàn)證了。下面的代碼簡(jiǎn)單的創(chuàng)建一個(gè)并發(fā)的 routine,分別做整數(shù)的 send, receive 操作,以驗(yàn)證是否能正確的發(fā)送和接受值:
view sourceprint?01 /// <summary>
02 /// 測(cè)試多個(gè) Sender 多個(gè) Receiver 同時(shí)在一個(gè) channel 上發(fā)送/接受消息
03 /// </summary>
04 private static void Test1()
05 {
06 var ch = new Channel<int>();
07
08 // 啟動(dòng)多個(gè) Sender
09 GoLang.go(() =>
10 {
11 var id = Thread.CurrentThread.ManagedThreadId;
12 for (var i = 0; i < 7; i++)
13 {
14 Thread.Sleep(new Random((int)DateTime.Now.Ticks).Next(3000));
15 Console.WriteLine("線程{0}發(fā)送值: {1}", id, i);
16 ch.Send(i);
17 }
18 });
19
20 GoLang.go(() =>
21 {
22 var id = Thread.CurrentThread.ManagedThreadId;
23 for (var i = 7; i < 15; i++)
24 {
25 Thread.Sleep(new Random((int)DateTime.Now.Ticks).Next(3000));
26 Console.WriteLine("線程{0}發(fā)送值: {1}", id, i);
27 ch.Send(i);
28 }
29 });
30
31 // 啟動(dòng)多個(gè) Receiver
32 GoLang.go(() =>
33 {
34 var id = Thread.CurrentThread.ManagedThreadId;
35 for (var i = 0; i < 5; i++)
36 {
37 //Console.WriteLine("線程{0}阻塞", id);
38 var value = ch.Receive();
39 Console.WriteLine("線程{0}獲得值: {1}", id, value);
40 }
41 });
42
43 GoLang.go(() =>
44 {
45 var id = Thread.CurrentThread.ManagedThreadId;
46 for (var i = 0; i < 5; i++)
47 {
48 //Console.WriteLine("線程{0}阻塞", id);
49 var value = ch.Receive();
50 Console.WriteLine("線程{0}獲得值: {1}", id, value);
51 }
52 });
53
54 GoLang.go(() =>
55 {
56 var id = Thread.CurrentThread.ManagedThreadId;
57 for (var i = 0; i < 5; i++)
58 {
59 //Console.WriteLine("線程{0}阻塞", id);
60 var value = ch.Receive();
61 Console.WriteLine("線程{0}獲得值: {1}", id, value);
62 }
63 });
64 }
再嘗試實(shí)現(xiàn)一下 Go 語(yǔ)言文檔里舉出的一個(gè)例子 - 篩法求素?cái)?shù):
(見:http://golang.org/doc/go_tutorial.html, Prime numbers)
view sourceprint?01 public class PrimeNumbers
02 {
03 public void Main()
04 {
05 var primes = Sieve();
06
07 // 測(cè)試:打印前100個(gè)素?cái)?shù)
08 for (var i = 0; i < 100; i++)
09 {
10 Console.WriteLine(primes.Receive());
11 }
12 }
13
14 /// <summary>
15 /// 篩法求素?cái)?shù)
16 /// </summary>
17 /// <returns></returns>
18 Channel<int> Sieve()
19 {
20 var @out = new Channel<int>();
21 GoLang.go(() =>
22 {
23 var ch = Generate();
24 for (; ; )
25 {
26 // 當(dāng)前序列中的第一個(gè)值總是素?cái)?shù)
27 var prime = ch.Receive();
28
29 // 將其發(fā)送到輸出序列的尾部
30 @out.Send(prime);
31
32 // 用這個(gè)素?cái)?shù)對(duì)列表進(jìn)行過濾,在進(jìn)入下一次循環(huán),可以保證至少第一個(gè)數(shù)是素?cái)?shù)
33 ch = Filter(ch, prime);
34 }
35 });
36 return @out;
37 }
38
39 /// <summary>
40 /// 產(chǎn)生從2開始的自然數(shù)的無窮序列,這是原始數(shù)列
41 /// 其開始元素 2 是一個(gè)素?cái)?shù)。
42 /// </summary>
43 /// <returns></returns>
44 Channel<int> Generate()
45 {
46 var ch = new Channel<int>();
47 GoLang.go(() =>
48 {
49 for (var i = 2; ; i++)
50 {
51 ch.Send(i);
52 }
53 });
54 return ch;
55 }
56
57 /// <summary>
58 /// 從輸入 channel 里逐個(gè)讀取值,將不能被 prime 整除
59 /// 的那些發(fā)送到輸出 channel (即用 prime 對(duì) @in 序列進(jìn)行一次篩選)
60 /// </summary>
61 Channel<int> Filter(Channel<int> @in, int prime)
62 {
63 var @out = new Channel<int>();
64 GoLang.go(() =>
65 {
66 for (; ; )
67 {
68 var i = @in.Receive();
69 if (i % prime != 0)
70 {
71 @out.Send(i);
72 }
73 }
74 });
75 return @out;
76 }
77 }
下面是整個(gè)測(cè)試工程的 Main 方法:
view sourceprint?01 class Program
02 {
03 static void Main(string[] args)
04 {
05 Test1();
06
07 new PrimeNumbers().Main();
08
09 Console.ReadLine();
10 }
11 }
因?yàn)榇a中已經(jīng)詳細(xì)注釋了,不多做解釋。可以看到,利用 Channel 的概念(好像和 Reactive Programming 有點(diǎn)關(guān)系?),我們可以更清晰的構(gòu)建多線程或者并發(fā)的應(yīng)用程序。
學(xué)習(xí)其他語(yǔ)言,并不是為了學(xué)習(xí)其特定的語(yǔ)法,而是學(xué)習(xí)一種思想。