Процесс с готовой нитью никогда не завершается - PullRequest
1 голос
/ 18 января 2020

Почему поток должен сохраняться и препятствовать завершению своего процесса даже после достижения цели?

Хотя в этом вопросе используется дополнительный дочерний процесс, проблема, лежащая в основе полностью коренится в многопоточности. Поэтому эту проблему с базисом c можно воспроизвести только с помощью MainProcess. (Под редакцией @Darkonaut)

Я создал класс, который наследует multiprocessing.Process:

class Task(Process):
    def run(self) :
        print("RUN")

        t = threading.Thread(target=do_some_work)
        t.start()
        # ...
        t.join()
        print("CLOSED")

И я запускаю его следующим образом:

proc = Task()
proc.start()
proc.join()
print("JOINED")

Но он не присоединится, и результат будет таким:

>> RUN
>> CLOSED

Я не использую Queues и Pipes.

Когда я запустил это на Ubuntu, я отслеживал процесс с его pid. Процесс все еще существует даже после выполнения строки print("CLOSED") без каких-либо исключений. Я также запустил это на Windows и отслеживал процесс в диспетчере задач. Процесс завершается после print("CLOSED") и все еще не включается.

Еще один момент заключается в том, что в Ubuntu, когда все зависает после print("CLOSED"), и я нажимаю Ctrl + C, я получаю следующее:

Traceback (most recent call last):
  File "Scheduler.py", line 164, in <module>
    scheduler.start()
  File "Scheduler.py", line 152, in start
    self.enqueueTask(plan)
  File "Scheduler.py", line 134, in enqueueTask
    proc.join()
  File "/usr/local/lib/python3.8/multiprocessing/process.py", line 149, in join
    res = self._popen.wait(timeout)
  File "/usr/local/lib/python3.8/multiprocessing/popen_fork.py", line 47, in wait
    return self.poll(os.WNOHANG if timeout == 0.0 else 0)
  File "/usr/local/lib/python3.8/multiprocessing/popen_fork.py", line 27, in poll
    pid, sts = os.waitpid(self.pid, flag)

В соответствии с последней строкой, я думаю, что основной процесс чего-то ждет, но что и почему?

Кажется, проблема в потоке, не являющемся демоном, который я запускаю в run() метод Task. Создание этого потока в качестве демона решает проблему , поэтому я могу с уверенностью сказать, что этот поток не позволяет моему процессу закрыться даже после выполнения MainThread. Я все еще в замешательстве, потому что целевая функция потока, не являющегося демоном, выполнена успешно.

1 Ответ

1 голос
/ 20 января 2020

Почему поток должен сохраняться и препятствовать завершению своего процесса даже после достижения цели?

Хотя в этом вопросе используется дополнительный дочерний процесс, основная проблема полностью укоренена в многопоточности. Следовательно, эту проблему с базисом c можно воспроизвести только с помощью MainProcess. Ответ, связанный с дополнительным дочерним процессом, можно найти в edit 2 .


Сценарий

Не увидев, чем на самом деле является ваш новый поток в вашем дочернем процессе. при этом вероятный сценарий вашего наблюдаемого поведения заключается в том, что ваш thread-1 запускает еще один thread-2, о котором вы, возможно, даже не подозреваете. Возможно, он запускается из сторонней библиотеки, в которую вы звоните, или для того, чтобы оставаться внутри stdlib, multiprocessing.Queue.put() также запускает поток фидера в фоновом режиме.

Этот общий сценарий не является Process -подклассом -и не связаны с вызовом Process.close() из самого дочернего процесса (неправильное использование, но без последствий).

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}} * * * * * * * * * * * * * * * *}} в процессе всегда является последним потоком в выходе из процесса присоединение non-daemoni c потоков как часть его _shutdown() -программы. Это то, что удерживает MainThread в подвешенном состоянии, в то время как его "поверхностная" работа уже выполнена.

Проблема в потоке, не являющемся демоном, который я запускаю в run () Метод задания. поэтому я могу с уверенностью сказать, что поток не позволяет моему процессу закрыться даже после того, как его MainThread завершено. но я все еще в замешательстве, потому что целевая функция этого потока, не являющегося демоном, выполнена успешно.

Теперь в этом изображенном сценарии ваша целевая функция для thread-1 может фактически завершиться sh успешно. Однако этот thread-1 запустил еще один thread-2, который затем делает что-то очень длительное, например, навсегда блокировать в худшем случае.

В: Если сама проблема thread-1 не является проблемой, то почему не зависает, когда вы делаете thread-1 a daemon?

