¿Cuál es el uso de join () en Python threading?

197

Estaba estudiando el subproceso de pitón y me encontré join().

El autor dijo que si el hilo está en modo demonio, entonces necesito usarlo join()para que el hilo pueda terminar antes de que termine el hilo principal.

pero también lo he visto usar t.join()aunque tno eradaemon

código de ejemplo es este

import threading
import time
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='(%(threadName)-10s) %(message)s',
                    )

def daemon():
    logging.debug('Starting')
    time.sleep(2)
    logging.debug('Exiting')

d = threading.Thread(name='daemon', target=daemon)
d.setDaemon(True)

def non_daemon():
    logging.debug('Starting')
    logging.debug('Exiting')

t = threading.Thread(name='non-daemon', target=non_daemon)

d.start()
t.start()

d.join()
t.join()

No sé de qué sirve, t.join()ya que no es demonio y no puedo ver ningún cambio, incluso si lo elimino

usuario192362127
fuente
13
+1 por el título. 'Unirse' parece estar especialmente diseñado para fomentar un bajo rendimiento (creando / terminando / destruyendo hilos continuamente), bloqueos de la GUI (esperando en controladores de eventos) y fallas en el cierre de la aplicación (esperando que los hilos ininterrumpibles terminen). Nota: no solo Python, este es un antipatrón entre idiomas.
Martin James

Respuestas:

288

Un ascii-arte algo torpe para demostrar el mecanismo: join()el hilo principal presumiblemente lo llama. También podría ser llamado por otro hilo, pero complicaría innecesariamente el diagrama.

join-call debe colocarse en la pista del hilo principal, pero para expresar la relación de hilo y mantenerlo lo más simple posible, elijo colocarlo en el hilo secundario.

without join:
+---+---+------------------                     main-thread
    |   |
    |   +...........                            child-thread(short)
    +..................................         child-thread(long)

with join
+---+---+------------------***********+###      main-thread
    |   |                             |
    |   +...........join()            |         child-thread(short)
    +......................join()......         child-thread(long)

with join and daemon thread
+-+--+---+------------------***********+###     parent-thread
  |  |   |                             |
  |  |   +...........join()            |        child-thread(short)
  |  +......................join()......        child-thread(long)
  +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,     child-thread(long + daemonized)

'-' main-thread/parent-thread/main-program execution
'.' child-thread execution
'#' optional parent-thread execution after join()-blocked parent-thread could 
    continue
'*' main-thread 'sleeping' in join-method, waiting for child-thread to finish
',' daemonized thread - 'ignores' lifetime of other threads;
    terminates when main-programs exits; is normally meant for 
    join-independent tasks

Entonces, la razón por la que no ve ningún cambio es porque su hilo principal no hace nada después de su join. Se podría decir que joines (solo) relevante para el flujo de ejecución del hilo principal.

Si, por ejemplo, desea descargar simultáneamente un montón de páginas para concatenarlas en una sola página grande, puede iniciar descargas simultáneas usando hilos, pero debe esperar hasta que la última página / hilo termine antes de comenzar a ensamblar una sola página Fuera de muchos. Ahí es cuando lo usas join().

Don Question
fuente
¿Confirma que se puede unir un hilo demonizado () sin bloquear la ejecución del programa?
Aviator45003
@ Aviator45003: Sí, con el argumento de tiempo de espera como: demon_thread.join(0.0), join()es mediante el bloqueo sin tener en cuenta el atributo endemoniada por defecto. ¡Pero unirse a un hilo demonizado abre muy probablemente toda una lata de problemas! Ahora estoy considerando eliminar la join()llamada en mi pequeño diagrama para el hilo del demonio ...
Don Question
@DonQuestion Entonces, si nos ponemos en marcha, daemon=True¿no necesitamos join()si necesitamos al join()final del código?
Benyamin Jafari
@BenyaminJafari: Sí. De lo contrario, el hilo principal (= programa) saldría si solo quedara el hilo del demonio. Pero la naturaleza de un hilo de demonio (python) es que al hilo principal no le importa si esta tarea en segundo plano todavía se está ejecutando. Pensaré en cómo explicarlo en mi respuesta, para aclarar ese problema. ¡Gracias por tu comentario!
Don Question
En el primer caso, cuando main threadfinalice, ¿finalizará el programa sin dejar que child-thread(long)termine de ejecutarse ( child-thread(long)es decir, no está completamente terminado)?
skytree
65

