Как вызвать функцию в запущенном потоке Python

Скажем, у меня есть этот класс, который порождает поток:

import threading
class SomeClass(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        while True:
            pass

    def doSomething(self):
        pass

    def doSomethingElse(self):
        pass

Я хочу

someClass = SomeClass()
someClass.start()
someClass.doSomething()
someClass.doSomethingElse()
someClass.doSomething()

Как я могу это сделать? Я знаю, что могу вызвать функцию внутри функции run(), но это не то, к чему я стремлюсь.

Ответы

Ответ 1

Вы не можете напрямую делать то, что хотите. В фоновом потоке запущена функция run, которая просто петли навсегда, поэтому она не может ничего сделать.

Вы можете, конечно, вызвать методы класса в своем потоке, но это, вероятно, не то, что вы хотите здесь.


Рамочные схемы, такие как Qt,.NET или Cocoa, могут предложить методы runOnOtherThread -type, так это то, что каждый поток запускает "цикл событий", поэтому все, что они действительно делают, это размещение события. Вы можете сделать это самостоятельно, если переписать метод run в цикл событий. Например:

import queue
import threading

class SomeClass(threading.Thread):
    def __init__(self, q, loop_time = 1.0/60):
        self.q = q
        self.timeout = loop_time
        super(SomeClass, self).__init__()

    def onThread(self, function, *args, **kwargs):
        self.q.put((function, args, kwargs))

    def run(self):
        while True:
            try:
                function, args, kwargs = self.q.get(timeout=self.timeout)
                function(*args, **kwargs)
            except queue.Empty:
                self.idle()

    def idle(self):
        # put the code you would have put in the `run` loop here 

    def doSomething(self):
        pass

    def doSomethingElse(self):
        pass

Теперь вы можете сделать это:

someClass = SomeClass()
someClass.start()
someClass.onThread(someClass.doSomething)
someClass.onThread(someClass.doSomethingElse)
someClass.onThread(someClass.doSomething)

Если вы хотите немного упростить интерфейс вызова, за счет большего количества кода в классе, вы можете добавить методы-обертки следующим образом:

    def _doSomething(self):
        # put the real code here
    def doSomething(self):
        self.onThread(self._doSomething)

Однако, если ваш метод idle не работает, вы на самом деле просто строите эквивалент однопоточного пула потоков здесь, и есть гораздо более простые способы сделать это, чем создавать его с нуля. Например, используя модуль futures от PyPI (backport модуля Python 3 concurrent.futures):

import futures

class SomeClass(object):
    def doSomething(self):
        pass
    def doSomethingElse(self):
        pass

someClass = SomeClass()
with futures.ThreadPoolExecutor(1) as executor:
    executor.submit(someClass.doSomething)
    executor.submit(someClass.doSomethingElse)
    executor.submit(someClass.doSomething)

Или, только с помощью stdlib:

from multiprocessing import dummy as multithreading

class SomeClass(object):
    def doSomething(self):
        pass
    def doSomethingElse(self):
        pass

someClass = SomeClass()
pool = multithreading.Pool(1)
pool.apply(someClass.doSomething)
pool.apply(someClass.doSomethingElse)
pool.apply(someClass.doSomething)
pool.close()
pool.join()

У пулов есть и другие преимущества, а исполнители - еще больше. Например, что, если методы возвращают значения, и вы хотите запустить две функции, затем дождаться результатов, а затем начать третью с результатами первых двух? Легко:

with futures.ThreadPoolExecutor(1) as executor:
    f1 = executor.submit(someClass.doSomething)
    f2 = executor.submit(someClass.doSomethingElse)
    futures.wait((f1, f2))
    f3 = executor.submit(someClass.doSomethingElser, f1.result(), f2.result())
    result = f3.result()

Даже если вы позже переключитесь на пул из 4 потоков, так что f1 и f2 могут быть ожидающими одновременно, а f2 может даже вернуться первым, вы гарантированно начнете с doSomethingElser, как только оба из них закончены, и не раньше.


Здесь есть еще одна возможность. Вам действительно нужен код для запуска в этом потоке, или вам просто нужно изменить переменные, от которых зависит поток? Если это последнее, просто синхронизируйте доступ к переменным. Например:

class SomeClass(threading.Thread):
    def __init__(self):
        self.things_lock = threading.Lock()
        self.things = []
        while True:
            with self.lock:
                things = self.things[:]
            for thing in things:
                # pass
    def doSomething(self):
        with self.lock:
            self.things.append(0)

someClass = SomeClass()
someClass.start()
someClass.doSomething()

Там нет ничего волшебного в том, чтобы быть на главной теме здесь. Если, кроме необходимости изменять переменные, от которых зависит SomeClass, вы также хотели просто ударить doSomething с основного потока, чтобы вы могли делать больше важных вещей, чем просто ждать, пока он закончится, вы можете создать короткий -lested extra thread только до doSomething:

someClass = SomeClass()
someClass.start()
somethingThread = threading.Thread(target=someClass.doSomething)
somethingThread.start()
doOtherImportantStuffWithSomethingIsHappening()
somethingThread.join()

Ответ 2

Посмотрите concurrent.futures и его метод submit, который делает то, что вы хотите, когда ограничиваете пул потоков одним рабочим.