
前段時間在用 <code>Python</code> 實作業務的時候發現一個坑,準确的來說是對于 <code>Python</code> 門外漢容易踩的坑;
大概代碼如下:
首先定義了一個 <code>Mom</code> 的類,它包含了一個字元串類型的 <code>name</code> 與清單類型的 <code>sons</code> 屬性;
在使用時首先建立了該類的一個執行個體 <code>m1</code> 并往 <code>sons</code> 中寫入一個清單資料;緊接着又建立了一個執行個體 <code>m2</code> ,也往 <code>sons</code> 中寫入了另一個清單資料。
如果是一個 <code>Javaer</code> 很少寫 <code>Python</code> 看到這樣的代碼首先想到的輸出應該是:
但其實最終的輸出結果是:
如果想要達到期望值需要稍微修改一下:
隻需要修改類的定義就可以了,我相信即使沒有 <code>Python</code> 相關經驗對比這兩個代碼應該也能猜到原因:
在 <code>Python</code> 中如果需要将變量作為執行個體變量(也就是每個我們期望的輸出)時,需要将變量定義到構造函數中,通過 <code>self</code> 通路。
如果隻放在類中,和 <code>Java</code> 中的 <code>static</code> 靜态變量效果類似;這些資料由類共享,也就能解釋為什麼會出現第一種情況,因為其中的 <code>sons</code> 是由 <code>Mom</code> 類共享,是以每次都會累加。
既然 <code>Python</code> 可以通過類變量達到變量在同一個類中共享的效果,那是否可以實作單例模式呢?
可以利用 <code>Python</code> 的 <code>metaclass</code> 的特性,動态的控制類的建立。
首先建立一個 <code>Singleton</code> 的基類,然後我們在我們需要實作單例的類中将其作為 <code>metaclass</code>
這樣<code>Singleton</code> 就可以控制 <code>MySQLDriver</code> 這個類的建立了;其實在 <code>Singleton</code> 中的 <code>__call__</code> 可以很容易了解這個單例建立的過程:
定義一個私有的類屬性 <code>_instances</code> 的字典(也就是 <code>Java</code> 中的 <code>map</code>)可以做到在整個類中共享,無論建立多少個執行個體。
當我們自定義類使用了 <code>__metaclass__ = Singleton</code> 後,便可以控制自定義類的建立了;如果已經建立了執行個體,那就直接從 <code>_instances</code> 取出對象傳回,不然就建立一個執行個體并寫回到 <code>_instances</code> ,有點 <code>Spring</code> 容器的感覺。
最後我們通過實驗結果可以看到單例建立成功。
由于最近團隊中有部分業務開始在用 <code>go</code> ,是以也想看看在 <code>go</code> 中如何實作單例。
在這樣一個簡單的結構體(可以簡單了解為 <code>Java</code> 中的 <code>class</code>)中是沒法類似于 <code>Python</code> 和 <code>Java</code> 一樣可以聲明類共享變量的;<code>go</code> 語言中不存在 <code>static</code> 的概念。
但我們可以在包中聲明一個全局變量來達到同樣的效果:
這樣在使用時:
就不需要直接構造 <code>MySQLDriver</code> ,而是通過<code>GetDriver()</code> 函數來擷取,通過 <code>debug</code> 也能看到 <code>driver</code> 和 <code>driver1</code> 引用的是同一個記憶體位址。
這樣的實作正常情況是沒有什麼問題的,機智的朋友一定能想到和 <code>Java</code> 一樣,一旦并發通路就沒那麼簡單了。
在 <code>go</code> 中,如果有多個 <code>goroutine</code> 同時通路<code>GetDriver()</code> ,那大機率會建立多個 <code>MySQLDriver</code> 執行個體。
這裡說的沒那麼簡單其實是相對于 <code>Java</code> 來說的,<code>go</code> 語言中提供了簡單的 <code>api</code> 便可實作臨界資源的通路。
稍加改造上文的代碼,加入了
代碼就能簡單的控制臨界資源的通路,即便我們開啟了100個協程并發執行,<code>mySQLDriver</code> 執行個體也隻會被初始化一次。
這裡的 <code>defer</code> 類似于 <code>Java</code> 中的 <code>finally</code> ,在方法調用前加上 <code>go</code> 關鍵字即可開啟一個協程。
雖說能滿足并發要求了,但其實這樣的實作也不夠優雅;仔細想想這裡
建立執行個體隻會調用一次,但後續的每次調用都需要加鎖進而帶來了不必要的開銷。
這樣的場景每個語言都是相同的,拿 <code>Java</code> 來說是不是經常看到這樣的單例實作:
這是一個典型的雙重檢查的單例,這裡做了兩次檢查便可以避免後續其他線程再次通路鎖。
同樣的對于 <code>go</code> 來說也類似:
和 <code>Java</code> 一樣,在原有基礎上額外做一次判斷也能達到同樣的效果。
但有沒有覺得這樣的代碼非常繁瑣,這一點 <code>go</code> 提供的 <code>api</code> 就非常省事了:
本質上我們隻需要不管在什麼情況下 <code>MySQLDriver</code> 執行個體隻初始化一次就能達到單例的目的,是以利用 <code>once.Do()</code> 就能讓代碼隻執行一次。
檢視源碼會發現 <code>once.Do()</code> 也是通過鎖來實作,隻是在加鎖之前利用底層的原子操作做了一次校驗,進而避免每次都要加鎖,性能會更好。
相信大家日常開發中很少會碰到需要自己實作一個單例;首先大部分情況下我們都不需要單例,即使是需要,架構通常也都有內建。
類似于 <code>go</code> 這樣架構較少,需要我們自己實作時其實也不需要過多考慮并發的問題;摸摸自己肚子左上方的位置想想,自己寫的這個對象真的同時有幾百上千的并發來建立嘛?
不過通過這個對比會發現 <code>go</code> 的文法确實要比 <code>Java</code> 簡潔太多,同時輕量級的協程以及簡單易用的并發工具支援看起來都要比 <code>Java</code> 優雅許多;後續有機會再接着深入。
參考連結:
Creating a singleton in Python
How to implement Singleton Pattern in Go
作者:
crossoverJie
出處:
https://crossoverjie.top
歡迎關注部落客公衆号與我交流。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出,
如有問題, 可郵件(crossoverJie#gmail.com)咨詢。