Directo de los documentos

join ([tiempo de espera]) Espere hasta que el hilo termine. Esto bloquea el subproceso de llamada hasta que el subproceso cuyo método join () se llama termina, ya sea normalmente o mediante una excepción no controlada, o hasta que se produce el tiempo de espera opcional.

Esto significa que el hilo principal que genera ty despera a tque termine hasta que termine.

Dependiendo de la lógica que emplee su programa, es posible que desee esperar hasta que un hilo termine antes de que su hilo principal continúe.

También de los documentos:

Un hilo se puede marcar como un "hilo de demonio". La importancia de este indicador es que todo el programa Python se cierra cuando solo quedan hilos de daemon.

Un ejemplo simple, digamos que tenemos esto:

def non_daemon():
    time.sleep(5)
    print 'Test non-daemon'

t = threading.Thread(name='non-daemon', target=non_daemon)

t.start()

Que termina con:

print 'Test one'
t.join()
print 'Test two'

Esto generará:

Test one
Test non-daemon
Test two

Aquí el hilo maestro espera explícitamente ta que termine el hilo hasta que llame por printsegunda vez.

Alternativamente, si tuviéramos esto:

print 'Test one'
print 'Test two'
t.join()

Obtendremos esta salida:

Test one
Test two
Test non-daemon

Aquí hacemos nuestro trabajo en el hilo principal y luego esperamos que el thilo termine. En este caso, incluso podríamos eliminar la unión explícita t.join()y el programa esperará implícitamente a tque termine.

dmg
fuente
¿Puedes hacer un cambio en mi código para que pueda ver la diferencia de t.join()? agregando algo de sueño o algo más. en este momento puedo ver cualquier cambio en el programa, incluso si lo uso o no. pero para damemon puedo ver su salida si yo uso d.join(), que no veo cuando yo no uso d.join ()
user192362127
34

Gracias por este hilo, también me ayudó mucho.

Hoy aprendí algo sobre .join ().

Estos hilos se ejecutan en paralelo:

d.start()
t.start()
d.join()
t.join()

y estos se ejecutan secuencialmente (no es lo que quería):

d.start()
d.join()
t.start()
t.join()

En particular, estaba tratando de ser inteligente y ordenado:

class Kiki(threading.Thread):
    def __init__(self, time):
        super(Kiki, self).__init__()
        self.time = time
        self.start()
        self.join()

¡Esto funciona! Pero se ejecuta secuencialmente. Puedo poner self.start () en __ init __, pero no self.join (). Eso debe hacerse después de que se haya iniciado cada hilo.

join () es lo que hace que el hilo principal espere a que termine su hilo. De lo contrario, su hilo se ejecuta por sí solo.

Por lo tanto, una forma de pensar en join () como una "retención" en el hilo principal: deshilacha el hilo y se ejecuta secuencialmente en el hilo principal, antes de que el hilo principal pueda continuar. Asegura que su hilo esté completo antes de que el hilo principal avance. Tenga en cuenta que esto significa que está bien si su hilo ya está terminado antes de llamar a join (): el hilo principal simplemente se libera inmediatamente cuando se llama a join ().

De hecho, justo ahora se me ocurre que el hilo principal espera en d.join () hasta que el hilo d finalice antes de pasar a t.join ().

De hecho, para ser muy claro, considere este código:

import threading
import time

class Kiki(threading.Thread):
    def __init__(self, time):
        super(Kiki, self).__init__()
        self.time = time
        self.start()

    def run(self):
        print self.time, " seconds start!"
        for i in range(0,self.time):
            time.sleep(1)
            print "1 sec of ", self.time
        print self.time, " seconds finished!"


t1 = Kiki(3)
t2 = Kiki(2)
t3 = Kiki(1)
t1.join()
print "t1.join() finished"
t2.join()
print "t2.join() finished"
t3.join()
print "t3.join() finished"

Produce esta salida (tenga en cuenta cómo se enhebran las declaraciones de impresión entre sí).

$ python test_thread.py
32   seconds start! seconds start!1

 seconds start!
1 sec of  1
 1 sec of 1  seconds finished!
 21 sec of
3
1 sec of  3
1 sec of  2
2  seconds finished!
1 sec of  3
3  seconds finished!
t1.join() finished
t2.join() finished
t3.join() finished
$ 

