如何在Julia多进程环境中创建子图并汇总结果?
问题背景
之前给每个进程单独创建图,但图数量过多不便对比,希望将它们排列为子图。
首次尝试与问题
尝试通过pmap让每个进程绘制一个子图,代码如下:
using Distributed @everywhere using PyPlot fig, ax = plt.subplots(1, 4) pmap(i-> begin ax[i].scatter([i, 2], [3, 4]) end, 1:4) fig.savefig("test.png", transparent=false)
结果生成空的子图,原因是ax对象会被复制到每个进程,进程间无法共享该实例,修改的都是各自的副本而非主进程的子图对象。
尝试SharedArray共享的错误
试图用SharedArrays共享ax:
using SharedArrays convert(SharedArray, ax)
触发错误:
ArgumentError: type of SharedArray elements must be bits types, got PyCall.PyObject
目前已采用的可行方案是:将绘图操作移出进程,先在进程中收集数据,再统一绘图。但好奇是否存在方法让每个进程创建图/子图后汇总到一起。
解答
为何直接共享Axes对象不可行
PyPlot的Axes是PyCall封装的Python对象,属于非bits类型,而SharedArray仅支持存储bits类型(可直接序列化到内存的简单类型),因此无法用SharedArray共享这类对象。此外,PyPlot的绘图上下文是进程本地的,跨进程操作会因Python的GIL(全局解释器锁)和进程状态隔离失效,这也是首次尝试生成空图的核心原因。
进程创建子图后汇总的可行思路
如果一定要实现进程绘制子图再汇总,有两种方向:
1. 进程生成图像数据,主进程拼接
每个进程单独创建绘图、完成绘制后,将图像的像素数据传回主进程,再由主进程拼接成子图布局。示例代码:
using Distributed, PyPlot, PyCall @everywhere using PyPlot, PyCall @everywhere function plot_subplot(i) fig, ax = plt.subplots() ax.scatter([i, 2], [3, 4]) # 获取图像像素数组 fig.canvas.draw() img_data = pycall(fig.canvas.tostring_rgb, PyCall.PyObject) img_arr = reshape(PyCall.Array(img_data), fig.canvas.get_width_height()..., 3) plt.close(fig) # 释放进程内的绘图资源 return img_arr end # 收集所有进程的图像数据 subplot_imgs = pmap(plot_subplot, 1:4) # 主进程拼接成子图 fig, axs = plt.subplots(1, 4, figsize=(12,3)) for (idx, img) in enumerate(subplot_imgs) axs[idx].imshow(img) axs[idx].axis("off") # 隐藏原坐标轴(图像已包含) end fig.savefig("combined_subplots.png")
缺点:生成的是像素图而非矢量图,放大后会模糊,且子图的坐标轴样式难以统一。
2. 使用纯Julia绘图库(如Makie)
Makie这类纯Julia绘图库的对象是原生Julia类型,支持跨进程传递绘图元素的描述信息(而非直接传递绘图对象),主进程可接收后组装成子图布局。但需要熟悉Makie的布局系统,与PyPlot的使用思路差异较大。
更推荐的方案:先收数据再绘图
你目前采用的「进程收集数据,主进程统一绘图」是最稳妥的方案,既能保证绘图样式(坐标轴、图例等)的一致性,又能避免跨进程共享绘图对象的各种问题。示例代码:
using Distributed, PyPlot @everywhere function generate_plot_data(i) # 模拟进程内生成绘图数据 x = [i, 2] y = [3, 4] return (x, y) end # 从所有进程收集数据 data_collection = pmap(generate_plot_data, 1:4) # 主进程统一绘制子图 fig, axs = plt.subplots(1, 4, figsize=(12,3)) for (idx, (x, y)) in enumerate(data_collection) axs[idx].scatter(x, y) axs[idx].set_title("Subplot $idx") end fig.tight_layout() fig.savefig("unified_subplots.png")
内容的提问来源于stack exchange,提问作者Xiaoyu Liu

