執行緒區域快取與爭用問題
想像一座繁忙的倉庫,只有一個中央庫房,門口由一位管理員把守。如果每個工人連最小的零件都得在那道門前排隊,整個工作場的速度就被拖到管理員的步調——那一道門就是瓶頸。現在給每個工人一個裝著他最常用零件的私人工具箱。大多數時候他們只要伸手進自己的箱子,不必排隊、不必找管理員。那個私人工具箱就是執行緒區域快取(thread-local cache),它正是讓記憶體配置器能跨許多執行緒擴展的核心技巧。
它所解決的爭用問題是這樣的:一個簡單的配置器用一把鎖(互斥鎖)保護它唯一的共享堆積。當許多執行緒同時呼叫 malloc() 與 free() 時,它們在那把鎖上序列化——每個都得等輪到自己——於是增加核心不再有幫助,甚至可能更糟,因為執行緒把時間花在搶鎖、以及讓堆積的資料在各 CPU 快取之間彈來彈去。執行緒區域快取的修法是給每個執行緒一小份按 size class 組織、存在執行緒區域儲存中的私有空閒區塊存貨。malloc 的快速路徑變成:在我自己的快取中找這個 size class 的空閒區塊並取下——純粹執行緒區域、完全不取鎖。free 變成:把區塊推回我自己的快取。只有當某執行緒的快取空了(或滿出)時,它才短暫取得共享鎖,一次補充或刷回一整批,於是昂貴的同步操作被攤提在許多便宜的區域操作之上。
這正是為什麼每個可擴展配置器——tcmalloc、jemalloc、mimalloc——都圍繞執行緒快取打造,也是為什麼換進其中之一能讓一台多執行緒伺服器的吞吐量脫胎換骨。誠實的取捨:執行緒快取花費記憶體(每個執行緒握著它目前沒在用的區塊,所以總足跡隨執行緒數成長),且當一個執行緒配置一個區塊、卻由另一個執行緒釋放時可能造成膨脹(區塊最後被快取在錯的執行緒上),所以配置器會加上機制來遷移或定期清掉快取。快取是速度對記憶體的交易,不是免費的午餐。
/* 快速路徑:不取鎖,只看這個執行緒自己的快取 */ _Thread_local struct free_block *tcache[NCLASSES]; void *fast_malloc(size_t n) { size_t c = size_to_class(n); struct free_block *b = tcache[c]; if (b) { tcache[c] = b->next; return b; } /* 命中:執行緒區域、無鎖 */ return refill_from_central(c); /* 未命中:取鎖、批次抓一把 */ }
常見情況只碰執行緒區域儲存、不取鎖;只有批次補充時才碰共享鎖。
執行緒快取以記憶體換速度:每個執行緒釘住它沒在用的空閒區塊,所以足跡隨執行緒數成長,而跨執行緒釋放(一執行緒配置、另一執行緒釋放)需要額外處理以避免膨脹。