El t1.join () está sosteniendo el hilo principal. Los tres subprocesos se completan antes de que t1.join () termine y el subproceso principal se mueve para ejecutar la impresión, luego t2.join () luego imprime y luego t3.join () luego imprime.

Correcciones de bienvenida. También soy nuevo en el enhebrado.

(Nota: en caso de que le interese, estoy escribiendo un código para un DrinkBot, y necesito enhebrar para ejecutar las bombas de ingredientes de forma simultánea en lugar de secuencial, menos tiempo para esperar cada bebida).

Kiki Jewell
fuente
Oye, también soy nuevo en el subproceso de Python y estoy confundido sobre el hilo principal. ¿El primer hilo es el hilo principal? Si no es así, ¿me guían?
Rohit Khatri
El hilo principal es el programa en sí. Cada uno de los hilos se bifurca desde allí. Luego se vuelven a unir, porque en el comando join (), el programa espera hasta que el hilo finalice antes de continuar ejecutándose.
Kiki Jewell el
15

El método join ()

bloquea el subproceso de llamada hasta que finaliza el subproceso cuyo método join () se llama.

Fuente: http://docs.python.org/2/library/threading.html

Ketouem
fuente
14
Entonces, ¿de qué sirve unir? vea la pregunta de OP, no solo parafrasee los documentos
Don Question
@DonQuestion incluso intenté agregar sleep.timer (20) en un hilo que no es de daemon sin usar t.join()y el programa todavía lo espera antes de la finalización. No veo ningún uso t.join()aquí en mi código
user192362127
Vea mi respuesta, para más explicaciones. con respecto a su temporizador sleep.timer en no demonio -> un hilo demoníaco se desacopla del tiempo de vida de su hilo padre y, por lo tanto, los hilos padre / hermano no se verán afectados por el tiempo de vida del hilo demonizado y viceversa .
Don Question
2
La terminología de 'unirse' y 'bloquear' es desconcertante. 'Bloqueado' sugiere que el proceso de llamada está 'bloqueado' para que no pueda hacer cualquier cantidad de cosas que todavía tiene que hacer, mientras que de hecho solo está bloqueado para que no finalice (regrese al sistema operativo), no más. Del mismo modo, no es tan obvio que hay un hilo principal que llama a un hilo hijo para 'unirlo' (es decir, terminar). Entonces, Don Q, gracias por la explicación.
RolfBly
5

Simple comprensión,

con join: el intérprete esperará hasta que su proceso se complete o finalice

>>> from threading import Thread
>>> import time
>>> def sam():
...   print 'started'
...   time.sleep(10)
...   print 'waiting for 10sec'
... 
>>> t = Thread(target=sam)
>>> t.start()
started

>>> t.join() # with join interpreter will wait until your process get completed or terminated
done?   # this line printed after thread execution stopped i.e after 10sec
waiting for 10sec
>>> done?

sin unirse: el intérprete no esperará hasta que finalice el proceso ,

>>> t = Thread(target=sam)
>>> t.start()
started
>>> print 'yes done' #without join interpreter wont wait until process get terminated
yes done
>>> waiting for 10sec
Mohideen bin Mohammed
fuente
1

Al realizar la join(t)función tanto para el hilo que no es el demonio como para el hilo del demonio, el hilo principal (o proceso principal) debe esperar tsegundos, luego puede ir más allá para trabajar en su propio proceso. Durante los tsegundos de espera, ambos hilos secundarios deben hacer lo que pueden hacer, como imprimir un texto. Después de los tsegundos, si el hilo que no es de daemon todavía no terminó su trabajo, y todavía puede terminarlo después de que el proceso principal termine su trabajo, pero para el hilo de daemon, simplemente perdió su ventana de oportunidad. Sin embargo, eventualmente morirá después de que el programa python salga. Por favor corrígeme si hay algo mal.

usuario1342336
fuente
1

En python 3.x join () se usa para unir un hilo con el hilo principal, es decir, cuando join () se usa para un hilo particular, el hilo principal dejará de ejecutarse hasta que se complete la ejecución del hilo unido.

#1 - Without Join():
import threading
import time
def loiter():
    print('You are loitering!')
    time.sleep(5)
    print('You are not loitering anymore!')

