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