MuPDF C 多线程渲染实战:主线程读页 + 每页一线程并行输出 PNG

发布时间:2026/10/6 15:54:25

MuPDF C 多线程渲染实战:主线程读页 + 每页一线程并行输出 PNG 图形学图像处理【免费下载链接】mupdfmupdf mirror项目地址https://gitcode.com/gh_mirrors/mu/mupdf点击查看免费下载MuPDF 是一个轻量级、模块化的 PDF/XPS/CBZ/EPUB 渲染引擎其 C API 刻意不绑定任何具体线程框架多线程能力完全通过调用方注入的锁函数来获得。本文以仓库中官方多线程示例 multi-threaded.c 为核心讲解一个主线程读取页面、为每一页创建一个渲染线程的经典并行渲染模型包括fz_locks_context锁机制的初始化、fz_clone_context上下文克隆、display list 跨线程共享以及fz_try/fz_always/fz_catch异常体系在并发环境下的正确用法。读完本文你将能够独立编写一个把 PDF 全页并行渲染为 PNG 的多线程 C 程序并理解 MuPDF 多线程使用的全部约束与资源生命周期规则。前置知识先理解单线程版 example.c官方要求在学习多线程示例之前先读懂单线程示例 docs/examples/example.c。该示例的核心调用链为fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED)创建上下文第二参数为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 多线程的五大铁律多线程总览章节 明确给出了并发调用时必须遵守的五条规则多线程示例正是对这五条规则的完整落地不同线程不得同时使用同一个 context最简单的方式是每个线程各用一个 context而新 context 通过克隆产生见下文上下文克隆不同线程不得同时访问同一个 document同一时刻只能有一个线程访问文档对象但从文档生成的 display list 创建之后多个线程可以同时操作它不同线程不得同时调用同一个 device多线程对同一 device 并发调用会使其状态错乱甚至崩溃必须串行化创建 context 时必须提供fz_locks_context除非 MuPDF 完全以单线程方式使用——即使使用完全独立的 MuPDF 实例这一约束也成立所有在用的 context 必须共享同一个fz_locks_context或其底层锁官方强烈建议fz_new_context只调用一次其余 context 全部由fz_clone_context派生以保证锁机制一致虽然当前版本仍支持多次fz_new_context创建完全独立的 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 内部有一条简单规则绝不先持有编号更大的锁再去拿编号更小的锁即不会在持有锁 n 时去拿任何 i ≤ n 的锁。若定义FITZ_DEBUG_LOCKING可开启调试代码验证这一规则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可用静态库直接编译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运行前的两个警告示例源码的注释明确提醒所有页面会同时渲染请选择页数较少的文件以免过度消耗机器资源每页一个线程线程数量可能受运行环境对线程数的限制影响。换句话说这个示例是架构演示而非生产级批量工具——它用最直白的方式展示并发模型实际产品中通常需要线程池与页数配额。程序架构总览一主线程 每页一线程示例采用官方 overview 推荐的服务器模型overview.md单个主线程独占 document负责把所有页面冻结成 display list每个渲染线程各自克隆一个 context只操作共享的 display list 与自己的 pixmap。整体流程主线程初始化FZ_LOCK_MAX把互斥锁组装fz_locks_context创建主 context主线程打开文档、统计页数 N主线程对第 i 页加载页面 → 计算边界框 → 创建 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 的要求一个线程连续渲染多页同样合法。线程间通信的数据结构struct thread_datastruct thread_data { fz_context *ctx; // 主线程的 context 指针供渲染线程克隆 int pagenumber; // 页码用于打印日志 fz_display_list *list; // 主线程生成的页面绘图命令列表跨线程共享 fz_rect bbox; // 页面渲染区域 fz_pixmap *pix; // 渲染结果主线程传入NULL渲染线程填充 int failed; // 渲染是否失败1 表示失败 };这个结构是主线程与渲染线程之间唯一的通信协议。注意它的数据流方向主线程 → 渲染线程ctx、pagenumber、list、bbox以及初始为NULL的pix和初始为 0 的failed渲染线程 → 主线程填充好的pix由fz_new_pixmap_with_bbox创建与可能被置 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创建第二个参数传入locks。此后所有克隆出的 context 会自动共享同一套锁符合五大铁律第 5 条FZ_STORE_UNLIMITED表示资源存储store不设上限其值为 0如需限制缓存大小可用FZ_STORE_DEFAULT256 MiB或自定义字节数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 是整个并发的枢纽页面被录制成一组绘图命令后document 与 page 对象就可以被丢弃display list 则成为可被任意线程、甚至多个线程同时重放的数据。相关 API 定义见 include/mupdf/fitz/display-list.hfz_new_display_list(ctx, mediabox)创建空 display listfz_new_list_device(ctx, list)创建 list device把后续绘制命令写入 listfz_run_page(ctx, page, dev, ...)让页面画到 list device 上即录制绘图命令fz_close_device(ctx, dev)收尾确保所有命令都已刷入 list之后任意线程用fz_run_display_list重放这些命令。每一轮循环结束后立即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) { // 尝试执行任务禁止 return/goto/longjmp 逃出 } fz_always(ctx) { // 无论是否抛异常都会执行同样禁止 return/goto/longjmp } fz_catch(ctx) { // 仅当 try 块含其调用的函数抛出异常时执行 }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 开始之后、抛异常之前被赋值的真正局部变量其值在异常抛出过程中可能变为未定义。为规避第 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 级别的致命错误锁初始化失败、线程创建失败等这类错误没有恢复意义而渲染层面的错误则走异常宏路径通过data-failed温和地汇报给主线程体现了致命错误立即终止、业务错误结构化传播的层次。设计取舍何时该用每页一线程示例选择每页一个线程注释明确说明这只是本示例的设计决策而非 MuPDF 的约束。对照 overview.md实现者有两条基本路线单一线程作为服务器一个线程打开文档并持续生成 display list其他线程只做渲染。示例即此路线也是官方认为长期运行更高效的方式自己加锁串行化文档访问在调用文档相关 API 的外围包一层自己的互斥锁让多个线程轮流访问 document——正确但效率更低。此外display list 支持多线程同时重放同一份 listbanded rendering分条带并行渲染因此把每页一线程扩展为每页分多个条带、多个线程并行渲染是完全可行的方向。示例的局限还在于线程数等于页数页数很大时会创建过多线程受系统线程数限制且资源占用高——生产代码通常会改用固定大小的线程池加任务队列。延伸阅读单线程基线示例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构建目标与安装规则Makefile赞分享图形学图像处理【免费下载链接】mupdfmupdf mirror项目地址https://gitcode.com/gh_mirrors/mu/mupdf点击查看免费下载相关推荐MuPDF C API 多线程渲染实战用 display list 并行把 PDF 逐页渲染为 PNGMuPDF C API 多线程渲染实战用 display list 并行把 PDF 逐页渲染为 PNG 导读 本文围绕 ext/mupdf/docs/cook桌面应用文档MuPDF C 语言实战指南单页渲染、多线程批量渲染与 Story 排版引擎示例解析MuPDF C 语言实战指南单页渲染、多线程批量渲染与 Story 排版引擎示例解析 本篇指南以 MuPDF 官方文档 C 语言示例章节 https://li图形学图像处理MuPDF C Cookbook 实战解析 SumatraPDF 内置 MuPDF 的渲染、多线程与 Story API 示例MuPDF C Cookbook 实战解析 SumatraPDF 内置 MuPDF 的渲染、多线程与 Story API 示例 本指南以当前仓库 ext/mu桌面应用文档上一篇zls枚举类型完整的枚举和联合支持下一篇容器镜像加速实战public-image-mirror 让镜像拉取从 90 分钟缩到 4 分钟创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/10/6 15:54:25

