无法使Boost.Python使用matplotlib事件处理程序

时间:2019-01-04 22:32:56

标签: python c++ matplotlib boost-python

我正在用解释器(用C ++编写)开发C ++ 11程序,该解释器调用各种类方法来工作。我想添加使用具有matplotlib(后端为TkAgg)的Python 3.5创建数据图的功能。我还希望用户能够在程序中启动Python解释器或运行python脚本,以便实时对图进行详细的调整/增强。

到目前为止,我已经成功地使用Boost.Python 1.65.1作为我的界面层创建和保存了图,并使用Python的代码模块启动了Python解释器。但是,matplotlib图的事件循环不会在我的C ++解释器提示符下运行(即,如果我在图上放置另一个窗口,则该图将变为空白并且不会重绘)。只有当用户启动Python解释器时,事件循环才会起作用,并且图形会重新绘制。我希望该程序的行为就像用户发出matplotlib.pyplot.ion()命令后在本机Python解释器中创建绘图一样。我已经在用于创建绘图的Python代码中添加了一个plt.ion()调用,但这似乎对所产生的行为没有影响。

我试图通过在不同线程上执行Python绘图和C ++解释来解决此问题,但这似乎无济于事。我想知道我是否应该以不同于当前的方式来处理Python GIL,或者是否可以做一些其他事情来使matplotlib事件循环在后台线程中运行。我已经发布了一个简单的示例,重现了我遇到的核心问题。任何帮助表示赞赏。

我尝试过的另一件事是手动调用matplotlib的canvas.start_event_loop()。这似乎可行,但它阻止了我创建新图,直到它返回/超时都不理想。

以下是创建绘图的Python代码

# Import matplotlib and pyplot and turn on interactive mode
import matplotlib
import matplotlib.pyplot as plt
plt.ion()

def PlotData(num):
    """ Create a simple plot and return the figure.
    """
    data_list = [[1,2,3,4],[3,2,4,1],[4,3,2,1]]
    data = data_list[num]

    f = plt.figure()
    ax = f.gca()

    ax.set_xlabel('x label')
    ax.set_ylabel('y label')

    ax.plot([1,2,3,4],data,'bo-')

    title = 'Data Set ' + str(num+1)
    f.suptitle(title)
    print(title)
    f.show()
    return f

这是C ++代码

#include <boost/python.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/chrono.hpp>

#include <iostream>
#include <string>

PyThreadState* mainThreadState = nullptr;
PyThreadState* pts = nullptr; /*!< Pointer for the current thread state */

/*! Initialize Python */
void init()
{
    Py_Initialize();
    PyEval_InitThreads();
    mainThreadState = PyEval_SaveThread();
}

void init_thread()
{
    pts = PyThreadState_New(mainThreadState->interp);
}

/*! Calls Python to create a simple plot interactively */
void PlotData(const int& inp)
{
    using namespace boost::python;

    // Acquire the GIL
    std::cout << "Python Thread GIL State (before): " << PyGILState_Check() << std::endl;
    PyEval_RestoreThread(pts);

    object background = import("sto");
    object fig = background.attr("PlotData")(inp);

    // The code below will show the plot, but the plot won't update in real time at the C++ command line
    object plt = import("matplotlib.pyplot");
    fig.attr("show")();
    plt.attr("pause")(.1);

    std::cout << "Python Thread GIL State (during): " << PyGILState_Check() << std::endl;

    // Release the GIL
    pts = PyEval_SaveThread();
    std::cout << "Python Thread GIL State (after): " << PyGILState_Check() << std::endl;
}