Это потому, что начальное значение "флага демона унаследовано от потока создания" . Таким образом, создание thread-1 a daemon делает его потомком thread-2 a daemon, если только флаг daemon для thread-2 не установлен явно. При выключении демоны не объединяются, и весь процесс «завершается, когда не осталось живых потоков, не являющихся демонами».

Обратите внимание, что до Python 3.7 , не-daemoni c Темы, созданные Process, имеют , а не . Это расходящееся поведение для потоков вне MainProcess было исправлено в bpo-18966 .


Code

Чтобы показать, что этот сценарий уже воспроизводим с более простым В приведенном ниже примере в качестве процесса используется MainProcess, который не завершается. thread-2 - это Timer -поток, который начнет и вызовет threading.Barrier(parties=1).wait() через 10 секунд. Этот .wait() вызов затем завершится sh немедленно с parties=1, или будет заблокирован навсегда с parties=2, потому что в нашей настройке не существует никакой другой стороны, вызывающей .wait() для этого Barrier. Это позволяет легко переключать поведение, которое мы хотим воспроизвести.

import threading

def blackbox(parties):
    """Dummy for starting thread we might not know about."""
    timer = threading.Timer(10, threading.Barrier(parties=parties).wait)  # Thread-2
    timer.name = "TimerThread"
    timer.start()


def t1_target(parties):  # Thread-1
    """Start another thread and exit without joining."""
    logger = get_mp_logger()
    logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
    blackbox(parties)
    logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
    logger.info("DONE")


if __name__ == '__main__':

    import logging

    parties = 1
    daemon = False
    print(f"parties={parties}, daemon={daemon}")

    logger = get_mp_logger(logging.INFO)
    logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
    t = threading.Thread(target=t1_target, args=(parties,), daemon=daemon)
    t.start()
    t.join()
    logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")    
    logger.info("DONE")

Журнал ниже предназначен для parties=1, поэтому бесконечная блокировка отсутствует, но, поскольку thread-2 не является потоком демона, MainThread присоединится к нему при выключении. Обратите внимание, что TimerThread все еще жив после выполнения t1_target. Здесь основной интерес представляет то, как MainThread требуется ~ 10 секунд до go с "DONE" до "process shutting down". Эти 10 секунд TimerThread жив.

parties=1, daemon=False
[18:04:31,977 MainThread <module>] ALIVE: ['MainThread']
[18:04:31,977 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1']
[18:04:31,978 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1', 'TimerThread']
[18:04:31,978 Thread-1 t1_target] DONE
[18:04:31,978 MainThread <module>] ALIVE: ['MainThread', 'TimerThread']
[18:04:31,978 MainThread <module>] DONE
[18:04:41,978 MainThread info] process shutting down

Process finished with exit code 0

С parties=2 он зависает навсегда на этом этапе, ...

parties=2, daemon=False
[18:05:06,010 MainThread <module>] ALIVE: ['MainThread']
[18:05:06,010 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1']
[18:05:06,011 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1', 'TimerThread']
[18:05:06,011 Thread-1 t1_target] DONE
[18:05:06,011 MainThread <module>] ALIVE: ['MainThread', 'TimerThread']
[18:05:06,011 MainThread <module>] DONE

... если вы не установите daemon=True, либо для thread-1 (thread-2 наследование), либо только для thread-2 напрямую.

parties=2, daemon=True
[18:05:35,539 MainThread <module>] ALIVE: ['MainThread']
[18:05:35,539 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1']
[18:05:35,539 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1', 'TimerThread']
[18:05:35,539 Thread-1 t1_target] DONE
[18:05:35,539 MainThread <module>] ALIVE: ['MainThread', 'TimerThread']
[18:05:35,539 MainThread <module>] DONE
[18:05:35,539 MainThread info] process shutting down

Process finished with exit code 0

Помощник

DEFAULT_MP_FORMAT = \
    '[%(asctime)s,%(msecs)03d %(threadName)s %(funcName)s]' \
    ' %(message)s'
DEFAULT_DATEFORMAT = "%H:%M:%S"  # "%Y-%m-%d %H:%M:%S"


def get_mp_logger(level=None, fmt=DEFAULT_MP_FORMAT, datefmt=DEFAULT_DATEFORMAT):
    """
    Initialize multiprocessing-logger if needed and return reference.
    """
    import multiprocessing.util as util
    import logging
    logger = util.get_logger()
    if not logger.handlers:
        logger = util.log_to_stderr(level)
    logger.handlers[0].setFormatter(logging.Formatter(fmt, datefmt))
    return logger
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...