同济版高等数学微分方程一章的总结和理解

这里这里总结了我的笔记 极限,导数以及多元函数这几章还是不错,微分方程这一章绕过去绕过来,总觉得不够清晰,一会儿是解法,一会儿是方程形式。 第七章介绍的都是常微分方程,也简称微分方程,对于…

2026/10/6 16:49:29

原子缓冲I/O如何解决数据库撕裂写?内核存储实践与排查指南

数据库领域有个老生常谈但又特别难缠的问题:撕裂写(torn write)。我在内核存储和数据库调优这条线上摸爬滚打这些年,见过不少朋友在半夜三点被“页面一半新一半旧”的问题叫醒。2026 年的 LSFMMBPF 峰会上,社区把目光明…

2026/10/6 16:49:29

Webpack 5 前端工程化实战:从模块化原理到打包优化踩坑指南

如果你最近几年才正式接触前端,可能已经习惯了“npm run dev 启动开发、npm run build 出包”这条流水线,但未必想过这条流水线是怎么来的。Webpack 是如今前端工程化绕不开的一个名字,我在 2016 年第一次被项目逼着学它的时候,面…

2026/10/6 16:49:28

MySQL三层B+树能存多少数据?InnoDB页结构与容量估算全解析

很多人在面试里被问过这道题:MySQL的三层B树能存多少数据?网上流传的标准答案是“两千万行左右”,但你要是接着问“为什么是两千万,而不是两百万或者两个亿”,不少人就卡壳了。说实话,这道题能答好的人不多…

2026/10/6 16:49:28

基于InsightFace与ArcFace的人脸检索系统Python实操教程

在计算机视觉领域,人脸识别与检索技术已应用于身份核验、相册分类等场景。面对非标准光照、大角度侧脸等长尾场景时,传统方法的准确率往往下降。对于普通开发者和初学者,如何快速构建高可用的人脸检索系统,是实际项目中的常见技术…

2026/10/6 16:49:28

批量创建TradingView警报:3Commas自动化交易的命令行工具

简介:面向TradingView与3Commas交易用户的批量警报添加工具,基于TypeScript语言开发,主要解决TradingView缺少批量添加警报接口、不得不为大量交易对手动维护自定义警报的痛点。工具借助浏览器自动化框架驱动内置的Chromium浏览器实例&#x…

2026/10/6 16:44:28

C#实现U盘禁用守护进程:插拔检测、强制禁用与自我保活

干这行的人多少都遇到过这种需求:公司内网机器要封U盘、机房设备只允许指定U盘读写、或者就是单纯不想让人随便往服务器上插东西。市面上现成的管控软件不是价格离谱,就是策略太死板,弄来弄去还不如自己用C#写一个。今天这篇就聊一个完整的C#…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/5 17:38:27

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 0:03:23

MR25H40CDF+STM32F031C6工业级高可靠数据存储方案

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的 PLC 控制柜里、在风电变流器的散热片背面、在矿井监测终端的金属外壳下,你经常能看到一块指甲盖大小的黑色芯片——它既不是 Flash,也不是…

2026/10/6 0:03:23

MRAM+STM32工业断电数据保全实战指南

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的PLC柜里、在野外无人值守的环境监测终端里、在高速运转的包装机控制板上,你经常能看到一块指甲盖大小的黑色芯片,旁边贴着“MR25H40CDF”丝…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