https://developer.apple.com/metal/tensorflow-plugin/
過程中會自動下載cifar100數據集,不大,才169MB。
在我自己的MacBook上,由於可以自由設定DNS與特殊上網方法,所以自動下載絲滑的完成。但在服務器上就相當麻煩了,由於你懂的原因,會卡住。
有很多網頁都提供這個連結:https://mirrors.bfsu.edu.cn/osdn//datasets/74526/cifar-100-python.tar.gz ,但其實是失效的
scp上傳下載好的數據集到服務器的/tmp裏面
scp ~/Downloads/cifar-100-python.tar.gz user@ip:/tmp
vim修改/usr/local/anaconda3/envs/base/lib/python3.7/site-packages/keras/datasets/cifar100.py
下圖反白就是添加的程式碼
結果…
raise ValueError("unknown url type: %r" % self.full_url)ValueError: unknown url type: '/tmp/cifar-100-python.tar.gz'
不認得本地路徑
cp /tmp/cifar-100-python.tar.gz /usr/local/anaconda3/envs/base/lib/python3.7/site-packages/keras/datasets/
結果照樣到原網址下載卡住,毫無變化
改成如上圖,加上file://
使本機路徑與網址有一樣的格式,就能正常跑了。
結果如下:
參考文獻:
正解:
我嘗試了沒有成功的方法:
]]>1 | class LeastSq3D: |
pts 就是所有的三維點,pts[0] 就是第一個點,pts[0][0] 就是第一個點的 x 座標,依此類推。
就是重現上一篇文章以 Ax=b 求解的過程。
首先初始化矩陣 A
1 | A = [ |
1 | m11 = np.sum(pts[:, 0] * pts[:, 0]) |
再來初始化矩陣 b
1 | b = [xz, yz, z] |
1 | b1 = np.sum(pts[:, 0] * pts[:, 2]) |
最後求A的反矩陣,並且乘上 b,就是最後的結果,把係數 A,B,C 存在 self.coeficient
。
1 | self.coeficient = np.dot(np.linalg.inv(A), b) |
NumPy 還提供了更強的實現
1 | self.coeficient = np.linalg.solve(A, b) |
這個部分最簡單,就是把所有的點帶入方程式,得到預測的 z 座標。
就只是寫 z = Ax + By + C
。
不過要注意的是,最好用向量化寫法,不要用迴圈,這樣會比較快。
1 | def predict(self, pts): |
我用 Free3D 的一個人體模型來做範例。
1 | if __name__ == '__main__': |
如果有一群 3D 的點,要如何 regression 一個平面出來?
平面方程式:
1 | Ax + By + Cz + D = 0 |
但是把 Cz 移動到等號另一邊,C 就可以消掉,所以實際上只有三個未知數:
1 | (A/C)x + (B/C)y + (D/C) = z |
C 可以是除了0或者無限大以外的任何實數,因此,在這裡就不重要了。
為了簡化,重寫平面方程式:
1 | Ax + By + C = z |
以下就用 Ax + By + C = z 推導
無誤差情況:Ax + By + C = z
實際上情況:Ax + By + C = f(x, y)
也就是說,存在誤差:z - f(x, y)
為了同時消除方向性、逞罰大誤差,所以將誤差平方:( z - f(x, y) )^2
將誤差方程式寫出來,求出微分=0的極值點,就能得到此方程式參數。
1 | Err |
有多個未知數 A, B, C,因此需要分別對 A, B, C 微分:
1 | dErr/dA = ( z - Ax - By - C )(-x) |
( x, y, z 都是已有的點雲數據集,都有值!所以未知數是 A, B, C!不是 x, y, z!別搞錯了!)
(我自己寫的時候都搞錯了!寫成「對 x, y, z 微分」,囧)
根據我們的需要與期望,要找出誤差變化率為0的點。這一點的誤差,不是最大就是最小。
1 | 0 = ( z - Ax - By - C )(-x) |
把 chain rule 長出來的這一項乘進去:
1 | 0 = -xz + Ax^2 + Bxy + Cx |
把已知項移到等號左邊(再次提醒!未知數是 A, B, C):
1 | xz = Ax^2 + Bxy + Cx |
第一條,就是原本的平面方程式同乘x
第二條,就是原本的平面方程式同乘y
第三條,剛好就是原本的平面方程式
寫成矩陣形式:
1 | Ax = b |
矩陣計算沒有除法,只能用反矩陣搬移。兩邊同時左乘A的逆矩陣:
1 | (A^-1)Ax = (A^-1)b |
由此解得所有未知數。
逆矩陣計算也是超複雜的,我忘光了,工程數學課本都有,在此不贅述。
以調包的角度來看,到這裡已經可以輕易使用現成工具實現了。
應該沒有哪種程式語言,找不到現成逆矩陣算法的(雖然有的語言比較難找,比如 C#,官方有 System.Numerics.Matrix4x4.Invert
,但知名度還遠不如需要另外安裝的第三方套件來得高)。
下一篇文章會說如何用 Python 底層實現這個演算法,不使用np.linalg.lstsq
、scipy.optimize.leastsq
這些已經寫好的方法。
難的是什麼?如果我卷積的演算法,是有條件的、不能用 NumPy 的convolve
函式、不能用 OpenCV 的filter2D
函式、不能用一個 MxN 的卷積核描述,那我該怎麼辦?
要是用迴圈,大家都會寫啊。可是效能很差,怎麼辦?
這篇文章,我要介紹一個技巧,讓你自定義有條件的卷積演算法,變快 250 倍。
需求:
為了說明簡潔,以下說明不會放完整程式碼,完整程式碼會放在最後連結。
1 | # 方法1: 使用 for 迴圈 (最慢) |
上面這個程式碼,就是一般初學者最直覺,用 for loop 寫出來的程式碼,但是效能很差,因為 for 迴圈一次只能處理一個數,數據頻繁的進出往來 RAM 與 CPU,效能也很差。
1 | # 方法2: 使用 NumPy 向量化 |
上面這個程式碼,的概念是什麼?
我個人對卷積的理解,就是「以我自己為中心,對周圍的 MxN 個點做處理」,通常 M 與 N 皆為奇數。
那為了向量化,我就這樣做:
如何在向量化的同時,實現條件式演算法?:
這篇文章主要是介紹了 NumPy 的向量化計算,用一個…很不生活化(暫時想不到卷積如何生活化)的簡單案例,來說明 NumPy 向量化計算的優點。
這篇文章的完整程式碼,可以在這裡找到:https://gist.github.com/mosdeo/32230317309bab30727c0c76f09b47f0
旋轉梯圖片:该图片由Wolfgang Eckert在Pixabay上发布
]]>這一篇我們要用顏色過濾的需求,來示範快速過濾三維陣列。
需求:把圖片中的膚色去除,只留下其他顏色
1 | import NumPy as np |
輸出如下:
1 | Elapsed time for loop: 0.38222789764404297 seconds |
上面這個程式碼,就是一般初學者最直覺,用 for loop 寫出來的程式碼,但是效能很差,for 迴圈一次只能處理一個數,數據頻繁的進出往來 RAM 與 CPU,拖慢很多時間。
1 | # 方法2: 使用 NumPy 向量化 |
輸出如下:
1 | Elapsed time vectorlize: 0.001753091812133789 seconds |
技巧上的大方向,跟上一篇是一樣的。
我們不逐一比較,而是先產生一個篩子,一個與樣本數量同樣長度的篩子,直接拿這個篩子去一次過濾所有樣本,只要一次!
只是這一次需求更加複雜。上只有一個條件,這一次有六個條件取交集,所以用 np.bitwise_and.reduce()
來產生篩子。
我用以下程式碼,給六個條件都產生與樣本數量同樣維度的篩子
1 | [ |
把這六個篩子放進一個陣列。
為什麼用 np.bitwise_and.reduce()
?
bitwise_and 就是取交集、reduce 就是把陣列裡面的元素做某種「化N合1」的運算。
所以這六個篩子就會化簡成一個,篩出所有符合條件的樣本。
最後可以用 img_vectorlize[skin_mask] = 0
一次對符合六個條件的樣本,改顏色。
在我完整的程式碼中,還用了 np.array_equal()
來驗證兩種方法的結果是否一樣。
原圖:
膚色換黑色
膚色換綠色
這篇文章主要是介紹了 NumPy 的向量化計算,用一個生活化簡單案例,來說明 NumPy 向量化計算的快速。
這篇文章的完整程式碼,可以在這裡找到:https://gist.github.com/mosdeo/5b0193bead09251f13e649f5cf6129da
]]>我也熟悉 Go 或 C# 這些靜態型別的語言,但是欠缺各式各樣方便好用的套件,難以快速的驗證各種演算法。
這篇文章我們來看看如何用 NumPy 來加速 Python 的效能,並且用一個簡單的例子來說明。
本文要介紹的是 NumPy 當中一種叫做「Boolean array indexing」的技巧,官方文件的連結如下:
需求: 找出身高大於 178 cm 的資料
1 | import NumPy as np |
輸出如下:
1 | 檢驗結果: 高於178的人數為 126011670 |
上面這個程式碼,就是一般初學者最直覺,用 for loop 寫出來的程式碼,但是效能很差,因為:
1 | # 使用向量化 (最快) |
輸出如下:
1 | 檢驗結果: 高於178的人數為 126011670 |
上面這個程式碼,直接把整個陣列的數值都拿出來比較,然後再把結果存回陣列,這樣就不用一個一個比較了,效能就會快很多。
這樣可能比較難懂,我寫個分解動作的範例,讓大家可以更清楚的理解:
1 | is_above_178 = heights > 178 |
輸出如下:
1 | [ True True False ... False False True] |
如果在 debug console 查看 is_above_178
,會看到一個很長的陣列,裡面的值都是 True 或 False,代表每個數字是否大於 178。結果如下:
1 | array([ True, True, False, ..., False, False, True]) |
所以真正的動作是:
我們不逐一比較,而是先產生一個篩子,一個與樣本數量同樣長度的篩子,直接拿這個篩子去一次過濾所有樣本,只要一次!
從計算機原理講,為什麼快?
當演算法被正確地向量化時,CPU 僅需一條指令完成這行程式碼,而不是對每個 i 進行獨立操作。理想情況下,array[boolean_mask]
操作將只發生於 CPU 內部而不用將數據傳回 RAM。
這篇文章主要是介紹了 NumPy 的向量化計算,用一個生活化簡單案例,來說明 NumPy 向量化計算的優點。
這篇文章的完整程式碼,可以在這裡找到:https://gist.github.com/mosdeo/341216a87a099486c1420760f24ced00
]]>我也熟悉 Go 或 C# 這些靜態型別的語言,但是欠缺各式各樣方便好用的套件,難以快速的驗證各種演算法。
這篇文章我們來看看如何用 NumPy 來加速 Python 的效能,並且用一個簡單的例子來說明。
需求是:計算 1780 萬個矩形的面積
1 | import NumPy as np |
輸出如下:
1 | 檢驗結果: 19769742257 |
上面這個程式碼,就是一般初學者最直覺,用 for loop 寫出來的程式碼,但是效能很差,因為:
1 | import NumPy as np |
輸出如下:
1 | 檢驗結果: 19769742257 |
上面這個程式碼,直接把矩形的兩個對角點座標的XY值,分別按照大小取出來,成為四個 Nx1 矩陣。然後只做1次矩陣化的面積運算,就算完了 N 個矩形的面積,效能提升了 16 倍。
為什麼快?
當演算法被正確地向量化時,CPU 僅需一條指令完成這行程式碼,而不是對每個 i 進行獨立操作。理想情況下,any(result)操作將只發生於 CPU 內部而不用將數據傳回 RAM。
1 | # 以上重複部份程式碼省略 |
輸出如下:
1 | 檢驗結果: 19769742257 |
上面這個程式碼,就是用 NumPy 向量化計算的方式,效能提升了 22 倍。
為什麼又更快?
因為 np.ptp 的意思就是「peak to peak」,把「最大值、最小值、取差值」的計算合併成一個函式,所以效能更好。
1 | # 以上重複部份程式碼省略 |
輸出如下:
1 | 檢驗結果: 19769742257 |
上面這個程式碼,就是用 NumPy 向量化計算的方式,效能提升了 177 倍。
為什麼又更快?
老實說,到這裡已經到了我的知識盲區了。
不論是 slicing 或者進出 RAM 與 CPU 的次數,與前一種方法看起來都一樣,但是效能卻可以從 22 倍提升到 177 倍。我猜測可能是因為 np.ptp 有些限制導致速度比較慢。
用 NumPy 就是這樣,通常會比只用單純的 Python 快很多,但是各種不同用法的效能差異卻很大。
想達到最好的效能,有沒有一次到位的方法?除非有時間仔細鑽研 NumPy 的底層原理,不然我覺得沒有,只能一步一步的嘗試,並且不斷的優化。
可以看到,在最後一段程式碼中,有一行被我註解的地方。
用 np.diff 也可以達到同樣的效果,還更快一點,而且我完全想不出原因是什麼?
但是我開了 Docker 之後,就會發生爆記憶體的錯誤:
1 | Exception has occurred: _ArrayMemoryError |
可以看到 np.diff 跟記憶體要了一塊 N x N 的空間,而 N 高達 17800000,這對許多系統來說是無法承受的。而且你開發時可能不會出現這個問題,但是到了生產環境才發現,那就踩了大坑了。
三種向量化計算的效能,與原本的 for-loop 效能比較如下:
1 | Speed up for vectorlize: 16.214365142630854x |
這篇文章主要是介紹了 NumPy 的向量化計算,用一個工作上實際遇過的簡單案例,來說明 NumPy 向量化計算的優點。
這篇文章的程式碼,可以在這裡找到:https://gist.github.com/mosdeo/743a6e2275ea7676dc2f98246e055b33
]]>由於買了新的 M1 MacBook Air,做完數據移轉、相容性驗證之後,舊的 Intel MacBook Air 就「食之無味、棄之可惜」了。
算一算賣給蘋果官方還有 3000 元可以拿!於是就開啟了人生第一次 Apple Trade In。
要是還在台灣,處理舊機的首選通常是上 PTT 或蝦皮賣掉,自己搓合賣家,能拿到比較好的價錢。例如我的 Intel MacBook 當初就是在 PTT 上買到的;確認可用以後,又在蝦皮上把 ThinkPad 賣掉。
但中國大陸有特殊的民情,自己上閒魚等平台找買家,過程非常耗時耗力噁心人,所以第三方收二手機的情況很發達,願意買二手機的情況也比台灣少很多,甚至買二手機被認為是丟臉的事。
線上申請完以後,就會看到如下畫面:
好尷尬啊,台灣人沒有中國大陸身份證,不知道 Apple 會怎麼處理?
當天就會收到這個簡訊:
1 | 【爱锋派】Apple Trade In 换购计划 - |
點連結進去,挑選收件時間。
收件前一天還會收到這個提醒簡訊:
1 | 【爱锋派】Apple Trade In 换购计划 - |
收件當天早上還會收到這個提醒簡訊:
1 | 【顺丰速运】顺丰小哥1326830XXXX正上门收件,运单尾号3853,验证码1808。谢谢! |
整個過程的提醒,都相當貼心到位。
交機前,我先刪除一些敏感數據、登出服務,但是我故意沒有登出 Apple ID、關閉 Find My,我想看能不能在路上追蹤到?因為蘋果裝置本身就如同一個 AirTag,感覺會挺好玩的。
果然,當天下午就一路從廣州移動到深圳寶安區。
隔天早上出現在鄭州。
但中間會有很長的間斷,我猜可能是這樣:
這也算是有點在我預期中的,所以我就把密碼告訴他們,然後在 Find My 上執行清除數據。並且在大約一個小時後顯示清除完成(只是顯示,後面會詳述)。
沒想到,事情並不如我想得這麼順利。
第二天,是一個操北方口音的女人打電話來,要我執行清除數據,我說:
「昨天下午就做過了啊!你們打來要密碼之後我就主動清除了。」
「你們有聯網嗎?是不是沒有聯網?」
她表示不太清楚狀況,會轉告工廠。
第三天,是一個男人打電話來,聽起來應該是第一線執行的員工,又說我沒有清除數據,並且為了證明,說可以登入我的電腦,看到哪些數據等等(我不太擔心,敏感數據交付順豐之前都已經刪除)。
我再次確認,他說已經聯網。
這一次,我半信半疑的拿出手機照做,看到畫面上顯示這台電腦的狀態是「Erased now」(如下圖),而且我沒辦法再做更多操控的動作,只能看到這台電腦的「電量、是否充電中、位置」。至於「電量、是否充電中、位置」,是不是暫存的歷史訊息?我跟電話中的小哥核對後,都與現況相符,巧合的機率不高,應該是即時訊息。
也就是說,透過 Find My 清除自己電腦中的數據後,唯一能掌握的訊息應該就是「電量、是否充電中、位置」。
我把狀況轉告給電話中的小哥,他又做出以下要求:
「你還有其他蘋果裝置嗎?」
「其他蘋果裝置也顯示已抹除嗎?」
「你用瀏覽器登入 iCloud 看看,是不是也已清除?」
此時,我已經非常的不耐煩,但是怕 trade-in 失敗造成更多麻煩,只好先照做。
我用不同的裝置在瀏覽器上登入 iCloud(如下圖),結果也是一樣,已經清除過,不能再清除。只多了「播放聲音」的功能。
我反覆描述這個狀況後,電話另一端的小哥才放棄,說再看看怎麼辦。
第三天,是一個週六早上,我正在外頭忙,他們又打電話來。這次電話中又是不同人,似乎把狀況搞得比較清楚了,說我之前做的那些動作需要「電腦當下聯網」才會生效,請我重新做一次。(難道前幾次電話處理,都不是當下聯網?)
這次我又不耐煩地拿出平板打開 Find My,終於有點「新東西」了!顯示出在鄭州工廠的那台電腦,並且是未清除的狀態,可以進行所有常規操作。
這一次清除,電話中的小哥立刻就說有在清除了。但我在忙、可能也沒耐心了,沒把之前的清除訊息再打一次。之後就按照指示把電腦從 Find My 中移除。
到這邊,麻煩的溝通才算告一段落。
接下來幾天就陸續收到這些簡訊。
週日下午3點:
1 | 【爱锋派】Apple Trade In 换购计划 - |
週日下午5點:
1 | 【爱锋派】Apple Trade In 换购计划 - |
幸好上傳沒有阻擋台胞證,有跳出視窗說是人工審核,要等。
週一下午5點:
1 | 【爱锋派】Apple Trade In 换购计划 - |
這部分過程都寫了,就不再詳述。
畢竟回收舊機器還是比較髒的事,原廠摸透流程後,大多就會包給第三方做,就難免有這些小缺點。
整個流程都說要「二代身份证」,這對於在中國大陸各種日常生活系統上,經常因為證件不符,習慣被拒絕使用的台灣人來說,是特別不友善的用詞。
真的考慮的夠,就不會寫「上传您的二代身份证正反面照片」,因為在字面上已經排除收其他證件的可能。應該寫「有效身分證件」。
都寫了要「二代身份证」還拿台胞證硬闖,我在中國大陸生活兩年多,這是第一次成功!一般遇到這種情況,我都會基於成功率太低的經驗放棄,這次是憑著對蘋果的信任,還有果粉朋友的話,才硬著頭皮把流程走到底。
果粉朋友的話是這麼說的:
那放心,水果对第三方要求很高,会考虑到港台中国居民
只有 sb 私企才不考虑港澳台的中国居民,你又懒得投诉
所以转人工审核了,我不信他们只收内地身份证的
(我也不確定,是不是這樣才轉人工審核?或許不論什麼證件都人工審核?)
換句話說,蘋果只寫了收「二代身份证」,卻還讓台胞證硬闖成功,也是變相鼓勵在中國大陸生活卻沒有「二代身份证」的人,不去遵守字面上的系統規範,鼓勵試探規則底限與漏洞,這種作風非常的不蘋果,也不是高端品牌應該有的作法。
]]>下圖是筆者在 Apple Silicon 上跑起了 OpenCV 4.5.3 與 TensorFlow 2.5.0。
在朋友推坑之下,筆者入手了 Apple Silicon (以下簡稱M1) MacBook。
這兩天做完相容性驗證之後,準備把舊的 Intel MacBook 賣掉。
(好尷尬啊,台灣人沒有中國大陸身份證,不知道 Apple 會怎麼處理?)
相容性驗證有哪些項目?主要驗證我常用的 OpenCV、scikit、numpy、TensorFlow 等套件如何安裝?能不能正常跑?
經過大量苦逼無聊的工作後,發現目前還是不能常規無痛安裝,必須以 workaround 的手段安裝。
經過我徹夜摸索後,修正國外大神帖子中部分問題,終於跑通一套可以正確安裝 OpenCV 當下最新版(4.5.3)與 TensorFlow 2.5.0(僅限 Apple 分支) 的流程。
以從頭乾淨安裝來說,目前各種 Python 的數據科學 package 幾乎只能走 conda 安裝,疑似還是要依賴 Rostta2?這部分我就不懂,有請熟悉編譯器或虛擬機器的大神補充。
這步驟我覺得對 macOS 上的開發者來說是必備且熟練了,所以懶得寫。
記得 brew 要安裝 Apple Silicon 用的版本。
1 | $ which brew |
Apple Silicon 用的 brew 版本,路徑是 opt 開頭的。
這個連結,下載並執行:
https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-arm64.sh
這裡就會包含有針對 Apple Silicon 最佳化的 Python 3.9,也一併安裝。
我沒有要做環境隔離,所以直接 conda activate
進入到名為 base 的 conda env。
你嘗試下圖的指令,看是不是跟我一樣?
這代表成功切換虛擬環境,並用上 Python 3.9
按照國外大神提供的步驟,在這一步就出錯,走不下去,原因是內容有些過時了。
你照做就會看到以下的錯誤:
1 | $ (base) ➜ pip install --upgrade --no-dependencies --force numpy-1.18.5-cp38-cp38-macosx_11_0_arm64.whl |
原因:這個 numpy 包的「cp38」代表是給 Python 3.8,版本號多或少一點都不行!可是最多也只出到 3.8,但指定的 conda 環境已經變成 Python 3.9,而且為了 M1 優化也是從 Python 3.9 開始,所以這邊就衝突了。那這個坑怎麼辦呢?
不論只安裝 OpenCV 與 TensorFlow 其中哪一個,這一步都是必要的前置步驟。
這邊直接安裝 Apple 官方提供的 tensorflow-deps,會聯同 numpy 一起搞定。上面單獨安裝一個 .whl 的步驟就不用了。
conda install -c apple tensorflow-deps
安裝完以後測試看看,有 numpy 版本號出現就是可以了。
python -c "import numpy as np;print(np.__version__)"
通常說到編譯,就會想到等很久。還好我們用的是 Apple Silicon,所以這次很快,我記得開始編譯之後就去洗澡,洗完出來剛好完成。
在家目錄下載 OpenCV 與他的延伸套件、解壓縮、進入編譯資料夾。
1 | mkdir /Library/OpenCV && cd /Library/OpenCV |
然後 cmake
外國大神提供的指令這邊有錯,照做會得到這個錯誤。
1 | Make Error: The source directory "/Library/OpenCV/opencv-4.5.3/_" does not exist. |
我撞牆多次後,已經為大家修正。
首先在 conda 環境下輸入 which python
,得到以下結果:
1 | (base) ➜ ~ which python |
然後把輸出的路徑放到以下 cmake 指令中,PYTHON3_EXECUTABLE 後面那串
1 | cmake \ |
接著輸入 make -j8
,這就是編譯的步驟。
j8 就是用八個核心,目前能買到的 Apple Silicon 都是八個核心,所以也沒有改多的空間了。
給大家看看我編譯的過程:
編譯完成後,執行 sudo make install
,這一步就算完成了。
將 macOS 上的 OpenCV 4 符號鏈接到虛擬環境 site-packages
輸入 mdfind cv2.cpython
應該能看到這兩行以「.so」結尾的路徑,其他的路徑不管他
1 | (base) ➜ ~ mdfind cv2.cpython |
我們要在虛擬環境的 site-packages 中產生一個假的「cv2.so」,實際上是連結到剛才編譯出來的那個「cv2.cpython-39-darwin.so」
1 | cd /Users/<你的使用者名稱>/miniforge3/lib/python3.9/site-packages |
這一步做完,OpenCV 應該就安裝到 Python 能用了。來測試一下:
1 | conda activate |
結果應該要出現「4.5.3」。
1 | conda activate |
這一步做完,TensorFlow 應該就安裝到 Python 能用了。來測試一下:
1 | conda activate |
結果應該要出現「2.5.0」還有以下隨機內容的 Tensor:
1 | tf.Tensor(-48.841095, shape=(), dtype=float32) |
到這裡,以下這些套件就安裝完成:
另外這些常用的 package 都這樣安裝:
1 | conda install matplotlib |
以後使用之前,都必須要先 conda activate
進入虛擬環境,這是目前還有一點不完美的地方。
成功的話,我就不用在新電腦上搞麻煩的 node 與 hexo 環境了。
這一條拖了一年多的 to do list,終於在今天完成了(淚~)
但其實寫之前就想很久了,真要算下來都不只兩年了。
原始碼:https://github.com/mosdeo/HexoContainer
]]>一開始的問題是樣本極少,第一天還不到10張,之後也可預見的不好蒐集,所以其他人一頭熱用 YOLO 之類的演算法同時,我評估深度學習不是好方法。越老舊的方法雖然上限越低,但可解釋性比較好,一般來說需要的數據量比較少。
都用一些比較傳統的方法,一開始先用 Cascade(就是 Viola–Jones 那個)
這裡就來了第一個大坑,OpenCV 4.X 已經拿掉了 Cascade 相關的東西,網路上一找可以看到很多人抱怨,當然抱怨大本營是 Github issue,官方建議大家改用 DNN,但還是很多人想要用復古的東西。
好吧,我只好降低版本到 3.X,這連帶讓我找適合的 docker image 基底都變得困難(因為編譯 OpenCV 太久,不能每次都編譯),還好最後還是找到了。
然後隨著樣本增加到幾百個,效果也不是很好。中間還做了哪些努力?因為我自己的 MacBook Air 很慢,所以花了很多時間寫 Dockerfile 還有一些部署相關的 bash,讓計算可以在公司的服務器上跑。但 docker build 與 debug 還是吃自己電腦上的算力,電腦不夠快的影響還是有。
後來覺得為了 debug 而 docker build 的次數太多了、太浪費時間,所以決定:
後來我就發現,預設的特徵擷取法是 Haar,那個是針對人臉的明暗變化擷取特徵,不太適合現在的檢測目標。另外兩種可選特徵是 LBP 與 HoG,這兩種我都在寫論文或寫作業自乾過了,HoG 應該是最佳選擇。理論上,這只要改個參數就可以看到辨識率爆炸性的推進了吧?我以為看到了一條很棒的近路。
結果發現 OpenCV 3.X 可以訓練基於 HoG 的 Cascade 模型,但是卻不能 load 基於 HoG 的 XML model?必須要 2.X 才能,那我就再度降低版本到 2.X 吧!這次更慘,發現沒得降低了,因為 Python 的 OpenCV 最低就是從 3.X 開始,如果降低到 OpenCV 2.X 那從 load XML model 以後的所有事情我都要用 CPP 寫,太累了!
https://github.com/shimat/opencvsharp/issues/1022
搜一下發現很多人用 HoG + SVM 的組合做目標檢測,尤其行人與車輛,感覺還蠻適合現在的目標。發現用的函式庫有兩大派別,老的 OpenCV 與新的 scikit-image。
然後就找一下 Github 現成的輪子,發現有一個看起來寫得不錯、夠規整、文件夠清楚,有把一些設定獨立在 config.cfg。clone 下來之後發現是除了 Python2 之外,還有一些錯字、一些API過時了,改了將近一天才跑起來。
跑起來之後有坑啊,一直跑出「array has an inhomogeneous shape after 1 dimensions.」,原來是我的訓練圖片大小都不一樣,沒有經過正規化。給模型的所有數據維度都要一樣大,這其實應該是個機器學習常識才對,甚至也是統計常識,只是現成的工具太方便了,習慣都被養壞了。
接下又要繼續解這個不是坑的坑…(待續)
(還有一些數據視覺化的坑沒寫,不過我懶了,下次再寫)
]]>以前總覺得把程式碼放到別的 server、別的電腦上跑,要搞定各種環境細節上的差異,是一件骯髒的苦差事。自從有了 Docker,一切都變得輕鬆快速無負擔。
每次一鍵部署(如上圖)都覺得很爽快!從改完程式碼到上線生效,就只有一行指令的距離(前提是網路穩定、指令編寫正確)。
物聯網裝置通常都是吃電池,很少有插電的機會。新硬件設計的時候,最大瓶頸是裝置的電力有限,但各部門都想在裝置上加入對自己有利的耗電項目。
行銷部門想放廣告、GIS 部門想裝 GPS、維運部門想寫入更多的 log、跟醫院合作的部門想裝運動感測器等等,這些需求都在爭搶極其有限的電力。給這些裝置充電,比給自己的手機半天一充還要困難。
為了裝更多的感測器,我連「用 DMA 節省 CPU 耗電」、「常態休眠,等感測器發出中斷再喚醒」這些很韌體技巧都提出來了,我還自己去查感測器的 datasheet 想辦法找出耗電量比較少的型號。
這個問題已經在網上被介紹過很多次,就不詳述。數據孤島有企業與企業之間的、也有部門與部門之間的,沒有統一的標準、沒有流通的數據,也會降低的數據的價值。一但被認定數據的價值不夠,在上一點「裝置電力短缺」就會被否決安裝感測器的需求。
通常數據價值已經被管理層看到的時候,都是別家企業已經拿數據做出應用、講出故事的時候,這時候想在營運中的系統加上感測器已經來不及,就算裝了,應用場域的機會與話語權,也早已經被先行的企業拿下。
上圖是我參與過初期規劃階段的產品,我離職時:
參考了很多中國大陸共享單車的方案,呼聲最高是類似現在中國大陸實行的虛擬停車柱/停車區,最後不知道為什麼沒採用,但不論開發商或當局政府都採取很保守的態度,很多先進的提案都沒實施,估計是被「單車墳場」的文章嚇到了。
當時落地營運的還只有 1.0,1.0 上沒有任何感測器或長距離通訊裝置,只有一組向停車柱回報車 ID,類似 NFC 的線圈。
2.0 版連塗裝都是我離職多年後才在路上看到,也不知道當年提案的感測器到底裝了哪些…
2020-09-13 面试官:这个经典的并发问题用 Go 语言如何实现?
2020-04-03 LeetCode Go 并发题详解:交替打印字符串
2020-02-19 只在我计算机上能跑的代码:select-case-default 忘记让出 CPU 的坑
2020-02-16 面试题实战:给一个数 n,使用 Go 打印交替顺序零与奇偶数
2020-02-11 多 Goroutine 的并发程序如何保证按序输出?channel 的使用是关键
2020-02-04 LeetCode上并发题目无Go版本:台湾同胞试水 — 交替打印FooBar
2020-02-14 在台湾亲身经历约口罩抢购热潮…
2020-02-11 我在台湾亲历口罩抢购潮 | 人在书店×2002
]]>https://leetcode.com/problems/the-dining-philosophers/
「哲學家吃飯問題」是一個作業系統中的經典問題,所以抽象題幹我就不再贅述,直接說實作要求。
The philosophers’ ids are numbered from 0 to 4 in a clockwise order. Implement the function void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork) where:
有幾位哲學家,他們的 ID 順時針由 0~4,實作一個函數 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
,其中…
philosopher is the id of the philosopher who wants to eat.
參數 philosopher
代表想要吃飯的哲學家的 ID。
pickLeftFork and pickRightFork are functions you can call to pick the corresponding forks of that philosopher.
參數 pickLeftFork
and pickRightFork
是函數,你必須呼叫他們來使哲學家拿起對應的叉子。
eat is a function you can call to let the philosopher eat once he has picked both forks.
當哲學家拿起兩隻叉子後,你必須呼叫 eat
這個函數讓哲學家吃一次。
putLeftFork and pickRightFork are functions you can call to put down the corresponding forks of that philosopher.
參數 putLeftFork
and pickRightFork
是函式,你必須呼叫他們來使哲學家放下手中的叉子。
The philosophers are assumed to be thinking as long as they are not asking to eat (the function is not being called with their number).
假設哲學家們都會思考很久,中間都不會要求吃東西(呼叫函式 thinking() 不必使用哲學家們的 ID)
Five threads, each representing a philosopher, will simultaneously use one object of your class to simulate the process. It is possible that the function will be called for the same philosopher more than once, even before the last call ends.
五個執行緒,每一個執行緒代表都一個哲學家,用一個類(在 Go 語言是 struct)模擬這個 process。這個函式可能被同一個哲學家呼叫多次,甚至在最後一次呼叫結束前的途中都有可能。
最早課本裡都是說「叉子」。但我大學上 OS 的時候老師就提過一個疑問:「用叉子吃義大利麵,一隻就夠了,沒必要用到兩隻吧?所以,改成用筷子是不是更合理一點?但沒辦法,誰叫這門學問是西方先發明的?我們就當作筷子吧」。
於是,本文也決定照改,以下都用「筷子」代替「叉子」。
在過去的 LeetCode Concurrency 詳解中,我提到過很多次:
goroutine 若不刻意控制,將無法保證執行的先後順序,因此本題就是要考核對 goroutine 順序控制的能力。
但前面幾題的解法,大多是把判斷責任中心化,方便控管順序。這次,與前面幾題不同的是,這一題要求把判斷責任分散到每一位哲學家 thread 身上,哲學家彼此之間並不溝通,因此很容易發生資源互卡,也就是 deadlock。本文所示範的 channel 使用方法已經完全避免了死結(deadlock)。但這樣就沒問題了嗎?不,還有可能發生活結(livelock)。
這邊我為了示範 goroutine,先用最笨的碰運氣解法,也就是不刻意做任何資源配置,要在運氣很壞的情況下才會遇上 livelock。什麼是「運氣很壞的情況」?就是所有哲學家剛好在同一時間拿起同一邊的叉子。但實作上,由於我給每位哲學家一個隨機的思考時間 50mS(如下列程式碼),碰撞的機會是(1/50)^5,所以絕大部分情況下不會發生 livelock。
1 | func Think() { |
Wiki 上有介紹不需要碰運氣,保證不會讓 thread 飢餓致死的演算法,但我自己也沒搞懂,請容我日後再介紹。
本題採用 5 個 buffered channel,分別代表 5 支筷子
1 | type DiningPhilosophers struct { |
1 | // Channel 初始化 |
1 | // 叫所有哲學家開始動作 |
這邊開始計時後,是一個 foreach。
老方法,用 sync.WaitGroup
同步 5 個哲學家 goroutine 結束時間。
給每一位哲學家起一個「WantToEat」的 goroutine,告訴他 i 你是幾號?又給入「PickLeftFork, PickRightFork, Eat, PutLeftFork, PutRightFork」五個函式的的 function reference。
沒有交接棒問題,每位哲學家就憑運氣去搶左右邊的兩隻筷子。
要注意的只有三件事情:
這次解題沒有實作這些協調機制,5 個 goroutine 只靠前述的三條規範野蠻生長。
有一行「return //吃飽離開
」,整個流程最終目的就是要走到這一行。
1 | func (this *DiningPhilosophers) WantToEat(philosopher int, pickLeftFork func(int), pickRightFork func(int), eat func(int), putLeftFork func(int), putRightFork func(int)) { |
這邊對於每一隻筷子的具體表現就是一個 buffered channel,迴圈流程如下:
先嘗試把自己的號碼塞入左邊的 buffered channel
default: //無法拿起左邊筷子
」,思考一下,然後從頭開始。再嘗試把自己的號碼塞入右邊的 buffered channel
default: //無法拿起右邊筷子
」,把已經搶到的左邊筷子還回去,思考一下,然後從頭開始。在 console 輸出,可以看到代表每一位哲學家的 goroutine 詳細動作過程,錯過筷子次數並不多,大部分執行結果的錯過次數在 3~5 次(點擊以下的「完整解題程式碼」就能體驗)。
https://play.golang.org/p/neTH25E8ayX
所以,我們得知道怎樣可以讓生長激素更多?又怎樣避免生長激素減少?這是很多營養健身 YouTuber 會介紹的主題,但是營養健身 YouTuber 偶爾講到有些地方會與課本衝突,所以這邊地特整理:
比較特別的是,這邊寫到胺基酸的增減會影響到生長激素,這是我在其他地方都沒看到的調控因子。
這張圖用很清楚的方塊來表示各個因子之間的調控關係,
然而,書中講到生長激素作用時,強調「靜效果是保持血漿葡萄糖濃度」。若以這點來看,眾多的生長激素調控因子中,可能只有「低血糖」這一項是鐵律,其他或許只是相關,而非因果?
書中又給了更清楚的量化關係,顯示血中的生長激素與 VO2Max% 有極為強烈的關係!也難怪有些說法是「高強度間歇訓練可促進生長激素」。
這本書花了不少篇幅談生長激素,但沒有像其他幾本書說的那麼簡單,關注在相當多模凌兩可或很複雜的點上,例如:
真要說這本書讓我認識了什麼新的調控因子?大概就是重訓前後吃些蛋白質醣類吧(如下圖)
05:14 所說「氫離子、乳酸可以促進生長激素」,但我目前沒在任何文獻上看到這兩樣物質有被列為生長激素調控因子。
若更積極一點,進一步去跟生長激素扯上關係?我能想到的是,乳酸會被作為糖質新生的原料用,所以會間接提高血糖,故乳酸會抑制生長激素才對吧?
]]>前言:由於 LeetCode Concurrency(併發) 還沒有 Go 語言版本,我先自行用 Go 語言來解題。為了能在 LeetCode 以外的平台獲得討論,所以我打算逐漸把自己的解題思路寫下。
https://leetcode.com/problems/fizz-buzz-multithreaded/
給定一個數列從 1 ~ n,依序輸出,但是:
實作要求:使用 4 個執行緒實現一個多執行緒版本。一個 FizzBuzz 的 instance 要被傳遞到以下四個執行緒中:
fizz()
以檢查 n 是否可以被 3 整除?若可以就輸出 fizzbuzz()
以檢查 n 是否可以被 5 整除?若可以就輸出 buzzfizzbuzz()
以檢查 n 是否可以被 3, 5 整除?若可以就輸出 fizzbuzznumber()
照常輸出原本數字 n我一開始認為「這題沒什麼難的嘛~還不就那些套路再用一次!」,所以最早的實作版本,是寫了一個中心控管的 goroutine,判斷整除條件後,再把輸出任務透過 channel 發派給其他 goroutine A, B, C, D。
直到我為了分享這題,將英文題目翻譯為中文的時候,才發現自己誤解題目了(尷尬)!題目真正的要求更困難,要各個 goroutine 自行負擔檢查整除條件的責任。所以只好重寫 XD
在過去的 LeetCode Concurrency 詳解中,我提到過很多次:
goroutine 若不刻意控制,將無法保證執行的先後順序,因此本題就是要考核對 goroutine 順序控制的能力。
但前面幾題的解法,大多是把判斷責任中心化,方便控管順序。這次,與前面幾題不同的是,這一題要求把判斷責任分散到 thread A, B, C 中,所以每個 goroutine 也無法準確得知下一個要接棒的 goroutine 是哪一個?這樣的順序控制會由於分散化,變得更加困難。
By the way,我還解過「DiningPhilosophers」這一題用的就是去中心化方法,但目前還沒寫那一題詳解。
1 | type FizzBuzz struct { |
1 | fizzbuzz := &FizzBuzz{ |
依照題目採用一個 FizzBuzz 物件 pass 到各個 goroutine 之中,當中有 buffered channel streamBaton
長度為一,可儲存一個整數。
這一題在 goroutine 之間交接棒的規則更複雜,所以我決定不像之前一樣指定的交接棒,而是每一個 goroutine 都把訊息丟到同一個 channel 裡面去,大家都去「各取所需」,看看是不是符合自己的整除規則?如果不是,表示自己還沒接到棒,要把數字再寫回 channel 讓應該接這一棒的 goroutine 可以讀取到資訊。
這樣有壞處,那就是會多很多次沒有命中的 channel 讀取,若不是自己要的還得把數值還回去。做個比喻,就像老闆雇用員工吧,因為不具備識人能力,都先雇用再說,不對再趕走。(只是比喻,如有雷同,純屬巧合)
受限於 channel 的性質,看了就會改變內容,所以若沒有命中就多了「還回去」的動作,無法如同 get 存取子一樣只讀不寫。
首先,這個循環是從 0 開始,沒有人交棒給 0,所以 main()
要自己丟。
1 | fizzbuzz.streamBaton <- 0 //啟動交棒 |
再來,本題不像之前有清楚的交接棒順序,不預設哪一個 goroutine 會收尾,所以需要用 sync.WaitGroup
同步 4 個 goroutine 結束時間。
最後,由於最後一個 print 交出去的棒子沒 goroutine 接,所以要記得關閉通道,否則在交棒點會發生 deadlock。(你想知道後果的話,可以在下面原始碼自行把 close 這行註解掉看看)
1 | close(fizzbuzz.streamBaton) |
這一次採用去中心化的交棒決策,所以每一個 goroutine 的流程都是相同的,因此我將各自的「整除條件」PassCondition(i int)bool
與「字串輸出」PrintString(i int)
取出,以下列程式碼 PrintFizz()
為例:
1 | func (this *FizzBuzz) PrintFizz() { |
其他的 PrintBuzz()
、PrintFizzBuzz()
、PrintNumber()
也都比照辦理。剩下都抽象為 PrintLoop()
以達到程式碼的 DRY,如下列程式碼:
1 | func (this *FizzBuzz) PrintLoop(passCondition func(int) bool, printString func(int)) { |
for loop 會獨自判斷 0~n 每一個數字是否滿足自己要輸出的條件?
streamBaton
裡頭看看,有沒有剛好與 i 相同的數字?streamBaton
讓該接棒的自己接棒。i--
使 for loop 不會前進,繼續原地等待接棒。runtime.Gosched()
,使自己不會獨佔 CPU,令其他 goroutine 有機會可以動作。過去幾次解題都用 unbuffered channel 的原因是,並沒有要共享什麼資料,就只要在 goroutine 之間交接棒,這個棒子上不需要帶其他訊息,因此 channel 用的也比較多,因為「交棒給誰?」的訊息用多個不同 topic 的 channel 區別。
這一次採用 buffered channel,是因為不只要交接棒了,還要透過一個 int 來指定下一個交接對象,這就是「透過通訊來共享」。
當我們要把一件事講清楚,除了講「應該是什麼」,最好也把「不應該是什麼」說明白,正反例都有更有助於建立清晰的認知。
那麼,要是我就故意反著做,硬要「透過共享來通訊」呢?很簡單,把 chan int
改成 int
,其他部分做些相應修改就是了。兩個版本的程式碼都會放在下面的 The Go Playground 連結。
但是這兩種方法,在本題的執行結果卻完全相同!花費時間也沒有明顯差異。所以「透過通訊來共享」的優越性到底在哪裡?或許本題的要求不夠嚴苛,不足以展示出差異,而筆者自己也學藝不精,尚未參透。如果有讀者能說得清楚,歡迎在本文底下留言,筆者會非常感謝你。
「透過通訊來共享」版本(使用 chan int):
https://play.golang.org/p/nHZtkI-pGs5
「透過共享來通訊」版本(使用 int):
https://play.golang.org/p/92wshFYlPG3
為什麼說是地雷?當程式在自己的電腦上正常,我會很容易以為自己是對的,而且這個現象與作業系統的排程細節有關,很難找一個明確的環境原因。
你可以看到,我的解題程式碼 default 那段是這麼寫的:
1 | default: |
以上其實是被高人「指點」後的。原本是這麼寫:
1 | default: |
這樣在我的 MacBook 依然正常,但是拿到 The Go Playground 上面就掛了,你可以自己修改程式碼(那篇解題文章最後有程式連結),在 The Go Playground 上試看看會怎樣?
原因是,雖然 select-case-default 會隨機均勻的嘗試每一個 case-default,但是並不會主動把 CPU 控制權交出去,需要用 runtime.Gosched()
或 <-time.After(time.Microsecond)
把 CPU 讓出給其他 goroutine。否則,其他的 goroutine 將可能沒有機會動作。
C# 裡的 Application.DoEvents()
也是一樣的意思,讓別的事件有機會被觸發。
那為什麼我的 MacBook 正常跑完?難道是 CPU 使用數量限制嗎?我們來看看這兩個平台可用的邏輯處理器數量:
好的,我的 MacBook 果然有比較多邏輯處理器可用,但也不足以說明這就是原因。
於是,我索性在筆電上的程式碼開頭加上一行 runtime.GOMAXPROCS(1)
限制此程式與 The Go Playground 一樣,只能用一個邏輯 CPU。結果,字出來是變慢了,好像古老的打字機那樣,但也是順利正確的跑完了,無法重現 The Go Playground 上發生的錯誤。所以這樣的 bug,真的很難在不同平台上重現,是很不容易發現的地雷。
https://leetcode.com/problems/print-zero-even-odd/
The same instance of ZeroEvenOdd will be passed to three different threads:
同一個 instance ZeroEvenOdd
會被傳到三個 thread 裡面:
Thread A will call zero() which should only output 0’s.
Thread B will call even() which should only ouput even numbers.
Thread C will call odd() which should only output odd numbers.
Thread A 將會呼叫 zero()
並且只會輸出 0
Thread B 將會呼叫 even()
並且只會輸出偶數
Thread C 將會呼叫 odd()
並且只會輸出奇數
Each of the threads is given a printNumber method to output an integer. Modify the given program to output the series 010203040506… where the length of the series must be 2n.
每一個 thread 都會被傳入一個 printNumber()
以輸出一個整數。
修改已給的程式碼,使其輸出序列為 010203040506…,該序列長度必須為 2n。
在一個未知長度的序列中,依照「0-奇數-0-偶數」的順序將數字印出,且一種元素只能由一個執行緒印出,代表各個執行緒之間要依照這個數列的規則溝通。
goroutine 若不刻意控制,將無法保證執行的先後順序,因此本題就是要考核對 goroutine 順序控制的能力。
與前面幾題不同的是,這一題最後工作的 thread 具有不確定性,視數列最後一個元素為奇數或偶數來決定,這點小小的提高了難度。
本題採用五個 unbuffered channel,並且是 ZeroEvenOdd
的成員變數。
1 | type ZeroEvenOdd struct { |
1 | var zeo = &ZeroEvenOdd{ |
定位分別是:
streamEvenToZero
: Even()
交棒給 Zero()
streamOddToZero
: Odd()
交棒給 Zero()
streamZeroToEven
: Zero()
交棒給 Even()
streamZeroToOdd
: Zero()
交棒給 Odd()
streamZeroToEnd
: Zero()
交棒給啟動它的 goroutine以前的文章說過,由於本題解法採用各個 goroutine 彼此循環交棒的方式,因此不能自行啟動,需要外界給訊號,所以在包住一整題的 PrintZeroEvenOdd()
執行各個 goroutine
同時以 zeo.streamEvenToZero <- struct{}{}
作為起頭的火種 ,讓 main()
假裝自己是 Even()
交棒給 Zero()
,以啟動交接棒循環。具體程式碼如下:
1 | go func() { zeo.streamEvenToZero <- struct{}{} }() //給起頭的火種 |
要特別注意的是,這個「啟動火種」也要寫成 goroutine,否則會由於執行當下尚未等到消費者「出世」,發生 deadlock!
另外一種不用 goroutine 啟動的做法,也可以讓消費者先「出世」,在 goroutine 的阻塞中等待時,再給「啟動火種」。具體程式碼如下:
1 | go zeo.Zero(PrintNumber) |
中心化:由 Zero()
做控管中心,遍歷 0 to n 每一個數字,印完自己責任該印的 “0” 以後,根據數字性質決定要把棒子交給 Even()
或 Odd()
。此處會用到 select-case-default。具體程式碼如下:
1 | func (this *ZeroEvenOdd) Zero(printNumber func(int)) { |
雖然順序都是固定的,但在此先假裝 Zero()
並不知道誰會交棒給自己?所以 Zero()
交棒(send to chan)以後,就會在 for-select 裡無窮迴圈,每一次 select{} 都會隨機選擇一個 case 或 default,也就是以亂槍打鳥的方式 polling 是誰交棒給自己?
謎之聲:「難道有不是中心化的流程嗎?」,有喔!我解決「DiningPhilosophers」這一題用的就是去中心化方法,但目前還沒寫那一題詳解。
對於 Even()
與 Odd()
來說,流程很固定,只有 Zero()
會交棒給自己,印完數字後,也只需要交棒給同樣的 Zero()
,一種「哪裡來,就哪裡去」的概念。
唯一比較複雜的部分,就是數字「遞增」與「終點」的控制:
具體程式碼如下(太相似,故此處只放 Even()
舉例):
1 | func (this *ZeroEvenOdd) Even(printNumber func(int)) { |
Zero()
善後?由於題目的關係,Even()
或 Odd()
其中一個,都有可能是最後印出字元的 goroutine,若讓這兩者去收尾,流程上的不確定性比較大。因此,幾經考慮後,還是決定讓 Zero()
去收尾。
讓 Zero()
去收尾的套路,之前的詳解也寫過,就是先 return 的 goroutine 最後都要 send to chan 到負責收尾的 goroutine,收尾 goroutine 在最後一一將這些 chan 都 receive 掉。
但由於本題特性,可由題目給定數字的奇偶判斷,Zero()
會從哪個 channnel 收到收尾訊號?因此在 Zero()
最後段的 receive,是以奇偶數判斷要在何處等待。具體的局部程式碼如下:
1 | if 0 == this.n%2 { |
主程式為了等待 goroutine 都結束才往下的同步情況,往往會用 sync.WaitGroup.Wait()
。
根據本文前面所介紹,我已經將流程結束的不確定性減少,使得一定會由 Zero()
負責收尾,因此只要在主程式阻塞一個 chan receive,由 Zero()
結束前 send 一下,便可以將主程式打通,繼續往下。
具體的局部程式碼如下:
goroutine Zero()
結束前 send 一下,交棒出去。
1 | func (this *ZeroEvenOdd) Zero(printNumber func(int)) { |
在主程式啟動完其他 goroutine 之後,阻塞一個 chan receive,等待被 Zero()
打通,繼續往下。
1 | go zeo.Zero(PrintNumber) |
https://play.golang.org/p/K5ZpQsHxlfN
https://leetcode.com/problems/print-in-order/
指定各種不同順序執行 First()
, Second()
, Third()
三個 goroutine,但三者都必須以不變順序印出字串,印出順序不受順序執行影響。
goroutine 若不刻意控制,將無法保證執行的先後順序,因此本題就是要考核對 goroutine 順序控制的能力。
本題採用三個 unbuffered channel,並且串在一個 slice 裡。
1 | // make an array of unbuffered |
分別是:
streamSync[0]
: First()
交棒給 Second()
streamSync[1]
: Second()
交棒給 Third()
streamSync[2]
: Third()
交棒給 PrintInOrder()
一開始由 PrintInOrder()
依照指定順序啟動三個 goroutine。
再看這三個 goroutine,只有 First()
可以不受限執行 Print,其餘都必須等待各自的 streamSync[i]
訊號,因此可以保證 “First” 先被印出。
1 | func First(streamSync [3]chan interface{}) { |
1 | func Second(streamSync [3]chan interface{}) { |
1 | func Third(streamSync [3]chan interface{}) { |
當 “First” 先被印出之後,交棒給 streamSync[0]
,然後…
streamSync[0]
卡住的 Second()
就可以印出 “Second”streamSync[1]
卡住的 Third()
繼續等待訊號streamSync[2]
卡住的 PrintInOrder()
繼續等待訊號當 “Second” 繼續被印出之後,交棒給 streamSync[1]
,然後…
streamSync[1]
卡住的 Third()
就可以印出 “Third”streamSync[2]
卡住的 PrintInOrder()
繼續等待訊號當 “Third” 最後被印出之後,交棒給 streamSync[2]
,然後…
streamSync[2]
卡住的 PrintInOrder()
就可以往下執行,最後程式順利結束。本題解答程式碼已經窮舉這三個 goroutine 所有啟動順序。
https://play.golang.org/p/cklu-vaxF6w