PyThreadState_SetAsyncExc has no effect
Asked Answered
D

1

6

I'm using multiple interpreters with multiple threads, and I'm trying to use PyThreadState_SetAsyncExc to stop a script, but it has no effect:

void stop()
{
    PyThreadState *ts = PyThreadState_Swap(this->tstate);
    PyObject *exc = PyObject_CallObject(PyExc_Exception, Py_BuildValue("(s)", "stopped"));
    int n = PyThreadState_SetAsyncExc(this->tstate->thread_id, exc);
    if(n < 1)
        std::cerr << "Script::stop: thread not found!" << std::endl;
    PyThreadState_Swap(ts);
}

The returned value is 1, but the thread continues running. Why?

Complete testcase:

#include <Python.h>
#include <iostream>
#include <thread>

using namespace std::chrono_literals;

struct Script
{
    Script()
    {
        PyThreadState *ts = PyThreadState_Get();
        this->tstate = Py_NewInterpreter();
        PyThreadState_Swap(ts);
    }

    virtual ~Script()
    {
        if(!this->tstate) return;
        PyThreadState *ts = PyThreadState_Swap(this->tstate);
        Py_EndInterpreter(this->tstate);
        PyThreadState_Swap(ts);
    }

    void run(const char *code)
    {
        PyThreadState *ts = PyThreadState_Swap(this->tstate);
        PyRun_SimpleStringFlags(code, nullptr);
        PyThreadState_Swap(ts);
    }

    void run_thread(const char *code)
    {
        thread = std::thread{std::bind(&Script::thread_func, this, code)};
    }

    void join()
    {
        thread.join();
    }

    void stop()
    {
        PyThreadState *ts = PyThreadState_Swap(this->tstate);
        PyObject *exc = PyObject_CallObject(PyExc_Exception, Py_BuildValue("(s)", "stopped"));
        int n = PyThreadState_SetAsyncExc(this->tstate->thread_id, exc);
        if(n < 1)
            std::cerr << "Script::stop: thread not found!" << std::endl;
        PyThreadState_Swap(ts);
    }

private:
    void thread_func(const char *code)
    {
        PyThreadState *ts = PyThreadState_New(this->tstate->interp);
        PyEval_RestoreThread(ts);
        PyThreadState *ts0 = PyThreadState_Swap(ts);
        PyRun_SimpleStringFlags(code, nullptr);
        PyThreadState_Swap(ts0);
        PyThreadState_Clear(ts);
        PyThreadState_DeleteCurrent();
    }

    PyThreadState *tstate;
    std::thread thread;
};

int main(int argc, char *argv[])
{
    Py_InitializeEx(1);
    PyEval_InitThreads(); // implicit since python 3.7

    Script s1, s2;
    s1.run_thread(
R"PY(
import time
import sys
i = 0
while 1:
    i += 1
    time.sleep(1)
    sys.stdout.write('s1: %d\n' % i); sys.stdout.flush()
)PY");

    PyThreadState *ts = PyEval_SaveThread();
    std::this_thread::sleep_for(4s);
    std::cout << "trying to stop s1..." << std::endl;
    s1.stop();
    s1.join();
    PyEval_RestoreThread(ts);

    PyMem_RawFree(program);

    if (Py_FinalizeEx() < 0)
        return 120;
    return 0;
}
Davedaveda answered 9/12, 2019 at 13:11 Comment(1)
Hi. Did you ever fix this? For me ` res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit)) ` works for quitting a thread on windows, but not on linuxHauptmann
R
1

It works for me in Linux now: e.g: if Threaded to run Flask Web Server in background around other python code

import threading
import ctypes
webserverurl  = 127.0.0.1
webserverport = 8080

class thread_with_exception(threading.Thread):
    def __init__(self, name):
        threading.Thread.__init__(self)
        self.name = name
        self.interval = 1
        self.daemon = True

    def __del__(self):
        print(f"Thread terminated");

    def run(self):
        # target function of the thread class
        try:
            while True:
                print(f"{self.name} Waiting for Web")
                app.run(host=webserverurl, port=webserverport)
        finally:
            print('ended')

    def get_id(self):
        # returns id of the respective thread
        if hasattr(self, '_thread_id'):
            return self._thread_id
        for id, thread in threading._active.items():
            if thread is self:
                return id

    def raise_exception(self):
        thread_id = self.get_id()
        res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
        print(f"{self.name} terminated")
        if res > 1:
            ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), 0)
            print('Exception raise failure')

t1 = thread_with_exception('Web Server 1')
t1.start()
time.sleep(10)
t1.raise_exception()
# del t1

credit to Andrew Abrahamowicz here: https://code.activestate.com/recipes/496960-thread2-killable-threads

ctypes.c_long( required for Linux else Windows only: https://mcmap.net/q/1917580/-how-to-terminate-a-thread-in-python

Riplex answered 31/10, 2022 at 22:7 Comment(1)
Thanks for this. Bizarre how this wouldn't throw an error to begin with. Made it very difficult to track down.Slang

© 2022 - 2024 — McMap. All rights reserved.