Windows 與 C++

執行緒集區取消和清除

Kenny Kerr

 

取消和清除是難解決多執行緒應用程式時的問題。何時應該關閉控點的安全?有關係的執行緒取消作業?若要更糟的是讓事情,某些多執行緒的 Api 不是可重新進入,可能會改善效能,但也讓開發人員加入複雜性。

我會介紹在上個月的專欄中的執行緒集區環境 (msdn.microsoft.com/magazine/hh394144)。此環境中啟用的重要功能是清除群組,和這就是我把精力放在這裡。清除群組不要嘗試解決所有世界的取消和清除問題。它們的功用是使執行緒集區的物件和回呼更容易管理,及間接協助簡化的取消和清除其他 Api 和資源所需。

到目前為止,我已經僅示範了如何使用 unique_handle 類別範本來自動關閉工作透過 CloseThreadpoolWork 函式的物件。(請參閱 [2011 年 8 月] 欄,在 msdn.microsoft.com/magazine/hh335066 如需詳細資訊。)但是有一些限制,使用這個方法。如果您希望說是否擱置中回呼會取消與否,您需要先呼叫 WaitForThreadpoolWorkCallbacks。這會使兩個函式呼叫的回呼產生的 multipliednumber 物件在使用您的應用程式。如果您選擇要使用 TrySubmitThreadpoolCallback 時,您甚至不會有機會執行這項操作,並會保留想要知道如何取消或等候結果的回呼。當然,實際的應用程式可能會有目前不只是工作物件。在下個月的專欄中,我會先介紹的其他執行緒集區物件產生回呼,物件可等候 i/o 計時器。[取消] 和 [清除所有的協調,很快就的夢魘。幸運的是,清除群組解決這些問題等等。

CreateThreadpoolCleanupGroup 函式會建立一個清除群組物件。如果函式成功,則會傳回不透明的指標,表示清除群組物件。如果失敗,它會傳回 null 指標值,並提供透過 getlasterror 時函式的詳細資訊。指定一個清除群組物件,CloseThreadpoolCleanupGroup 函式指示執行緒集區可能會釋放物件。我提到前例中傳遞,但它會以重複,執行緒集區 API 不會不容許無效的引數。呼叫 CloseThreadpoolCleanupGroup,或任何其他 API 的使用無效,先前關閉的功能或 null 指標值會導致您的應用程式損毀。這些是由程式設計人員所引入的缺失,應該不需要其他的檢查,在執行階段時。我在我 2011 年 7 月的專欄中介紹的 unique_handle 類別範本 (msdn.microsoft.com/magazine/hh288076) 負責這些詳細資料的清除群組特定特性類別:

struct cleanup_group_traits
{
  static PTP_CLEANUP_GROUP invalid() throw()
  {
    return nullptr;
  }
 
  static void close(PTP_CLEANUP_GROUP value) throw()
  {
    CloseThreadpoolCleanupGroup(value);
  }
};
typedef unique_handle<PTP_CLEANUP_GROUP, cleanup_group_traits> cleanup_group;

我現在可以使用方便的 typedef 並建立一個清除群組物件,如下所示:

cleanup_group cg(CreateThreadpoolCleanupGroup());
check_bool(cg);

雖然有私用的集區與回呼優先順序,清除群組是各種回呼產生物件的環境物件相關聯的。 首先,更新的環境,以指出將管理物件和其回呼,就像這樣的存留期的清除群組:

environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);

此時,您可以將物件加入的 [清除] 群組,然後稱為清除群組的成員。 這些物件可以也個別從清除群組中移除,但很常見,若要關閉單一作業中的所有成員。

工作物件可能會在建立時清除群組的成員,只要提供更新的環境,CreateThreadpoolWork 函式:

auto w = CreateThreadpoolWork(work_callback, nullptr, e.get());
check_bool(nullptr != w);

請注意我沒有使用 unique_handle 這一次。 新建立的工作物件現在是環境的清除群組的成員,而且它的存留期需要無法追蹤直接使用 RAII。

