如何在 Python 中停止循环线程?

告诉循环线程停止循环的正确方法是什么?

我有一个相当简单的程序,它在一个单独的 threading.Thread类中 ping 指定的主机。在这个类中,它休眠60秒,然后再次运行,直到应用程序退出。

我想实现一个“停止”按钮在我的 wx.Frame要求循环线程停止。它不需要立即结束线程,它可以在唤醒后停止循环。

下面是我的 threading类(注意: 我还没有实现循环,但它可能属于 PingAsset 中的 run 方法)

class PingAssets(threading.Thread):
def __init__(self, threadNum, asset, window):
threading.Thread.__init__(self)
self.threadNum = threadNum
self.window = window
self.asset = asset


def run(self):
config = controller.getConfig()
fmt = config['timefmt']
start_time = datetime.now().strftime(fmt)
try:
if onlinecheck.check_status(self.asset):
status = "online"
else:
status = "offline"
except socket.gaierror:
status = "an invalid asset tag."
msg =("{}: {} is {}.   \n".format(start_time, self.asset, status))
wx.CallAfter(self.window.Logger, msg)

在 wxPyhton Frame 中,我有一个从 Start 按钮调用的函数:

def CheckAsset(self, asset):
self.count += 1
thread = PingAssets(self.count, asset, self)
self.threads.append(thread)
thread.start()
340171 次浏览

This has been asked before on Stack. See the following links:

Basically you just need to set up the thread with a stop function that sets a sentinel value that the thread will check. In your case, you'll have the something in your loop check the sentinel value to see if it's changed and if it has, the loop can break and the thread can die.

I read the other questions on Stack but I was still a little confused on communicating across classes. Here is how I approached it:

I use a list to hold all my threads in the __init__ method of my wxFrame class: self.threads = []

As recommended in How to stop a looping thread in Python? I use a signal in my thread class which is set to True when initializing the threading class.

class PingAssets(threading.Thread):
def __init__(self, threadNum, asset, window):
threading.Thread.__init__(self)
self.threadNum = threadNum
self.window = window
self.asset = asset
self.signal = True


def run(self):
while self.signal:
do_stuff()
sleep()

and I can stop these threads by iterating over my threads:

def OnStop(self, e):
for t in self.threads:
t.signal = False

Threaded stoppable function

Instead of subclassing threading.Thread, one can modify the function to allow stopping by a flag.

We need an object, accessible to running function, to which we set the flag to stop running.

We can use threading.currentThread() object.

import threading
import time




def doit(arg):
t = threading.currentThread()
while getattr(t, "do_run", True):
print ("working on %s" % arg)
time.sleep(1)
print("Stopping as you wish.")




def main():
t = threading.Thread(target=doit, args=("task",))
t.start()
time.sleep(5)
t.do_run = False
    



if __name__ == "__main__":
main()

The trick is, that the running thread can have attached additional properties. The solution builds on assumptions:

  • the thread has a property "do_run" with default value True
  • driving parent process can assign to started thread the property "do_run" to False.

Running the code, we get following output:

$ python stopthread.py
working on task
working on task
working on task
working on task
working on task
Stopping as you wish.

Pill to kill - using Event

Other alternative is to use threading.Event as function argument. It is by default False, but external process can "set it" (to True) and function can learn about it using wait(timeout) function.

We can wait with zero timeout, but we can also use it as the sleeping timer (used below).

def doit(stop_event, arg):
while not stop_event.wait(1):
print ("working on %s" % arg)
print("Stopping as you wish.")




def main():
pill2kill = threading.Event()
t = threading.Thread(target=doit, args=(pill2kill, "task"))
t.start()
time.sleep(5)
pill2kill.set()
t.join()

Edit: I tried this in Python 3.6. stop_event.wait() blocks the event (and so the while loop) until release. It does not return a boolean value. Using stop_event.is_set() works instead.

Stopping multiple threads with one pill

Advantage of pill to kill is better seen, if we have to stop multiple threads at once, as one pill will work for all.

The doit will not change at all, only the main handles the threads a bit differently.

def main():
pill2kill = threading.Event()
tasks = ["task ONE", "task TWO", "task THREE"]


def thread_gen(pill2kill, tasks):
for task in tasks:
t = threading.Thread(target=doit, args=(pill2kill, task))
yield t


threads = list(thread_gen(pill2kill, tasks))
for thread in threads:
thread.start()
time.sleep(5)
pill2kill.set()
for thread in threads:
thread.join()

