等待0秒?

等待 0 秒?1 - Scratch 執行緒深入研究

常常有學生在做 Scratch 遊戲或動畫的時候,不管是要讓角色動起來或是變換造型,都會用到積木『等待OO秒』。但是,有沒有想過,如果執行『等待 0 秒』是否會被忽略掉而不等待?

利用變換造型做一個跳舞小動畫,應該是初學 Scratch 時做的第一個小專案。變換造型時,一定利用到了『等待OO秒』積木,而傑夫老師一定會說 0.1 秒是最適合的。

但有沒有被學生問過,Scratch 的重複迴圈 1 秒可以跑幾圈?最基本的答案是,如果沒有其他程式同時在執行,那看你的電腦效能。做個簡單的實驗:

這台電腦大約是50萬圈

這台電腦大約是50萬圈

在沒有其他程式執行的"理想"狀態,依照電腦效能可以跑上 50 萬圈。

加上『等待 0.1 秒』 1 秒執行了 9 圈

加上『等待 0.1 秒』 1 秒執行了 9 圈

如果加上『等待 0.1 秒』,1秒執行了9圈,也符合預期。

但換成了『等待 0 秒』,結果會是什麼呢?

換成了『等待 0 秒』 1 秒執行了 31 圈

換成了『等待 0 秒』 1 秒執行了 31 圈

那換一個方式,不用等待,但角色做造型變換,試試看!

換成了『圖像效果改變』 1 秒執行了 32 圈

換成了『圖像效果改變』 1 秒執行了 32 圈

『圖像效果改變』與『等待 0 秒』執行的結果幾乎一樣!?

或者,再做個改變:

換個方式執行『圖像效果改變』 1 秒也是執行了 32 圈

換個方式執行『圖像效果改變』 1 秒也是執行了 32 圈

利用另一個程式積木串執行『圖像效果改變』, 1 秒也是執行了 32 圈。

最後,改回同學們最喜歡用的 『等待 0.01 秒』

換成了『等待 0 .01 秒』 1 秒執行了 31 圈

換成了『等待 0 .01 秒』 1 秒執行了 31 圈

『等待 0.01 秒』也是 31 圈。

所以~結論是:如果有其他的程式要執行, 1 秒就只剩下 30 圈左右!換句話說,等待 0.033 秒以下根本沒意義。

怎麼會這樣!?待傑夫老師下篇再說明。

等待 0 秒?2 - Scratch 執行緒深入研究

前篇說明了『等待 0 秒』是會讓程式稍微停下來的,而在前面的單元提到了「讓點 (yield point)」,這兩者有什麼關係?這一篇就來深入研究。這篇內容主要翻譯自 Scratch 專家 Cliff Davies 的 "Scratch and its inner workings"

前面的單元提到了 Scratch 執行緒的「讓點 (yield point)」,意思是一段程式積木串的執行緒 (thread) 遇到了讓點就會暫停下來,讓給執行緒列表 中的下一個執行緒執行。這樣的過程,會使得列表中所有的執行緒都被執行到,而當列表中最後一個執行緒被執行到讓點或執行完畢時,會再回到列表中的第一個執行緒繼續執行下去,一直輪迴,直到所有執行緒都被執行完畢列表清空了為止。

那麼就來瞭解一下,哪些時機是「讓點」

  • 任何『重複迴圈(重複無限、重複OO次、重複直到)』內的最後一個積木執行之後

  • 執行完『廣播並等待』之後

  • 執行到『等待OO秒』積木且秒數還沒到達時

  • 每次遇到『等待直到』中的判斷式為否 (false) 時

  • 執行到『停止全部』時

第一個『重複迴圈』已經在前面的單元說明過;最後一個『停止全部』就是要結束了非常合理;其他三個時機都與各式的『等待』有關,等待的期間讓給其他的執行緒執行,其實一點也不難理解。

所以,正如前篇的實驗,『等待 0 秒』是因為執行緒遇到了讓點,Scratch 必須暫停執行緒去檢查列表並試圖轉換執行緒,但做這些事情需要一點時間,並不會毫不等待。而且,依照實驗結果『等待 0 秒』平均消耗了 0.033 秒的時間。

但為什麼是 0.033 秒?待傑夫老師下篇再說明。

等待 0 秒?3 - Scratch 執行緒深入研究

前篇提到了『等待 0 秒』是因為執行緒遇到了「讓點」,Scratch 必須暫停執行緒去並轉換到下一個執行緒去執行,並不會毫不等待,依照實驗結果『等待 0 秒』消耗了 0.033 秒的時間。

但為什麼是 0.033 秒?數學好的可以發現是 1/30,社群中也有人早就提到了是因為 30fps (frame per second),這些是什麼關係,傑夫老師就來說明。

Scratch 限制了專案畫面更新率在 30 fps,意思是每秒最多更新畫面 30 次,也就是每 0.033 秒更新畫面一次。相較於之前實驗的,一秒可以跑上 50 萬圈,0.033 秒可以做很多運算了。

每當 Scratch 要從列表中的第一個執行緒開始執行所有執行緒前,會檢查每個執行緒是否會有需要更新畫面,例如:角色有移動、旋轉、顏色有變化或是有『等待』。並用一個「旗標 "flag"」紀錄,如果沒有要更新畫面,此旗標的值就為「否 "false"」;但若有任何一個執行緒會更新畫面,那此旗標的值就被設為了「是 "true"」。

接著 Scratch 開始一一執行列表中的執行緒,每遇到讓點就換下一個執行緒,直到最後一個執行緒遇到讓點時,如果旗標的值為「是 "true"」, 那就要做一次畫面更新。這點很重要,只要有一個執行緒需要更新畫面,會造成所有的執行緒都停下來,等待畫面做完更新。而剛剛說了,畫面更新率被限制在每秒 30 次,所以,所有的角色移動、變色等等都統一每 0.033 秒發生一次。

注意,剛剛說的是"所有的執行緒"都會停下來,所以這種狀況,會造成 count 這個積木串,不再是 50 萬次。

只要有一個執行緒需要跟新畫面,所有的執行緒都要等待畫面更新

只要有一個執行緒需要跟新畫面,所有的執行緒都要等待畫面更新

而『等待』積木也把旗標設為了「是 "true"」,為了等待畫面更新,導致等待了 0.033 秒,而不是預期的 0 秒。

『等待』積木也把旗標設為了「是 "true"」,導致等待了 0.033 秒

『等待』積木也把旗標設為了「是 "true"」,導致等待了 0.033 秒

簡而言之,除非你的 Scratch 專案完全沒有畫面更新,否則一般的狀況下,重複迴圈一秒 30 圈,等待小於 0.033 秒毫無意義。