您可以撤銷工作物件的成員資格,只要關閉它,它可以完成個別使用 CloseThreadpoolWork 函式。 執行緒集區會知道工作物件清除群組的成員,然後再關閉撤銷其成員資格。 如此可確保當清除群組稍後嘗試關閉它所有成員的應用程式不會損毀。 反向並不是如此: 如果您先指示關閉其所有成員的清除群組,然後現在無效的工作物件上呼叫 CloseThreadpoolWork 時,您的應用程式將會損毀。

當然,清除群組的重點是讓應用程式不需要個別關閉所有使用發生的各種回呼產生的物件。 更重要的是,它可讓應用程式等候,並選擇性地取消任何未完成的回呼,在單一的等候作業而需要有應用程式執行緒會等候,然後繼續重複。 CloseThreadpoolCleanupGroupMembers 函式會提供所有這些服務和的詳細資訊:

bool cancel = ...
CloseThreadpoolCleanupGroupMembers(cg.get(), cancel, nullptr);

這個函式看起來簡單,但是在現實情況下執行一些重要的責任為所有成員的彙總。 首先,取決於第二個參數的值,它會取消還沒開始執行任何擱置中回呼。 接下來,它等待任何已經開始執行的回呼及 (選擇性),任何擱置的回呼如果您選擇不要取消它們。 最後,它會關閉所有其成員的物件。

某些已進行記憶體回收,likened 清除群組,但我認為這是一種可能造成誤導的比喻。 如果有任何項目,清除群組是更像標準樣版程式庫 (STL) 的回呼產生容器物件。 新增到群組的物件將不會自動關閉任何原因。 如果您無法呼叫 CloseThreadpoolCleanupGroupMembers,您的應用程式將會遺漏記憶體。 呼叫 CloseThreadpoolCleanupGroup,以關閉 [群組本身不會協助。 您應該改把這種管理存留期和並行處理的一組物件清除群組。 您當然可以個別管理不同的物件群組的應用程式中建立多個清除群組。 它是一個非常實用的抽象概念,但不魔法,且必須特別注意,若要正確地使用它。 請考慮下列的錯誤虛擬程式碼:

environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);
 
while (app is running)
{
  SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, nullptr, e.get()));
 
  // Rest of application.
}
 
CloseThreadpoolCleanupGroupMembers(cg.get(), true, nullptr);

預測,這段程式碼會使用無限的量的記憶體,且會出現速度變慢,因為系統資源耗盡較慢。

在我年 8 月 2011年欄,我會示範看起來簡單的 TrySubmitThreadpoolCallback 函式是相當有問題,因為沒有簡單的方法,等候其完成的回呼。 這是因為工作物件不會實際公開的 API。 執行緒集區本身,但是,有沒有這樣的限制。 TrySubmitThreadpoolCallback 接受環境的指標,因為您可以間接將產生的工作物件清除群組的成員。 如此一來,您可以使用 CloseThreadpoolCleanupGroupMembers,等待或取消產生回呼。 請考慮下列的改善虛擬程式碼:

environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);
 
while (app is running)
{
  TrySubmitThreadpoolCallback(simple_callback, nullptr, e.get());
 
  // Rest of application.
}
 
CloseThreadpoolCleanupGroupMembers(cg.get(), true, nullptr);

我幾乎無法原諒開發人員的思考模式類似於記憶體回收,這是因為執行緒集區自動關閉 TrySubmitThreadpoolCallback 建立的工作物件。 當然,這是與清除群組無關。 我在我 2011 年 7 月的專欄中說明這個行為。 CloseThreadpoolCleanupGroupMembers 函式在此情況下不一開始負責關閉工作,但是只能等候和物件可能取消回呼。 與前一個範例中,不同的是這種將無限期地執行,而不需要使用不必要的資源,並仍提供 
predictable 取消和清除。 回呼群組的說明,請使用 TrySubmitThreadpoolCallback redeems,提供安全且方便的替代方案。 在高度結構化的應用程式,其中相同的回呼已排入佇列重複,它仍會重複使用明確的工作物件,更有效率,但不再可以關閉這個函式的便利性。

