2013年2月11日 星期一

GUI 架構的考量

( 和 Scott 聊過後, 決定改寫《非同步程式新手心得: 為什麼要用單一 thread + task queue 的方式設計架構》的前半段。比較完整地描述 GUI 架構的入門心得。 )

GUI 需要低延遲的反應速度, 隨時能接收使用者輸入和並提供畫面回饋。這表示要在不互相阻檔的前提下應付三種工作:

  • 更新畫面
  • 偵測使用者輸入
  • 處理使用者輸入

甚至第四種情況:

  • 網路輸入

若是使用 multi-thread 處理上述情況 (比方說有三個 thread 分別專職處理前述三件工作), 必須留意 thread 之間有共享資料的情況。為了避免 race condition 必須使用 lock; 使用 lock 後又要擔心 dead lock。

使用 global lock 有可能成效不彰, 比方說更新畫面太久, 而沒有即時回饋使用者的輸入。另一方面, 切許多細小的 lock 處理各塊共享資料, 容易出錯造成 dead lock, 或誤以為有處理好卻仍存在 race condition。

反過來說, 若每個工作 (#1) 都切得很細, 執行時間不長, 但可能需要多個工作單元才能完成目標。這樣就能在一個 thread 內以類似 queue consumer 的方式執行全部的工作。有新的需求時, 放入工作到 queue 裡, 大致上可確保相依工作之間的執行順序 (#2)。由於全部工作都在同一 thread 執行, 自然不用擔心 dead lock 或 race condition。

於是可以看到, 像 Gtk、iOS、Android 的平台, 都將更新畫面、偵測事件和處理事件三者整合在 main thread 處理, 並且假設程式以 single thread 的方式執行 (#3)。

我不清楚其它平台的情況, iOS 有提供 NSURLRequest 應付常見的網路需求。讀寫資料和事件的 callback 也都在 main thread 內執行。

這麼一來, 框架開發者省事許多, 使用框架的開發者也可以安心地實作 callback。

備註

#1 可想像每個工作是一個函式, 在 iOS 或 Android 上, 整合這種「工作 queue」的概念到基本框架裡, 用起來很直覺, 希望盡量避免開發者從 thread 的角度去處理事情。

#2 不行的時候只好加入狀態變數了解目前該做什麼, 至少不用擔心狀態變數本身會有 race condition。

#3 若開發者有特別需求, 比方涉及檔案或網路 I/O, 或是需要大量 CPU 計算, 而無法在短時間內完成單一工作的話, 開發者需要自行另外處理。使用新的 thread 的話, 自然需要使用 lock 保護和 main thread 共享的資料, 但需要煩惱的範圍已降致最小。

async 與 non-blocking IO

以前一直沒有分清楚 asynchronous 和 non-blocking 的差異, 寫久了以後才明白兩者是不同層次的東西。

  • asynchronous programming: 程式不是一條線走到底, 可能目前做到一個段落停下來, 「過一會兒」再繼續執行
  • non-blocking IO: 呼叫讀寫函式時, 不會等到完成才返回

通常要讀寫外部資料時 (一般檔案、網路、pipe 等) , 無法確定返回的時間, 這樣會阻擋程式的運作, 所以希望這個讀取 (或寫入) 不要等完成後才返回 (即「非同步」)。

若不希望此次呼叫有不確定時間的擔擱, 有兩種可能的作法:

  • 開新 thread 或用既有的 thread 進行讀寫。總之, 停是停別人家的 thread, 和本 thread 無關
  • 設定 file descriptor 為 O_NONBLOCK, 告訴 OS 這個 fd 為 non-blocking IO, 呼叫後要能立即返回

兩者作法的優缺點分析已超出本「隨手記」的範圍 (書本作者愛用的大絕), 需要特別注意的是, 設了 O_NONBLOCK 不表示呼叫 read/write 後就結束了。

man 2 read (或 man 2 write) 的 ERRORS 提到:

  • EAGAIN The file descriptor fd refers to a file other than a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.
  • EAGAIN or EWOULDBLOCK The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.

意思是說:

  • 招對實體檔案無效 (為了檔案讀寫效能, 一次做完可減少硬碟更動讀寫頭的時間)
  • 對 socket 仍有可能失效, 記得用 select / epoll 之類的 system call 請系統在有機會完成 non-blocking 操作時通知你。也就是說, 系統只保證此次呼叫不會 block 你, 不保證此次呼叫一定會成功。由開發者持續嘗試呼叫, 總有完成的時候。

vim 顯示目前函式的名稱

我平常習慣用 gj 跳到某個 symbol 位置, 但接下來上下讀程式時, 有時需要知道目前在那個函式裡, 幸好有前人提供 script 做這件事。省了自己摸索的時間。

藉這機會順便學一些 vim script 的語法:

fun! ShowFuncName()
  let lnum = line(".")  " (1)
  let col = col(".")
  echohl ModeMsg        " (2)
  echo getline(search("^[^ \t#/]\\{2}.*[^:]\s*$", 'bW'))  " (3)
  echohl None
  call search("\\%" . lnum . "l" . "\\%" . col . "c")  "(4)
endfun
map F :call ShowFuncName() <CR>  " (5)

注意 vim 是用 「"」 表示後面的字為註解

1.

  • let VAR = VALUE: vim 的設值
  • line("."): expression, 取回目前行數
  • col("."): expression, 取回目前欄數

這裡存下目前的位置, 在 (4) 的時候可以跳回原位 (後述)。

2.

  • 切換 echo 字串 highlight 的方式, 比方 echohl WarningMsg 預設會顯示紅字

3.

  • :call FUNCTION(...): 呼叫函式 FUNCTION
  • getline(N): 顯示檔案內第 N 行字串
  • search(REGEXP, FLAG): 搜尋字串, 跳到符合的位置並傳回顯示符合的行數
  • :call search(REGEXP, 'bW'): b 表示往上搜; W 表示搜到底的時候, 不用從頭 (或尾) 繼續搜。見 Searching - Vim Tips Wiki 了解更多 search 技巧
  • 所以這一行的意思是往上找到開頭「兩個字不為空白字元和註解開頭」的行, 並且此行結尾不能是':'
  • 由於 search() 會傳回行數, 搭配 getline() 完成目的

若希望此函式一併支援 Objective-C 的函式, 更改 REGEXP 即可, 雖說會有一些辛苦就是了。

4.

  • 由於 search() 會更動游標位置, 再用 search("\\%Yl\\%Xc") 跳回原本的位置, 其中 (X, Y) 表示原本的游標位置

5.

  • 我平時會用到 f, 所以改用 F 觸發這個函式。

2013年2月9日 星期六

非同步程式新手心得: 為什麼要用單一 thread + task queue 的方式設計架構

最近多了一些 GUI + 非同步IO 的開發經驗, 簡記一下心得。

GUI 架構背後的考量

  • 由於 GUI 需要低延遲的反應速度, 必須使用非同步的方式實作。
  • Gtk/Android/iOS 都在 main thread 裡處理畫面顯示和事件 callback 是有道理的。藉由架構保證所有 task (繪圖、偵測事件、事件 callback) 在同一 thread 內執行, 寫程式時不用擔心 race condition。反之, 若將繪圖、偵測事件、事件 callback 三者拆到兩個 threads 以上, 會衍生許多潛在問題。
  • 共享變數太雜時, 使用 global lock + multi-thread 無法提高太多效能, 失去 multi-thread 的意義。
  • 若依各別變數使用不同的 lock 容易寫出問題。
  • 在同一個 thread 內執行內全部的 task, 可以避免使用 lock, 相對省事許多。然後用戶有效能需求時, 針對有需求的部份另開 thread 加速, 減少需要使用 lock 的地方。

實作注意事項

  • 由於程式不是一路通到底, 不適合用以往的直線方式思考。
  • 盡量從環境狀態的角度了解如何處理當下的 task, 而減少保證 task 之間的執行順序。這樣寫起來比較簡單, 也會比較穩定。
  • 程式本身或註解要能表示清楚狀態變化對各 task 的影響。提供 debug flag 可以 log 各 task 執行順序, 或註解提及主要的和特別的 task flow, 應該有助於日後維護。
  • 若能留下產生每個 task 的 task 為何, 有助於日後在 debugger 內除錯。
  • 開始實作以前, 要想清楚可能的狀態變化, 以及留好協助維護的 debug code, 由 debug flag 可以開關。

iOS crash 除錯小技巧

參考文章:

摘錄重點:

  • SIGABRT 比較好解, 是 framework 偵測到有異常, 能更接近問題的源頭; EXC_BAD_ACCESS 發生在存取記憶體出錯, 離案發現場可能有一段時間
  • 記得在 breakpoint 的輔助畫面君上 "Exception All + Break on Throw" 的 breakpoint, 可在有 exception 時看到 backtrace
  • edit scheme -> Diagnostics -> Enable Zombie Objects: 有機會在存取到已 release 的物件時, 當下抓到錯誤, 避免到後面出現不知所已的 EXC_BAD_ACCESS

之前用了 "Break on Throw" 和 "Enable Zombie Objects" 後, 減少了不少除錯時間。

2013年2月2日 星期六

Objective-C++ 使用 C function 的注意事項

寫 Objective-C++ (*.mm) 的時候, 記得規則和 C++ 一樣, 函式名稱會有name mangling。呼叫純 C 的函式時, 要留意編譯 C 的原始碼時, 是用 C 或 C++ 的方式編譯。若 object file 是用 C 的方式編譯, 引入的 header 檔要加上 "extern C" 的語法。

話說我是用 nm 看編出的 static lib 和呼叫它的 object file 時, 發現有兩個長得很像但不一樣的函式名稱, 才想起這件事。

相關文章:

XCode 編譯連結 static library 的注意事項

最近遇到兩個狀況

  • link 執行檔時, 找不到 static library 內用的 symbol
  • link 執行檔時, 找不到 Core Data 用的 schema (*.momd)

重新回憶《解決 undefined symbol / reference》, 想到 Mac OS/iOS 也是和 Linux 類似的架構, 原理應該一樣。上網搜了相關文章, 得到以下的解法:

  • static library 只能含有 object file, target 為 static library 的時候, 設定 Build Phases 裡的 Link Binary With Libraries 沒有意義。應該要設 application target 的 Link Binary With Libraries, 因為是建立 application (unit test, etc) 時才會 link libraries
  • 同上, static library 不能含有其它 resource file, 要放在其它 target 的 bundle 裡, 然後 static library 裡的程式讀另外的 bundle。

參考資料:

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...