:主線程讀頁 + 每頁一線程并行輸出 PNG)
圖形學圖像處理【免費下載鏈接】mupdfmupdf mirror項目地址https://gitcode.com/gh_mirrors/mu/mupdf點擊查看免費下載MuPDF 是一個輕量級、模塊化的 PDF/XPS/CBZ/EPUB 渲染引擎其 C API 刻意不綁定任何具體線程框架多線程能力完全通過調用方注入的鎖函數來獲得。本文以倉庫中官方多線程示例 multi-threaded.c 為核心講解一個主線程讀取頁面、為每一頁創(chuàng)建一個渲染線程的經典并行渲染模型包括fz_locks_context鎖機制的初始化、fz_clone_context上下文克隆、display list 跨線程共享以及fz_try/fz_always/fz_catch異常體系在并發(fā)環(huán)境下的正確用法。讀完本文你將能夠獨立編寫一個把 PDF 全頁并行渲染為 PNG 的多線程 C 程序并理解 MuPDF 多線程使用的全部約束與資源生命周期規(guī)則。前置知識先理解單線程版 example.c官方要求在學習多線程示例之前先讀懂單線程示例 docs/examples/example.c。該示例的核心調用鏈為fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED)創(chuàng)建上下文第二參數為NULL表示單線程使用不提供鎖fz_register_document_handlers(ctx)注冊默認文檔格式處理器fz_open_document(ctx, filename)打開文檔fz_count_pages(ctx, doc)統計頁數fz_new_pixmap_from_page_number(...)渲染某頁為 RGB pixmapfz_drop_*系列函數釋放資源。單線程版可以完全忽略鎖的存在而多線程版的一切復雜性——fz_locks_context、上下文克隆、fz_var與異常處理——都源于一個事實多個線程要同時調用 MuPDF。MuPDF 多線程的五大鐵律多線程總覽章節(jié) 明確給出了并發(fā)調用時必須遵守的五條規(guī)則多線程示例正是對這五條規(guī)則的完整落地不同線程不得同時使用同一個 context最簡單的方式是每個線程各用一個 context而新 context 通過克隆產生見下文上下文克隆不同線程不得同時訪問同一個 document同一時刻只能有一個線程訪問文檔對象但從文檔生成的 display list 創(chuàng)建之后多個線程可以同時操作它不同線程不得同時調用同一個 device多線程對同一 device 并發(fā)調用會使其狀態(tài)錯亂甚至崩潰必須串行化創(chuàng)建 context 時必須提供fz_locks_context除非 MuPDF 完全以單線程方式使用——即使使用完全獨立的 MuPDF 實例這一約束也成立所有在用的 context 必須共享同一個fz_locks_context或其底層鎖官方強烈建議fz_new_context只調用一次其余 context 全部由fz_clone_context派生以保證鎖機制一致雖然當前版本仍支持多次fz_new_context創(chuàng)建完全獨立的 context但這些 context 必須共享同一套底層鎖這一能力未來可能被移除。其中第 2 條直接決定了示例程序的架構讀頁只能在主線程做渲染放到各工作線程。為什么需要 fz_locks_context線程框架無關設計MuPDF 自身不依賴任何線程庫它以回調方式要求調用方提供鎖住/解鎖第 N 把鎖的函數。相關定義位于 include/mupdf/fitz/context.htypedef struct fz_locks_context { void *user; void (*lock)(void *user, int lock); void (*unlock)(void *user, int lock); } fz_locks_context; enum fz_lock_id { FZ_LOCK_ALLOC 0, FZ_LOCK_FREETYPE, FZ_LOCK_GLYPHCACHE, FZ_LOCK_MAX };要點fz_locks_context只包含user指針與兩個函數指針user由調用方自由定義通常指向鎖數組本身示例中就是pthread_mutex_t數組從而避免全局變量調用方必須提供FZ_LOCK_MAX把互斥鎖。當前版本中FZ_LOCK_MAX對應三個鎖編號FZ_LOCK_ALLOC內存分配器、FZ_LOCK_FREETYPEFreeType 字體引擎、FZ_LOCK_GLYPHCACHE字形緩存——枚舉值從 0 遞增FZ_LOCK_MAX即鎖的總數這些鎖可以是遞歸鎖也可以不是因為 MuPDF 內部只以非遞歸風格調用為避免死鎖MuPDF 內部有一條簡單規(guī)則絕不先持有編號更大的鎖再去拿編號更小的鎖即不會在持有鎖 n 時去拿任何 i ≤ n 的鎖。若定義FITZ_DEBUG_LOCKING可開啟調試代碼驗證這一規(guī)則context.h單線程程序把fz_new_context的locks參數傳NULL即可見 context.h。示例中的鎖函數實現非常直接用 pthread 包裝即可void lock_mutex(void *user, int lock) { pthread_mutex_t *mutex (pthread_mutex_t *) user; if (pthread_mutex_lock(mutex[lock]) ! 0) fail(pthread_mutex_lock()); } void unlock_mutex(void *user, int lock) { pthread_mutex_t *mutex (pthread_mutex_t *) user; if (pthread_mutex_unlock(mutex[lock]) ! 0) fail(pthread_mutex_unlock()); }由于user被強制轉換為鎖數組指針lock參數0 到FZ_LOCK_MAX-1就是數組下標正好對應FZ_LOCK_ALLOC、FZ_LOCK_FREETYPE、FZ_LOCK_GLYPHCACHE三把鎖。構建與運行在源碼樹中構建頂層 Makefile 提供了examples目標它同時構建example、multi-threaded、storytest、searchtest四個示例其中multi-threaded額外鏈接了-lpthreadexamples: $(OUT)/example $(OUT)/multi-threaded $(OUT)/storytest $(OUT)/searchtest $(OUT)/multi-threaded: docs/examples/multi-threaded.c $(MUPDF_LIB) $(THIRD_LIB) $(LINK_CMD) $(CFLAGS) $(THIRD_LIBS) -lpthread編譯并渲染文檔中每一頁為獨立 PNGmake examples ./build/debug/multi-threaded document.pdf基于安裝產物構建若已安裝 MuPDF默認安裝前綴為/usr/local示例會安裝到/usr/local/share/doc/mupdf/examples見 Makefile可用靜態(tài)庫直接編譯gcc -I/usr/local/include -o multi-threaded \ /usr/local/share/doc/mupdf/examples/multi-threaded.c \ /usr/local/lib/libmupdf.a \ /usr/local/lib/libmupdfthird.a \ -lpthread -lm ./multi-threaded document.pdf運行前的兩個警告示例源碼的注釋明確提醒所有頁面會同時渲染請選擇頁數較少的文件以免過度消耗機器資源每頁一個線程線程數量可能受運行環(huán)境對線程數的限制影響。換句話說這個示例是架構演示而非生產級批量工具——它用最直白的方式展示并發(fā)模型實際產品中通常需要線程池與頁數配額。程序架構總覽一主線程 每頁一線程示例采用官方 overview 推薦的服務器模型overview.md單個主線程獨占 document負責把所有頁面凍結成 display list每個渲染線程各自克隆一個 context只操作共享的 display list 與自己的 pixmap。整體流程主線程初始化FZ_LOCK_MAX把互斥鎖組裝fz_locks_context創(chuàng)建主 context主線程打開文檔、統計頁數 N主線程對第 i 頁加載頁面 → 計算邊界框 → 創(chuàng)建 display list → 用 list device 記錄繪圖命令 → 丟棄頁面與 device主線程為第 i 頁填充struct thread_data并pthread_create一個渲染線程渲染線程克隆 context、渲染 display list 到白底 pixmap、置failed標志主線程逐個pthread_join等待把成功的 pixmap 保存為out%04d.png并統一清理資源。fz_count_pages的返回值直接決定線程數——每頁一線程就是這個示例的設計決策并非 MuPDF 的要求一個線程連續(xù)渲染多頁同樣合法。線程間通信的數據結構struct thread_datastruct thread_data { fz_context *ctx; // 主線程的 context 指針供渲染線程克隆 int pagenumber; // 頁碼用于打印日志 fz_display_list *list; // 主線程生成的頁面繪圖命令列表跨線程共享 fz_rect bbox; // 頁面渲染區(qū)域 fz_pixmap *pix; // 渲染結果主線程傳入NULL渲染線程填充 int failed; // 渲染是否失敗1 表示失敗 };這個結構是主線程與渲染線程之間唯一的通信協議。注意它的數據流方向主線程 → 渲染線程ctx、pagenumber、list、bbox以及初始為NULL的pix和初始為 0 的failed渲染線程 → 主線程填充好的pix由fz_new_pixmap_with_bbox創(chuàng)建與可能被置 1 的failed。步驟一初始化鎖與主 contextpthread_mutex_t mutex[FZ_LOCK_MAX]; for (i 0; i FZ_LOCK_MAX; i) { if (pthread_mutex_init(mutex[i], NULL) ! 0) fail(pthread_mutex_init()); } locks.user mutex; locks.lock lock_mutex; locks.unlock unlock_mutex; ctx fz_new_context(NULL, locks, FZ_STORE_UNLIMITED);關鍵點互斥鎖用非遞歸的pthread_mutex_init(mutex[i], NULL)初始化正好滿足 MuPDF非遞歸風格調用的要求locks.user指向鎖數組本身lock/unlock指向上面的包裝函數——這樣鎖函數無需任何全局變量就能找到對應的鎖主 context 通過fz_new_context創(chuàng)建第二個參數傳入locks。此后所有克隆出的 context 會自動共享同一套鎖符合五大鐵律第 5 條FZ_STORE_UNLIMITED表示資源存儲store不設上限其值為 0如需限制緩存大小可用FZ_STORE_DEFAULT256 MiB或自定義字節(jié)數context.h。store 中緩存字體、圖像等資源多線程下所有克隆 context 共享它。步驟二主線程獨占讀頁并生成 display listfz_register_document_handlers(ctx); doc fz_open_document(ctx, filename); threads fz_count_pages(ctx, doc); thread malloc(threads * sizeof (*thread)); for (i 0; i threads; i) { page fz_load_page(ctx, doc, i); bbox fz_bound_page(ctx, page); list fz_new_display_list(ctx, bbox); dev fz_new_list_device(ctx, list); fz_run_page(ctx, page, dev, fz_identity, NULL); fz_close_device(ctx, dev); // ... 清理 dev 與 page ... }為什么必須在主線程完成這一步因為五大鐵律第 2 條同一時刻只有一個線程可以訪問 document。fz_load_page、fz_bound_page、fz_run_page都屬于對文檔/頁面的訪問因此集中放在主線程注釋也明確寫道This cannot be done on the worker threads, as only one thread at a time can ever be accessing the document.display list 是整個并發(fā)的樞紐頁面被錄制成一組繪圖命令后document 與 page 對象就可以被丟棄display list 則成為可被任意線程、甚至多個線程同時重放的數據。相關 API 定義見 include/mupdf/fitz/display-list.hfz_new_display_list(ctx, mediabox)創(chuàng)建空 display listfz_new_list_device(ctx, list)創(chuàng)建 list device把后續(xù)繪制命令寫入 listfz_run_page(ctx, page, dev, ...)讓頁面畫到 list device 上即錄制繪圖命令fz_close_device(ctx, dev)收尾確保所有命令都已刷入 list之后任意線程用fz_run_display_list重放這些命令。每一輪循環(huán)結束后立即fz_drop_device(dev)與fz_drop_page(page)page 已無用全部繪圖信息都已進入 display list。這也是內存友好的關鍵——N 頁文檔只同時持有 N 個 display list而不是 N 個 page 對象。步驟三渲染線程 renderer 的實現void * renderer(void *data_) { struct thread_data *data (struct thread_data *)data_; int pagenumber >for (i 0; i threads; i) { char filename[42]; struct thread_data *data; if (pthread_join(thread[i], (void **) data) ! 0) fail(pthread_join); if (data-failed) { fprintf(stderr, \tRendering for page %d failed\n, i 1); } else { sprintf(filename, out%04d.png, i); fprintf(stderr, \tSaving %s...\n, filename); fz_save_pixmap_as_png(ctx,>fz_try(ctx) { // 嘗試執(zhí)行任務禁止 return/goto/longjmp 逃出 } fz_always(ctx) { // 無論是否拋異常都會執(zhí)行同樣禁止 return/goto/longjmp } fz_catch(ctx) { // 僅當 try 塊含其調用的函數拋出異常時執(zhí)行 }fz_always塊可省略。三條必須注意的限制禁止從 try 塊內 return/goto/longjmp會破壞宏的內部簿記fz_try/fz_always/fz_catch不是一條原子 C 語句——if (condition) fz_try(ctx) { ... } fz_catch(ctx) { ... }不會按預期工作必須用花括號把整個 try/catch 包進 if 分支由于基于setjmp/longjmp標準 C 對這兩者的限制同樣適用在 fz_try 開始之后、拋異常之前被賦值的真正局部變量其值在異常拋出過程中可能變?yōu)槲炊x。為規(guī)避第 3 條MuPDF 提供fz_var()宏它指示編譯器確保變量不會因異常拋出而被重置。示例中凡是在fz_try內被賦值、且在fz_always/fz_catch中還要使用的變量如dev、thread、doc、page都先聲明、后fz_var登記fz_var(dev); fz_try(ctx) { ... dev fz_new_draw_device(...); ... } fz_always(ctx) fz_drop_device(ctx, dev);如果不寫fz_var(dev)在longjmp后fz_always中讀取的dev可能是垃圾值導致崩潰或泄漏。另外fail()函數用abort()立即終止進程——它只用于 pthread 級別的致命錯誤鎖初始化失敗、線程創(chuàng)建失敗等這類錯誤沒有恢復意義而渲染層面的錯誤則走異常宏路徑通過data-failed溫和地匯報給主線程體現了致命錯誤立即終止、業(yè)務錯誤結構化傳播的層次。設計取舍何時該用每頁一線程示例選擇每頁一個線程注釋明確說明這只是本示例的設計決策而非 MuPDF 的約束。對照 overview.md實現者有兩條基本路線單一線程作為服務器一個線程打開文檔并持續(xù)生成 display list其他線程只做渲染。示例即此路線也是官方認為長期運行更高效的方式自己加鎖串行化文檔訪問在調用文檔相關 API 的外圍包一層自己的互斥鎖讓多個線程輪流訪問 document——正確但效率更低。此外display list 支持多線程同時重放同一份 listbanded rendering分條帶并行渲染因此把每頁一線程擴展為每頁分多個條帶、多個線程并行渲染是完全可行的方向。示例的局限還在于線程數等于頁數頁數很大時會創(chuàng)建過多線程受系統線程數限制且資源占用高——生產代碼通常會改用固定大小的線程池加任務隊列。延伸閱讀單線程基線示例docs/examples/example.c多線程示例源碼本文主體docs/examples/multi-threaded.c本文對應的官方 cookbook 條目docs/cookbook/c/multi-threaded.rst多線程總覽、錯誤處理與上下文克隆docs/reference/c/overview.mdfz_locks_context、FZ_LOCK_MAX、fz_new_context、fz_clone_context的 API 文檔include/mupdf/fitz/context.hdisplay list 生命周期 APIinclude/mupdf/fitz/display-list.hexamples構建目標與安裝規(guī)則Makefile贊分享圖形學圖像處理【免費下載鏈接】mupdfmupdf mirror項目地址https://gitcode.com/gh_mirrors/mu/mupdf點擊查看免費下載相關推薦MuPDF C API 多線程渲染實戰(zhàn)用 display list 并行把 PDF 逐頁渲染為 PNGMuPDF C API 多線程渲染實戰(zhàn)用 display list 并行把 PDF 逐頁渲染為 PNG 導讀 本文圍繞 ext/mupdf/docs/cook桌面應用文檔MuPDF C 語言實戰(zhàn)指南單頁渲染、多線程批量渲染與 Story 排版引擎示例解析MuPDF C 語言實戰(zhàn)指南單頁渲染、多線程批量渲染與 Story 排版引擎示例解析 本篇指南以 MuPDF 官方文檔 C 語言示例章節(jié) https://li圖形學圖像處理MuPDF C Cookbook 實戰(zhàn)解析 SumatraPDF 內置 MuPDF 的渲染、多線程與 Story API 示例MuPDF C Cookbook 實戰(zhàn)解析 SumatraPDF 內置 MuPDF 的渲染、多線程與 Story API 示例 本指南以當前倉庫 ext/mu桌面應用文檔上一篇zls枚舉類型完整的枚舉和聯合支持下一篇容器鏡像加速實戰(zhàn)public-image-mirror 讓鏡像拉取從 90 分鐘縮到 4 分鐘創(chuàng)作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考