t1 = threading.Thread(target = loiter)
t1.start()
print('Hey, I do not want to loiter!')
'''
Output without join()--> 
You are loitering!
Hey, I do not want to loiter!
You are not loitering anymore! #After 5 seconds --> This statement will be printed

'''
#2 - With Join():
import threading
import time
def loiter():
    print('You are loitering!')
    time.sleep(5)
    print('You are not loitering anymore!')

t1 = threading.Thread(target = loiter)
t1.start()
t1.join()
print('Hey, I do not want to loiter!')

'''
Output with join() -->
You are loitering!
You are not loitering anymore! #After 5 seconds --> This statement will be printed
Hey, I do not want to loiter! 

'''
Shishir Nanoty
fuente
0

Este ejemplo demuestra la .join()acción:

import threading
import time

def threaded_worker():
    for r in range(10):
        print('Other: ', r)
        time.sleep(2)

thread_ = threading.Timer(1, threaded_worker)
thread_.daemon = True  # If the main thread kills, this thread will be killed too. 
thread_.start()

flag = True

for i in range(10):
    print('Main: ', i)
    time.sleep(2)
    if flag and i > 4:
        print(
            '''
            Threaded_worker() joined to the main thread. 
            Now we have a sequential behavior instead of concurrency.
            ''')
        thread_.join()
        flag = False

Fuera:

Main:  0
Other:  0
Main:  1
Other:  1
Main:  2
Other:  2
Main:  3
Other:  3
Main:  4
Other:  4
Main:  5
Other:  5

            Threaded_worker() joined to the main thread. 
            Now we have a sequential behavior instead of concurrency.

Other:  6
Other:  7
Other:  8
Other:  9
Main:  6
Main:  7
Main:  8
Main:  9
Benyamin Jafari
fuente
0

Hay algunas razones para que el hilo principal (o cualquier otro hilo) se una a otros hilos

  1. Un hilo puede haber creado o retenido (bloqueado) algunos recursos. El hilo de llamada conjunta puede borrar los recursos en su nombre

  2. join () es una llamada de bloqueo natural para que el hilo de llamada de unión continúe después de que el hilo llamado haya terminado.

Si un programa de Python no se une a otros subprocesos, el intérprete de Python aún se unirá a subprocesos que no sean demonios en su nombre.

yoonghm
fuente
-2

"¿De qué sirve usar join ()?" tu dices. Realmente, es la misma respuesta que "¿de qué sirve cerrar archivos, ya que Python y el sistema operativo cerrarán mi archivo cuando salga mi programa?".

Es simplemente una cuestión de buena programación. Debe unir () sus hilos en el punto del código que el hilo no debe debería ejecutarse, ya sea porque debe asegurarse de que el hilo no se esté ejecutando para interferir con su propio código, o que desea comportarse correctamente en un sistema más grande

Puede decir "No quiero que mi código demore en dar una respuesta" solo por el tiempo adicional que puede requerir la unión (). Esto puede ser perfectamente válido en algunos escenarios, pero ahora debe tener en cuenta que su código está "dejando un rumbo para que Python y el sistema operativo se limpien". Si hace esto por razones de rendimiento, le recomiendo que documente ese comportamiento. Esto es especialmente cierto si está creando una biblioteca / paquete que se espera que otros utilicen.

No hay ninguna razón para no unirse (), aparte de las razones de rendimiento, y diría que su código no necesita funcionar tan bien.

Chris Cogdon
fuente
66
Lo que dices sobre la limpieza de hilos no es cierto. Eche un vistazo al código fuente de threading.Thread.join (). Todo lo que hace esta función es esperar en un candado y luego regresar. Nada se limpia realmente.
Collin
1
@Collin: el subproceso en sí puede contener recursos, en ese caso, el intérprete y el sistema operativo necesitarán limpiar "cruft".
qneill
1
Nuevamente, mire el código fuente de threading.Thread.join (). No hay nada allí que desencadene la recolección de recursos.
Collin
No es necesariamente (y como usted dice, para nada) el módulo de subprocesos que contiene recursos, sino el subproceso en sí. El uso de join () significa que está esperando que el hilo termine de hacer lo que quería hacer, lo que podría incluir la asignación y liberación de recursos.
Chris Cogdon
2
Si espera o no no afecta cuando se liberan los recursos que posee el hilo. No estoy seguro de por qué estás vinculando esto con las llamadas join().
Collin