Search code examples
pythoncmultithreadingmultiprocessingpython-c-api

Can't kill multiprocessing pool in a multithreaded C application embedding Python


OS: linux
Python version: 3.6

I'm trying to extend a C application with Python runtime. The C application uses pthread and I tried to use multiprocessing forkserver in Python runtime but faced a problem. When I try to kill the program with SIGINT signal (by hitting Ctrl+C in the terminal) the worker processes get killed but the main program hangs.

Here is a toy program that produces the same problem.

#include <Python.h>
#include <pthread.h>

void * thread_start(void *unsed)
{
    PyObject *fs_mod = PyImport_AddModule("fs");
    PyObject *apply_fn = PyObject_GetAttrString(fs_mod, "apply");
    PyObject *job_fn = PyObject_GetAttrString(fs_mod, "job");
    PyObject *job_args = Py_BuildValue("()");
    PyObject_CallFunctionObjArgs(apply_fn, job_fn, job_args, NULL);
    printf("finished\n");
    return NULL;
}

int main(){
    Py_Initialize();
    PyRun_SimpleString(
        "import sys; sys.path.append('...');"
        "sys.argv=['a.out'];"  // prepare a dummy argument to avoid error in forkserver
        "import fs\n"
        "if __name__ == '__main__': fs.init()");

    while(1){
        pthread_t thread;
        pthread_create(&thread, 0, &thread_start, NULL);
        printf("joing\n");
        pthread_join(thread, 0);
    }
}
import multiprocessing as mp

pool = None


def job():
    import time
    print("running..")
    time.sleep(5)

def init():
    global pool
    mp.set_start_method('forkserver')
    pool = mp.Pool(1)

def apply(*args):
    global pool
    return pool.apply(*args)

I don't exactly know how Linux signal works. I tried to catch SIGINT signal in the main python process with signal module, but it seems that main it doesn't get to receive the signal. How would I be able to make this application die gracefully on SIGINT without hanging forever?


By reading ViKiG answer, I realized that I can first catch the KeyboardInterrupt(or SIGINT) exception in the worker processes and send some sentinel value to the main process to notify the exception and shut down the application.

After skimming through the CPython forkserver implementation, I potentially concluded that the author of the library intentionally made the main process ignore the SIGINT. I guess, currently, that the recommended way is to catch the exception in the worker processes, not in the main one.


Solution

  • It turned out that I don't have to catch the exception in the main process. I solved the problem by catching the KeyboardInterrupt(or SIGINT) exception in the worker processes and send some sentinel value to the main process to notify the exception and shut down the application.

    import multiprocessing as mp
    
    
    pool = None
    
    
    def job():
        try:
            import time
            print("running..")
            time.sleep(5)
            return True
        except KeyboardInterrupt:
            print("Exiting..")
            return False
    ...
    
    def apply(*args):
        global pool
        ret = pool.apply(*args)
        if ret:
            return pool.apply(*args)
        else:
            print("Gracefully die")