int main(int argc, char* argv[])
{
    // Create a thread pool with 1 thread for all Python code
    boost::asio::io_service ioService;
    boost::thread_group threadpool;

    // Start the service
    auto work = std::make_shared<boost::asio::io_service::work>(ioService);

    // Add a single thread to the thread pool for Python operations
    threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioService));

    // Initialize Python
    init();
    std::cout << "Submitting init_thread" << std::endl;
    ioService.post(boost::bind(init_thread));

    // Create some plots on the background thread
    std::cout << "Submitting PlotData calls" << std::endl;
    ioService.post(boost::bind(PlotData,0));
    ioService.post(boost::bind(PlotData,1));

    // Pause to allow plots to update in realtime
    boost::this_thread::sleep_for(boost::chrono::seconds(4));

    // Receive inputs from command line (the plots should update during this time but they don't)
    std::string inp{"1"};
    std::cout << "Enter to quit\n";
    while (!inp.empty())
    {
        std::cout << ">> ";
        std::getline(std::cin,inp);
        std::cout << "GIL State: " << PyGILState_Check() << std::endl;
    }

    // Finalize Python
    std::cout << "Submitting Py_Finalize" << std::endl;
    ioService.post(boost::bind(PyEval_RestoreThread,pts));
    ioService.post(boost::bind(Py_Finalize));

    // Allow jobs to complete and shut down the thread pool
    work.reset();
    threadpool.join_all();
    ioService.stop();

    return 0;
}

1 个答案:

答案 0 :(得分:0)

如果有人偶然发现了这篇文章并且感兴趣,我找到了一条合适的前进方向。我放弃了Boost.Asio方法,转而使用std :: future更直接的方法。另外,我更新了代码以在后台线程上获取输入,而不是在后台运行Python。我不得不尝试超时时间来获得所需的响应能力,但是现在Python图形更新时没有引起关注,用户可以在C ++提示符下输入命令。下面的代码假定C ++提示符启动时,已在Python中打开一个图形。

这是更新的C ++代码:

#include <iostream>
#include <string>
#include <future>
#include <chrono>

#include <Python.h>
#include <boost/python.hpp>

void GetInput()
{
    std::cout << "$ ";
    std::string tline;
    std::getline(std::cin, tline);
    std::cout << "You entered: '" << tline << "'\n";
}

int main(int argc, char* argv[])
{
    using namespace boost::python;

    Py_Initialize();

    try
    {
        std::cout << "Importing python code" << std::endl;
        object background = import("sto");

        std::cout << "Calling python code" << std::endl;
        background.attr("SimpleTest")(argc);
        object fig = background.attr("PlotData")(argc);

        std::chrono::milliseconds span(100);
        for (int i = 0; i < 10; ++i)
        {
            std::cout << "Iteration " << i << std::endl;
            std::future<void> fut = std::async(GetInput);
            while (fut.wait_for(span) == std::future_status::timeout)
                background.attr("Pause")(.2);
        }
    } catch (error_already_set& e)
    {
        PyErr_Print();
    }
    Py_Finalize();

    return 0;
}

以及从sto.py导入的相应Python代码:

# Import matplotlib and pyplot and turn on interactive mode
import sys
import matplotlib
import matplotlib.pyplot as plt
plt.ion()
plt.style.use('classic')


def SimpleTest(num):
    print('Python code: you entered ' + str(num))

def PlotData(num):
    """ Create a simple plot and return the figure.
    """
    if len(sys.argv) == 0:
        sys.argv.append('sto.py')

    data_list = [[1,2,3,4],[3,2,4,1],[4,3,2,1]]
    if num < len(data_list):
        data = data_list[num]
    else:
        data = data_list[1]

    f = plt.figure()
    ax = f.gca()

    ax.set_xlabel('x label')
    ax.set_ylabel('y label')

    ax.plot([1,2,3,4],data,'bo-')

    title = 'Data Set ' + str(num+1)
    f.suptitle(title)
    print(title)
    f.show()
    plt.pause(.02)
    return f

if __name__ == '__main__':
    SimpleTest(8)
    PlotData(4)

def Pause1(timeout):
    '''This function isn't great because it brings the 
       figure into focus which is not desired'''
    plt.pause(timeout)

def Pause2(timeout):
    '''This function starts the event loop but does not 
       bring the figure into focus (which is good)'''
    fig = plt.gcf()
    fig.canvas.start_event_loop(timeout)

def Pause(timeout):
    # print('Waiting for ' + str(timeout) + ' seconds')
    Pause2(timeout)
    # print('Done waiting')