1

This question has been answered before and I'm trying to implement the second solution in the first answer outlined here (NB: the first solution does not concern me, my thread is running a server from an external library and can't be modified to check a flag)

I've tried to implement the simplest case that corresponds to my circumstances. I have a class that spawns a thread and that thread should be stopped externally (the thread never finishes naturally, as in this example). NB: _async_raise and ThreadWithExc are copy/pastes of the accepted answer to this question on SO:

import threading
import inspect
import ctypes
import time

# https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python
def _async_raise(tid, exctype):
    if not inspect.isclass(exctype):
        raise TypeError("Only types can be raised (not instances)")
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid,
                                                  ctypes.py_object(exctype))
    if res == 0:
        raise ValueError("invalid thread id")
    elif res != 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0)
        raise SystemError("PyThreadState_SetAsyncExc failed")

# https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python
class ThreadWithExc(threading.Thread):
    def _get_my_tid(self):

        if not self.isAlive():
            raise threading.ThreadError("the thread is not active")

        if hasattr(self, "_thread_id"):
            return self._thread_id

        for tid, tobj in threading._active.items():
            if tobj is self:
                self._thread_id = tid
                return tid

        raise AssertionError("could not determine the thread's id")

    def raiseExc(self, exctype):
        _async_raise( self._get_my_tid(), exctype )

def work():
    while True:
        print('work')
        time.sleep(1)

class Server:
    def __init__(self):
        self.thread = ThreadWithExc(target=work)

    def start(self):
        self.thread.start()

    def stop(self):
        _async_raise(self.thread.raiseExc(TypeError))


server = Server()
server.start()
server.stop()

This gives a ValueError: invalid thread id exception. I also tried threading.get_ident() instead of the answer's _get_my_tid(); that gives me another ID but that one is also invalid.

Juicy
  • 11,840
  • 35
  • 123
  • 212

2 Answers2

1

I think the fundamental problem you have is that you're not calling _async_raise() correctly and should replace the line:

_async_raise(self.thread.raiseExc(TypeError))

in Server.stop() with:

self.thread.raiseExc(TypeError)

If you do just that, however, you'll get an Exception in thread Thread-1: because there's no exception handler in the work() function to handle the exception that gets raised by raiseExc().

The following fixes that and uses a custom Exception subclass to make things more clear:

import threading
import inspect
import ctypes
import time

# https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python
def _async_raise(tid, exctype):
    if not inspect.isclass(exctype):
        raise TypeError("Only types can be raised (not instances)")
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid,
                                                  ctypes.py_object(exctype))
    if res == 0:
        raise ValueError("invalid thread id")
    elif res != 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0)
        raise SystemError("PyThreadState_SetAsyncExc failed")

# https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python
class ThreadWithExc(threading.Thread):
    def _get_my_tid(self):

        if not self.isAlive():
            raise threading.ThreadError("the thread is not active")

        if hasattr(self, "_thread_id"):
            return self._thread_id

        for tid, tobj in threading._active.items():
            if tobj is self:
                self._thread_id = tid
                return tid

        raise AssertionError("could not determine the thread's id")

    def raiseExc(self, exctype):
        _async_raise(self._get_my_tid(), exctype )

def work():
    try:
        while True:
            print('work')
            time.sleep(1)
    except Server.ThreadStopped:
        pass

    print('exiting work() function')

class Server:
    class ThreadStopped(Exception): pass

    def __init__(self):
        self.thread = ThreadWithExc(target=work)

    def start(self):
        self.thread.start()

    def stop(self):
#        _async_raise(self.thread.raiseExc(TypeError))
        self.thread.raiseExc(self.ThreadStopped)

server = Server()
server.start()
server.stop()

Output:

work
exiting work() function
martineau
  • 119,623
  • 25
  • 170
  • 301
  • I tried copy-pasting your example and I'm still getting `ValueError: invalid thread id` (python3.6 - macOS) – Juicy May 29 '17 at 12:31
  • I'm also getting the same error on python 3.5.2 on Ubuntu – Juicy May 29 '17 at 12:49
  • @Juicy: Sorry can't reproduce your problem and the posted code works for me on Windows 7 with Python 3.6.1. – martineau May 29 '17 at 12:50
  • 1
    @Juicy: I just read a comment on the related ActiveState recipe that says in the `_async_raise()` function to use `res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype))` to wrap tid as `ctypes.c_long`. Do same for the other call in the function, too. – martineau May 29 '17 at 13:38
  • @Juicy: Since you accepted my answer, should I assume wrapping the `tid` with `ctypes.c_long(tid‌​)` made it start working for you? I would like to know so I can up the posted code (because it still works for me that way, too). – martineau May 29 '17 at 17:07
  • It unfortunately doesn't work on Linux/macOS even with you suggested edit. – Juicy May 31 '17 at 15:39
  • @Juicy: Thanks for the response. Are you using a 32- or 64-bit version of Python? That might affect the type if arguments the `pythonapi` functions expect—so the argument may need to be wrapped in something else (like `ctypes.c_longlong`. Note I'm using the 32-bit version of 3.6.1. – martineau May 31 '17 at 15:43
0

Python 3.6:

self._thread_id = ctypes.c_long(tid)

Python 3.7:

self._thread_id = ctypes.c_ulong(tid)