天天看點

軟體事務記憶體導論(十一)-STM的局限性

stm消除了顯式的同步操作,是以我們在寫代碼時就無需擔心自己是否忘了進行同步或是否在錯誤的層級上進行了同步。然而stm本身也存在一些問題,比如在跨越記憶體栅欄失敗或遭遇競争條件時我們捕獲不到任何有用的資訊。我似乎可以聽到你内心深處那個精明的程式員在抱怨“怎麼會這樣啊?”。确實,stm是有其局限性的,否則本書寫到這裡就應該結束了。stm隻适用于寫沖突非常少的應用場景,如果你的應用程式存在很多寫操作競争,那麼我們就需要在stm之外尋找解決方案了。

下面讓我們進一步讨論stm的局限性。stm提供了一種顯式的鎖無關程式設計模型,這種模型允許多個事務并發地運作,并且在沒有發生沖突時所有事務都能毫無滞礙地運作,是以相對其他程式設計模型而言stm可以提供更好的并發性和線程安全方面的保障。當事務對相同對象或資料的寫通路發生沖突時,隻有一個事務能夠順利完成,其他事務都會被自動重做。這種重做機制延緩了寫操作沖突時競争失敗的那些寫者的執行,但卻提升了讀者和競争操作的勝利者的執行速度。當對于相同對象的并發寫操作不頻繁時,其性能就不會受到太大影響。但是随着沖突的增多,程式整體性能将是以變得越來越差。

如果對相同資料有很高的寫沖突機率,那麼我們的應用程式輕則寫操作變慢,重則會因為重試太多次而導緻失敗。目前在本章我們所看到的例子都是在展示stm的優勢,但是在下面的例子中我們将會看到,雖然stm是易于使用的,但也并非在所有應用場景下都能得到理想的結果。

在4.2節的示例中,當多個線程同時通路多個目錄時,我們使用atomiclong來對檔案大小的并發更新操作進行同步。此外,如果需要同時更新多個變量,我們也必須依賴同步才能完成。雖然表面看起來使用stm對這段代碼進行重構似乎是個不錯的選擇,但大量的寫沖突卻使得stm不适用于這個應用場景。下面就讓我們将上述計算目錄大小的程式改用stm實作,并觀察其運作結果是否如我們所預料的那麼差。

在下面的代碼中,我們沒有使用atomiclong,而是采用了akka托管引用作為filesizewstm的屬性字段。

<code>1</code>

