我想要用 wxWidgets 寫一個程式,能夠掃瞄整個目錄結構,然後把不需要的檔案刪掉。掃瞄這件事情可以用遞迴來做:

void MainFrame::RecursiveClean(const wxString& dirname) {
  wxDir dir(dirname);
  if (!dir.IsOpened()) {
    return;
  }
  wxString name;
  wxFileName fn(dirname, "");
  bool found = dir.GetFirst(&name);
  while (found) {
    wxString fullpath = fn.GetPathWithSep() + name;
    if (wxDirExists(fullpath)) {
      if (delete_folder_patterns_.Match(name)) {
        // ...delete folder...
      } else {
        RecursiveClean(fullpath);
      }
    } else if (wxFileExists(fullpath)) {
      if (delete_file_patterns_.Match(name)) {
        // ...delete file...
      }
    }
    found = dir.GetNext(&name);
  }
}
Code language: C++ (cpp)

但掃瞄的過程很花時間。如果我們就直接這樣硬幹,在掃瞄的過程中,wxWidgets 就沒有辦法處理事件,導致整個 UI 就會停在那邊沒有反應。所以我們要想個方法去解決。

利用 wxEVT_IDLE

其實最終極的方法應該是把掃瞄放到另一個 thread 去做;但光是想到 thread 之間要怎麼妥善傳遞訊息就頭皮發麻。所以就想說先試著在 wxIdleEvent 的 handler 裡面做。

wxWidgets 的系統在沒有其他事件要處理的時候,就會送出 wxIdleEvent。但預設的行為,是「從 busy 變成 idle」時,才會送一次 event,而不會連續送出。所以,如果我們希望 wxWidgets 持續送出的話,可以在 wxIdleEvent 的 handler 裡,呼叫 wxIdleEvent::RequestMore(),這樣 wxWidgets 就會再送出下一個 wxIdleEvent

另外,為了要把控制權還給系統,我們必須要把任務細切:每一次呼叫 wxIdleEvent 的 handler 的時候,只執行任務的一小部份就結束函式,讓 wxWidgets 有機會處理其他的事件。通常這會是最累的工作。如果原本的任務是以迴圈方式執行還好;像現在這個例子,用到遞迴,就有點麻煩了。

我使用的方法,是利用 std::vector 模擬 stack,記錄正在處理的目錄。Stack 的最上層是目前正在處理的目錄。當程式在這個目錄中掃到子目錄、要往裡面深入時,就把要處理的目錄再往 stack 上層塞;等到最上層的目錄都處理完了,就把它 pop 出來,繼續處理前一層目錄。其實相當於模擬遞迴的行為。

Pseudo code 如下:

class MyFrame::wxFrame() {
  std::vector<Directory> stack;
  //....
  void ProcessSingleItem(void);
  void OnIdle(wxIdleEvent& evt);
}

void MyFrame::OnIdle(wxIdleEvent& evt) {
  if (ProcessSingleItem()) {
    Unbind(wxEVT_IDLE, &CleanStatusDialog::OnIdle, this);
  } else {
    // Add this for more performance
    evt.RequestMore();
  }
}

// Returns true if scanning task is done.
void MyFrame::ProcessSingleItem(void) {
  if (stack.empty()) {
    return true;
  }

  wxString item_name;
  Directory& top = stack.back();
  bool found = top.FindNextItem(&item_name);

  if (found) {
    wxString full_path = top.GetName() + item_name;
    if (is_dir(full_path)) {
      if (delete_folder_patterns_.Match(full_path)) {
        // ...delete folder...
      } else {
        Directory new_dir = Directory(full_path);
        stack.push_back(new_dir);
      }
    } else if (is_file(fullpath)) {
      if (delete_file_patterns_.Match(full_path)) {
        // ...delete file...
      }
    }
  } else {
    stack.pop_back();
  }

  return false;
}Code language: C++ (cpp)

之所以用 pseudo code 呈現、而且用 Directory 這個「假的」class 而不是直接用 wxDir,是因為還有幾個實作上的問題要解決:

  1. wxDir 物件無法被複製(它的 copy constructor 被隱藏起來了),所以無法直接用 std::vector 儲存管理。解決的方法是改存指標,也就是把 stack 的型別改成 std::vector<wxDir*>,或是 std::vector<std::unique_ptr<wxDir>>
  2. 如果第一點是採用 unique_ptr<wxDir> 的話,那在呼叫 std::vector<>::push_back() 的時候,要使用 std::move()。例如:
    auto new_dir = make_unique<wxDir>(full_path);
    stack.push_back(move(new_dir));
    
  3. 當我們想透過 wxDir 取得目錄中的檔案(或子目錄)的名稱時,我們一開始必須先用 wxDir::GetFirst() 取得第一個檔名,然後改用 wxDir::GetNext() 依次取得後面的檔名。因此我們必須有額外的機制記錄這是不是第一次呼叫。
  4. 如果新的 wxDir 物件建立失敗的話,應該要能立即終止整個掃瞄工作。

使用 wxThread 執行掃瞄任務

wxWidgets 提供了兩種不同的 thread 模型:detached(預設)和 joinable。我採用 detached thread 來實作。不過 detached thread 在使用上有幾個需要注意的地方:

  1. Sub thread 在執行結束後會自我銷毀(也就是自己 delete 自己)。雖然好處是不用 main thread 操心,但若是我們需要在 main thread 中保留一份指向這個 thread 的指標,那我們就必須非常小心,因為這個指標隨時可能因為 thread 已經執行結束而失效。
    我們的解決方法,是讓 sub thread 也保留一份指向 parent 的指標,然後在 thread 的解構式中,想辦法想自己的指標清為 nullptr
  2. 當 main thread 想要透過 thread 的指標去存取時(例如去呼叫 wxThread::Delete(),通知 thread 必須提前結束),必須要用 critical section 保護好,否則可能會發生 data racing 的問題。
  3. Sub thread 要傳遞訊息給 main thread,可以利用
    1. wxEvtHandler::QueueEvent() 傳遞標準(或自定義)的事件,有資料的話可以放在事件物件的欄位中。但要特別小心 wxString 物件,因為 wxString 內部使用 reference count 技術,並不符合 thread-safe 的規範。
    2. wxWidgets 3.0 之後提供了 wxEvtHandler::CallAfter() 這個函式,可以直接傳入成員函式的指標(以及簡單的參數)、或是函式物件。等到控制權回到 main thread 時,main thread 才會去執行這個成員函式。

Thread 這個主題實在是太龐大。再找時間寫另一篇文章來討論好了。

By closer

發表迴響