I had a different approach. I've sub-classed a Thread class and in the constructor I've created an Event object. Then I've written custom join() method, which first sets this event and then calls a parent's version of itself.

Here is my class, I'm using for serial port communication in wxPython app:

import wx, threading, serial, Events, Queue


class PumpThread(threading.Thread):


def __init__ (self, port, queue, parent):
super(PumpThread, self).__init__()
self.port = port
self.queue = queue
self.parent = parent


self.serial = serial.Serial()
self.serial.port = self.port
self.serial.timeout = 0.5
self.serial.baudrate = 9600
self.serial.parity = 'N'


self.stopRequest = threading.Event()


def run (self):
try:
self.serial.open()
except Exception, ex:
print ("[ERROR]\tUnable to open port {}".format(self.port))
print ("[ERROR]\t{}\n\n{}".format(ex.message, ex.traceback))
self.stopRequest.set()
else:
print ("[INFO]\tListening port {}".format(self.port))
self.serial.write("FLOW?\r")


while not self.stopRequest.isSet():
msg = ''
if not self.queue.empty():
try:
command = self.queue.get()
self.serial.write(command)
except Queue.Empty:
continue


while self.serial.inWaiting():
char = self.serial.read(1)
if '\r' in char and len(msg) > 1:
char = ''
#~ print('[DATA]\t{}'.format(msg))
event = Events.PumpDataEvent(Events.SERIALRX, wx.ID_ANY, msg)
wx.PostEvent(self.parent, event)
msg = ''
break
msg += char
self.serial.close()


def join (self, timeout=None):
self.stopRequest.set()
super(PumpThread, self).join(timeout)


def SetPort (self, serial):
self.serial = serial


def Write (self, msg):
if self.serial.is_open:
self.queue.put(msg)
else:
print("[ERROR]\tPort {} is not open!".format(self.port))


def Stop(self):
if self.isAlive():
self.join()

The Queue is used for sending messages to the port and main loop takes responses back. I've used no serial.readline() method, because of different end-line char, and I have found the usage of io classes to be too much fuss.

Depends on what you run in that thread. If that's your code, then you can implement a stop condition (see other answers).

However, if what you want is to run someone else's code, then you should fork and start a process. Like this:

import multiprocessing
proc = multiprocessing.Process(target=your_proc_function, args=())
proc.start()

now, whenever you want to stop that process, send it a SIGTERM like this:

proc.terminate()
proc.join()

And it's not slow: fractions of a second. Enjoy :)

My solution is:

import threading, time


def a():
t = threading.currentThread()
while getattr(t, "do_run", True):
print('Do something')
time.sleep(1)


def getThreadByName(name):
threads = threading.enumerate() #Threads list
for thread in threads:
if thread.name == name:
return thread


threading.Thread(target=a, name='228').start() #Init thread
t = getThreadByName('228') #Get thread by name
time.sleep(5)
t.do_run = False #Signal to stop thread
t.join()

I find it useful to have a class, derived from threading.Thread, to encapsulate my thread functionality. You simply provide your own main loop in an overridden version of run() in this class. Calling start() arranges for the object’s run() method to be invoked in a separate thread.

Inside the main loop, periodically check whether a threading.Event has been set. Such an event is thread-safe.

Inside this class, you have your own join() method that sets the stop event object before calling the join() method of the base class. It can optionally take a time value to pass to the base class's join() method to ensure your thread is terminated in a short amount of time.

import threading
import time


class MyThread(threading.Thread):
def __init__(self, sleep_time=0.1):
self._stop_event = threading.Event()
self._sleep_time = sleep_time
"""call base class constructor"""
super().__init__()


def run(self):
"""main control loop"""
while not self._stop_event.isSet():
#do work
print("hi")
self._stop_event.wait(self._sleep_time)


def join(self, timeout=None):
"""set stop event and join within a given time period"""
self._stop_event.set()
super().join(timeout)




if __name__ == "__main__":
t = MyThread()
t.start()


time.sleep(5)


t.join(1) #wait 1s max

Having a small sleep inside the main loop before checking the threading.Event is less CPU intensive than looping continuously. You can have a default sleep time (e.g. 0.1s), but you can also pass the value in the constructor.

Sometimes you don't have control over the running target. In those cases you can use signal.pthread_kill to send a stop signal.

from signal import pthread_kill, SIGTSTP
from threading import Thread
from itertools import count
from time import sleep


def target():
for num in count():
print(num)
sleep(1)


thread = Thread(target=target)
thread.start()
sleep(5)
pthread_kill(thread.ident, SIGTSTP)

result

0
1
2
3
4


[14]+  Stopped