清除群組提供了一個最終的功能,來簡化您的應用程式清除的需求。 通常,並不足夠耐心等候完成未完成的回呼。 您可能需要為每一個回呼產生的物件執行某些清除工作,一旦您確定沒有進一步執行回呼。 需要清除群組管理這些物件的存留期,也表示執行緒集區會知道這類的清除工作時應該發生的最佳位置。

當您清除群組關聯的環境,透過 SetThreadpoolCallbackCleanupGroup 函式時,您也可以提供的回呼,以關閉這些物件的 CloseThreadpoolCleanupGroupMembers 函式的處理程序執行清理群組的每個成員。 因為這是環境的屬性,您就可以不同的物件屬於相同的清除群組,甚至套用不同的回呼。 在下列範例中,我會建立清除群組和清除回呼環境:

void CALLBACK cleanup_callback(void * context, void * cleanup)
{
  printf("cleanup_callback: context=%s cleanup=%s\n", context, cleanup);
}
 
environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), cleanup_callback);

清除回撥的第一個參數是回呼產生物件的內容值。 這是當呼叫 CreateThreadpoolWork 或 TrySubmitThreadpoolCallback 函式時,您所指定的內容值,例如,它是您如何知道的物件清除回呼被呼叫的。 清除回撥的第二個參數是提供做為最後一個參數,呼叫 CloseThreadpoolCleanupGroupMembers 函式時的值。

現在請考慮下列的工作物件和回呼:

void CALLBACK work_callback(PTP_CALLBACK_INSTANCE, void * context, PTP_WORK)
{
  printf("work_callback: context=%s\n", context);
}
 
void CALLBACK simple_callback(PTP_CALLBACK_INSTANCE, void * context)
{
  printf("simple_callback: context=%s\n", context);
}
 
SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, "Cheetah", e.get()));
SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, "Leopard", e.get()));
check_bool(TrySubmitThreadpoolCallback(simple_callback, "Meerkat", e.get()));

以下何者不是像其他人? 就像這是可愛的小 Meerkat 想要在南非他相鄰大貓一樣,他只要絕對不其中一個圖形。 清除群組成員,如下所示關閉時,會發生什麼事?

CloseThreadpoolCleanupGroupMembers(cg.get(), true, "Cleanup");

很少是多執行緒程式碼中。 如果回呼管理執行正在取消和關閉之前,下列可能會列印:

work_callback: context=Cheetah
work_callback: context=Leopard
simple_callback: context=Meerkat
cleanup_callback: context=Cheetah cleanup=Cleanup
cleanup_callback: context=Leopard cleanup=Cleanup

常見的錯誤,就會假設清理回呼會呼叫其回呼未取得有機會執行的物件。 在 Windows API 是有點誤導之虞,因為有時候是指至清理回呼以做為 「 取消 」 回撥,但這不是這種情況。 清除回呼就稱為清除群組的每位目前成員。 您可以將它視為解構函式清除群組成員,但與記憶體回收集合象徵物中,這不是沒有風險。 這個比喻佔用美觀,也等到 TrySubmitThreadpoolCallback 函式,同樣地介紹複雜的因素。 請記住執行緒集區會自動關閉這個函式會建立,因為其回呼執行基礎工作物件。 這表示清理回呼執行這個隱含的工作物件取決於其回呼已經開始執行時,會呼叫 CloseThreadpoolCleanupGroupMembers。 清理回呼就只會執行這個隱含的工作物件其工作回呼是否仍在擱置中的,您要求取消任何擱置中回呼的 CloseThreadpoolCleanupGroupMembers。 這是所有而意外,,因此不建議使用清理回呼的 TrySubmitThreadpoolCallback。

最後,值得一提,即使 CloseThreadpoolCleanupGroupMembers 區塊,它並不會浪費任何時間。 已經準備好清除任何物件必須在呼叫的執行緒上執行,而它會等到其他未完成的回呼,以完成其清理回呼。 清除群組所提供的功能 — 尤其,CloseThreadpoolCleanupGroupMembers 函式,來有效地和完全分裂下所有或部份您的應用程式的工作非常重要。

Kenny Kerr 是軟體工藝人與熱情的原生的 Windows 開發。到達他在 kennykerr.ca

感謝給下列技術專家來檢閱這份文件: Hari Pulapaka