<code>public</code> <code>class</code> <code>filesizewstm {</code>

<code>2</code>

<code>private</code> <code>executorservice service;</code>

<code>3</code>

<code>final</code> <code>private</code> <code>ref&lt;long&gt; pendingfilevisits =</code><code>new</code> <code>ref&lt;long&gt;(0l);</code>

<code>4</code>

<code>final</code> <code>private</code> <code>ref&lt;long&gt; totalsize =</code><code>new</code> <code>ref&lt;long&gt;(0l);</code>

<code>5</code>

<code>final</code> <code>private</code> <code>countdownlatch latch =</code><code>new</code> <code>countdownlatch(</code><code>1</code><code>);</code>

為了保證安全性,pendingfilevisits的增減都需要在事務内完成。而在之前使用atomiclong時,我們隻需要簡單調用incrementandget()函數和decrementandget()函數就行了。但是由于托管引用都是通用的(generic),沒有專門針對數字類型的處理方法,是以我們還需要針對pendingfilevisits進行一些額外的加工,即把對于pendingfilevisits的操作封裝到一個單獨的函數裡。

<code>private</code> <code>long</code> <code>updatependingfilevisits(</code><code>final</code> <code>int</code> <code>value) {</code>

<code>return</code> <code>new</code> <code>atomic&lt;long&gt;() {</code>

<code>public</code> <code>long atomically() {</code>

<code>pendingfilevisits.swap(pendingfilevisits.get() + value);</code>

<code>return</code> <code>pendingfilevisits.get();</code>

<code>6</code>

<code>}</code>

<code>7</code>

<code>}.execute();</code>

<code>8</code>

在完成上述定義之後,通路目錄和計算檔案大小的函數就相對容易多了,我們隻需要把程式中的atomiclong替換成托管引用就好。

<code>01</code>

<code>private</code> <code>void</code> <code>findtotalsizeoffilesindir(</code><code>final</code> <code>file file) {</code>

<code>02</code>

<code>try</code> <code>{</code>

<code>03</code>

<code>if</code> <code>(!file.isdirectory()) {</code>

<code>04</code>

<code>new</code> <code>atomic() {</code>

<code>05</code>

<code>public</code> <code>object atomically() {</code>

<code>06</code>

<code>totalsize.swap(totalsize.get() + file.length());</code>

<code>07</code>

<code>return</code> <code>null</code><code>;</code>

<code>08</code>

<code>09</code>

<code>10</code>

<code>}</code><code>else</code> <code>{</code>

<code>11</code>

<code>final</code> <code>file[] children = file.listfiles();</code>

<code>12</code>

<code>if</code> <code>(children !=</code><code>null</code><code>) {</code>

<code>13</code>

<code>for</code><code>(</code><code>final</code> <code>file child : children) {</code>

<code>14</code>

<code>limitations of stm •</code><code>137</code>

<code>15</code>

<code>updatependingfilevisits(</code><code>1</code><code>);</code>

<code>16</code>

<code>service.execute(</code><code>new</code> <code>runnable() {</code>

<code>17</code>

<code>public</code> <code>void</code> <code>run() {</code>

<code>18</code>

<code>findtotalsizeoffilesindir(child); }</code>

<code>19</code>

<code>});</code>

<code>20</code>

<code>21</code>

<code>22</code>

<code>23</code>

<code>if</code><code>(updatependingfilevisits(-</code><code>1</code><code>) ==</code><code>0</code><code>) latch.countdown();</code>

<code>24</code>

<code>}</code><code>catch</code><code>(exception ex) {</code>

<code>25</code>

<code>system.out.println(ex.getmessage());</code>

<code>26</code>

<code>system.exit(</code><code>1</code><code>);</code>

<code>27</code>

<code>28</code>

最後,我們還需要寫一些建立executor服務池和使程式運作起來的代碼:

<code>private</code> <code>long</code> <code>gettotalsizeoffile(</code><code>final</code> <code>string filename)</code>

<code>throws</code> <code>interruptedexception {</code>

<code>service = executors.newfixedthreadpool(</code><code>100</code><code>);</code>

<code>findtotalsizeoffilesindir(</code><code>new</code> <code>file(filename));</code>

<code>latch.await(</code><code>100</code><code>, timeunit.seconds);</code>

<code>return</code> <code>totalsize.get();</code>

<code>}</code><code>finally</code> <code>{</code>

<code>service.shutdown();</code>

<code>public</code> <code>static</code> <code>void</code> <code>main(</code><code>final</code> <code>string[] args)</code><code>throws</code> <code>interruptedexception {</code>

<code>final</code> <code>long</code> <code>start = system.nanotime();</code>

<code>final</code> <code>long</code> <code>total =</code><code>new</code> <code>filesizewstm().gettotalsizeoffile(args[</code><code>0</code><code>]);</code>

<code>final</code> <code>long</code> <code>end = system.nanotime();</code>

<code>system.out.println(</code><code>"total size: "</code> <code>+ total);</code>

<code>system.out.println(</code><code>"time taken: "</code> <code>+ (end - start)/</code><code>1</code><code>.0e9);</code>

由于我懷疑這段代碼跑起來之後可能有問題,是以如果在程式中抓到事務失敗所導緻的異常,我就會結束掉整個應用程式。

根據事務的定義,如果變量的值在事務送出之前發生了改變,那麼事務将會自動重做。在本例中,多個線程會同時競争修改這兩個可變變量,進而導緻程式運作變慢或失敗。我們可以在多個不同的目錄上分别運作上述示例代碼來進行觀察,下面就列出了該示例程式在我的電腦上計算/etc和/usr這兩個目錄的輸出結果:

<code>total file size</code><code>for</code> <code>/etc</code>

<code>total size:</code><code>2266408</code>

<code>time taken:</code><code>0.537082</code>

<code>total file size</code><code>for</code> <code>/usr</code>

<code>too many retries on transaction</code><code>'defaulttransaction'</code><code>, maxretries =</code><code>1000</code>

<code>...</code>

從輸出結果來看,stm版本對于/etc目錄的計算結果與之前使用atomiclong的那個版本是完全相同的。但是由于會産生過多的重試操作,是以stm版本的運作時間要比後者慢一個數量級。而周遊/usr目錄的運作情況則更為糟糕,有相當多的事務超過了預設的最大重試限制。雖然我們的邏輯是一抓到異常就會立即終止整個程式,但由于多個事務是并發運作的,是以在程式真正停止之前我們還是能看到多條錯誤資訊輸出到控制台。

有個别評論家曾建議說是否用commute代替alter會對解決這個問題有所幫助。請回憶我們在6.4節中曾讨論過的在clojure中用來修改托管引用的那三個函數。由于在事務失敗之後不會進行重試,是以commute可以提供比alter更高的并發度。此外,commute也不會在沒有hold住調用方事務的情況下就單獨執行送出操作。然而單純就計算目錄大小這個程式而言,使用commute對性能的提升十分有限。在面對結構複雜的大型目錄時,使用該函數也無法在提供良好性能的前提下獲得一緻性的結果。除了将alter換成commute之外,我們還可以嘗試将atom與swap!函數一起使用。雖然atom是不可調整并且同步的操作,但其優點是不需要使用事務。此外,atom僅能在對單個變量(例如計算目錄大小示例中用于記錄目錄大小的變量)的變更時使用,并且變更期間不會遇到任何事務性重試。然而,由于在對atom做變更時會産生對使用者透明的同步操作,是以我們依然會遇到同步操作所導緻的延遲問題。

由于大量線程會同時嘗試更新totalsize變量,是以計算目錄大小示例在實際執行過程中會産生非常頻繁的寫沖突,這也就意味着stm不适合于解決此問題。事實上,當讀操作十分頻繁且寫沖突被控制在合理範圍内時,stm的性能還是不錯的,同時還能幫程式員免除顯式同步的負擔。但是在不考慮一般程式中常見的其他導緻延時問題的前提下,如果待解決問題中含有大量寫沖突,那就請不要使用stm,而是考慮采用我們在第8章中将會讨論的actor模型來避免同步操作。

stm是一個針對并發問題的非常強大的程式設計模型,該模型有很多優點:

stm可以根據應用程式的行為來充分挖掘出其最大的并發潛力。也就是說,用了stm之後,我們可以無需使用過度保守的、需要預先定義的同步操作,而是讓stm動态地管理競争沖突。

stm是一種鎖無關的程式設計模型,該模型可以提供良好的線程安全性和很高的并發性能。

stm可以保證明體僅能在事務内被更改。

stm沒有顯式鎖意味着我們從此無需擔心加鎖順序及其他相關問題。

stm可以幫助我們減輕前期設計的決策負擔,有了它我們就無需關心誰對什麼東西上了鎖,而隻需放心地把這些工作交給動态隐式組合鎖(implicit lock composition)。

該模型适用于對相同資料存在并發讀且寫沖突不頻繁的應用場景。

如果應用程式的資料通路方式符合stm的适用範疇,則stm就為我們提供了一種處理共享可變性的高效解決方案。而如果我們的應用場景裡寫沖突非常多,我們可能就會更傾向于使用将在第8章中讨論的基于角色(actor)的模型。但在下一章,還是讓我們先學習一下如何在其他jvm上的語言中使用stm程式設計模型。