Código fuente del análisis del grupo de subprocesos
Antes de presentarles este ERROR en detalle, déjenme hacerles una pregunta:
Esto, los viejos estereotipos, han estado en la industria por más tiempo que yo, así que tengo que Abre la boca para hablar de ello:
Una de las condiciones desencadenantes de este ERROR está oculta en esta DiscardPolicy.
Pero cuando miras el código fuente, este es un método vacío. ¿Qué ocurre?
El problema es que es un método vacío y maneja las excepciones de forma silenciosa.
No te preocupes, déjame configurarlo lentamente.
El enlace correspondiente al ERROR es así:
El título probablemente dice: Oh, viejos amigos, escúchenme. Descubrí que la política de rechazo del grupo de subprocesos, DiscardPolicy, puede hacer que el subproceso se bloquee cuando encuentra el método invokerAll.
Entonces preste atención a estos dos párrafos en la descripción del ERROR:
Estos dos párrafos revelan dos datos:
Entonces vamos a arreglar este ERROR por primera vez donde aparece. Este es el enlace:
A juzgar por el título, estos dos problemas son muy similares, ambos tienen invokerAll y block, pero las condiciones de activación son diferentes.
Una es la estrategia de descarte, la estrategia de rechazo y la otra es el método de cierre.
Entonces, mi estrategia es guiarlo primero a través del método ShutdownNow para que pueda comprender mejor los problemas causados por DiscardPolicy.
Básicamente, están diciendo lo mismo.
En esta descripción de ERROR relacionada con el apagado. Ahora, el interlocutor le dio un caso de prueba, que modifiqué ligeramente y lo usé.
El código está publicado aquí, también puedes ir a tu local:
Luego se te explicará qué está haciendo el código de prueba.
Primero, en el lugar etiquetado ①, coloque 10 tareas invocables en la lista.
¿Qué haces cuando tienes tantas tareas?
Debe haber sido arrojado al grupo de subprocesos, ¿verdad?
Por lo tanto, en el lugar marcado ②, se establece un grupo de subprocesos de 2 subprocesos y 2 subprocesos centrales. El método invokeAll del grupo de subprocesos se llama en el subproceso:
¿Qué hace este método?
Ejecutar un conjunto determinado de tareas y devolver una lista futura que contiene su estado y resultados una vez completadas todas las tareas.
Es decir, cuando se inicia el subproceso, el grupo de subprocesos ejecutará las tareas en la lista una por una y devolverá una lista futura después de la ejecución.
Cuando escribimos código, podemos usar esta lista para saber si este lote de tareas se ha completado.
Pero amigos, pero, ah, nota, en mi caso, no me importa en absoluto el valor de retorno del método invokerAll.
Lo que me preocupa es el resultado de esta frase después de ejecutar el método invokerAll:
Bien, ¿cuál es el problema con este programa ahora?
No se nota, ¿verdad?
No puedo decirlo, porque no hay ningún problema y el programa puede ejecutarse normalmente. Fin:
Luego, modifiqué el programa y agregué las siguientes líneas de código, marcadas ③:
Lo que se llama aquí es el método de cierre del grupo de subprocesos. El propósito es permitir que el programa salga después de que el grupo de subprocesos complete la tarea.
Por favor, ¿qué pasó con este programa?
Seguro que no puedes verlo, ¿verdad?
Yo tampoco, porque no hay ningún problema y el programa puede ejecutarse normalmente. Fin:
Está bien, a continuación comenzaré a transformarme nuevamente.
El programa se vuelve así:
Tenga en cuenta que aquí uso el método ShutdownNow, lo que significa que quiero cerrar inmediatamente el grupo de subprocesos frontales y luego dejar que todo el programa salga.
Entonces, ¿qué tiene de malo este programa?
De hecho, hay un problema. Es realmente feo a simple vista, pero primero podemos echar un vistazo a los resultados de ejecución:
Los resultados aún son fáciles de observar.
No hay ningún resultado "invokeAll devuelto" y el programa no sale.
Entonces la pregunta es: ¿Crees que esto es un ERROR?
No importa cuál sea el motivo, a juzgar por el fenómeno, esto es un ERROR, ¿verdad?
Llamé a Shutdown. Ahora solo quiero cerrar el grupo de subprocesos inmediatamente y dejar que todo el programa salga. Como resultado, la tarea no se ejecutó y el programa no salió. Esto no es lo que esperábamos.
Entonces, sé atrevido, ¡esto es un ERROR!
Veamos otro diagrama comparativo sobre la salida de los métodos de apagado y apagado, que es más intuitivo:
En cuanto a la diferencia entre los dos métodos, no la diré. . Si no lo sabes, búscalo en línea y memorízalo.
De todos modos, el ERROR se puede reproducir de forma estable ahora.
El siguiente paso es identificar la causa raíz.
¿Cómo encontrar la causa raíz?
Piensa en esto primero: el programa debería haber cerrado pero no lo hizo. ¿Significa esto que todavía hay subprocesos en ejecución o que hay subprocesos que no son demonios en ejecución?
Por cierto, es fácil pensar en ello aquí.
Mira la pila de hilos.
¿Qué opinas?
Cámara, amigo. Nuestro viejo amigo aparece a menudo en artículos anteriores, pero:
Puedes darte cuenta de que hay algún problema con un cable con un toque tan ligero:
Está en un estado de espera, pasando La información de la pila puede localizar claramente el código que provocó que entrara en este estado, que es la línea 244 del método invokeAll, que es esta línea de código:
Dado que el problema radica en el método invokeAll, usted Necesito entender este método ¿Para qué sirve?
El lugar marcado ① es encapsular la tarea entrante en un objeto Futuro, primero colocarla en una lista y luego llamar al método de ejecución, es decir, arrojarla al grupo de subprocesos para su ejecución.
Esta operación es particularmente como llamar directamente al método enviar() del grupo de subprocesos. Déjame hacerte una comparación:
El lugar marcado ② es la lista futura antes del ciclo. Si la ejecución del futuro no se completa, se llama al método get del futuro para bloquear la espera del resultado.
A juzgar por la información de la pila, el hilo está bloqueado en el método get futuro, lo que indica que el futuro aún no se ha ejecutado.
¿Por qué no se ejecuta?
Bien, volvamos a donde probamos el código:
10 tareas, usando 2 subprocesos principales y arrojados al grupo de subprocesos.
¿Hay dos subprocesos en el grupo de subprocesos que se pueden ejecutar y los ocho restantes ingresan a la cola?
Bien, déjame preguntarte: después de llamar a ShutdownAhora, ¿se agotan directamente los subprocesos de trabajo? ¿No hay recursos para implementar los ocho restantes?
Por otro lado, ¿incluso si solo no se ejecuta 1 tarea? ¿Future.get() en el método invokeAll también está bloqueado?
Pero amigos, pero, ah, cuando el ERROR fue tan claro, el caso anterior fue anulado por el gobierno.
¿Qué pasó?
Déjame mostrarte la respuesta oficial.
Oh, lo siento, no es el jefe, es la respuesta de los gigantes oficiales Martin y Doug:
Martin dijo: Lao Tie, leí tu código y parece que no hay nada malo. ? Escúchame, el método ShutdownNow devuelve una lista de tareas que aún no se han ejecutado. Así que hay que hacer algo con respecto a la regresión que ahora está cerrada.
Doug dijo que Martin tenía razón. Por cierto:
Se refieren a esta lista. En otras palabras, tomó esta situación en consideración cuando escribió el código, por lo que devolvió todas las tareas no ejecutadas a la persona que llamó.
El método ShutdownNow tiene un valor de retorno.
No noté este detalle antes:
Pero si miras de cerca el valor de retorno, es un Runnable en la lista. No es el futuro, por lo que no se puede llamar al método Future.cancel().
Entonces, ¿cómo cancelar la tarea después de obtener este valor de retorno?
Esta es una buena pregunta. Porque el interrogador también tenía esa pregunta:
Después de ver al gigante decir que quería operar con el valor de retorno, respondió con una mirada confusa: Tenga en cuenta que el método ShutdownNow devuelve una lista. Al menos para mí, no sé cómo cancelar estas tareas. ¿Debería describirse en la documentación?
El hermano Martin sintió que esta respuesta era realmente confusa. Respondió lo siguiente:
Hay dos formas de enviar tareas al grupo de subprocesos.
Si utiliza el método ejecutar() para enviar una tarea ejecutable, ShutdownNow devolverá una lista de tareas ejecutables que aún no se han ejecutado.
Si utiliza el método submit() para enviar una tarea ejecutable, se encapsulará como un objeto FutureTask, por lo que llamar al método ShutdownNow devolverá una lista de tareas futuras que aún no se han ejecutado:
Es decir, el conjunto de listas devuelto por el método ShutdownNow puede contener Runnable o FutureTask, dependiendo de qué método se llame cuando la tarea se lanza al grupo de subprocesos.
FutureTask es una subclase de Runnable:
Por lo tanto, según la declaración del hermano Martin y el código que proporcionó, podemos modificar el caso de prueba de la siguiente manera:
Atravesar ShutdownNow La colección de listas devuelta por el método se juzga si es el futuro. Si es así, se convierte al futuro y luego se llama a su método de cancelación.
De esta forma el programa podrá ejecutarse con normalidad y finalizar.
Desde esta perspectiva, no parece ser un ERROR y se puede evitar mediante la codificación.
Pero amigos, pero todo fue un presagio mío, y luego la trama empezó a revertirse.
Volvamos a este enlace:
Este enlace menciona una política de rechazo del grupo de subprocesos DiscardPolicy.
Siempre que modifique ligeramente nuestro programa de demostración y active la política de rechazo del hilo, el error anterior realmente será un error que no se puede evitar.
¿Cómo se debe cambiar?
Muy simple, simplemente cambie el grupo de subprocesos:
Reemplace nuestro grupo de subprocesos anterior con dos subprocesos centrales y una longitud de cola infinita con un grupo de subprocesos personalizado.
El número de subprocesos principales, el número máximo de subprocesos y la longitud de la cola de este grupo de subprocesos personalizado son 1, y la política de rechazo de subprocesos adoptada es DiscardPolicy.
Si el código no se mueve a otra parte, todo el código quedará así. Publicaré el código para que lo veas y puedas ejecutarlo directamente:
Así que primero ejecutemos el programa y veamos los resultados:
Oye, ¿qué pasó?
Obviamente manejé el valor de retorno de ShutdownNow. ¿Por qué el programa no muestra "invokeAll devuelto" y está bloqueado en el método invokeAll?
Incluso si no sabemos por qué el programa no se detuvo, desde una perspectiva de rendimiento, esto debe ser un error.
A continuación, lo llevaré a analizar por qué este fenómeno ocurre.
En primer lugar, déjame preguntarte, en nuestro ejemplo, ¿cuántas tareas puede acomodar este grupo de subprocesos como máximo?
¿Puedo aceptar sólo un máximo de 2 tareas?
Solo puedo aceptar 2 tareas como máximo.
¿Significa esto que no puedo manejar 8 tareas y necesito implementar una política de denegación del grupo de subprocesos?
Pero ¿cuál es nuestra estrategia de rechazo?
DiscardPolicy se implementa de la siguiente manera: procesamiento silencioso, descartar tareas y no generar excepciones:
Bien, entonces piénsalo, ¿qué devuelve ShuttleNow? ¿Es una tarea que aún no se ha ejecutado en el grupo de subprocesos, es decir, una tarea en la cola?
Pero hay como máximo una tarea en la cola, por lo que es inútil cancelarla por ti.
Por lo tanto, este caso no tiene nada que ver con no manejar el valor de retorno de ShutdownNow.
La clave son las ocho tareas que fueron rechazadas, o la clave es activar la política de rechazo DiscardPolicy.
El efecto de activarse una vez es el mismo que activarse varias veces. En este escenario en el que personalizamos el grupo de subprocesos y el método invokeAll, siempre que cualquier tarea se procese en silencio, esto incluso es una mala pasada.
¿Por qué dices eso?
Echemos un vistazo a la implementación de la política de rechazo del grupo de subprocesos predeterminada AbortPolicy:
Después de ser rechazada para su ejecución, se generará una excepción y luego se capturará en el método invokeAll. por lo que no se bloqueará:
Si se procesa de forma silenciosa, no hay lugar para que el Future procesado de forma silenciosa lance una excepción, y no hay ningún lugar para llamar a su método de cancelación, por lo que siempre estará bloqueado. .
Esto es un ERROR.
Entonces, ¿cómo respondió el funcionario a este ERROR?
Respuesta de Martin: Creo que debería indicarse en el documento que la estrategia de rechazo rara vez se utiliza en escenarios reales y no se recomienda para todos. De lo contrario, ¿lo considera una característica?
Creo que la implicación es: sé que esto es un ERROR, pero debes usar DiscardPolicy, una política de rechazo que no se usará en la codificación real. Creo que pusiste un error a propósito.
No estoy satisfecho con esta respuesta.
El hermano Martín no sabe algo. Durante nuestra entrevista, hubo una sesión de ensayo de ocho partes, y había una antigua pregunta de ensayo de ocho partes como esta:
Si eres lo suficientemente inteligente, cuando personalices la estrategia de rechazo del grupo de subprocesos, escribes una política de rechazo elegante, pero es equivalente a DiscardPolicy.
En otras palabras, no lo pusiste en la cola y lanzaste una excepción. No importa cuán sofisticado sea tu código, seguirás teniendo este problema.
Creo que sigue siendo un problema de diseño con el método invokeAll. No debe diseñar futuros que sean inaccesibles para subprocesos distintos del subproceso que realiza la llamada.
Esto va en contra de la filosofía de diseño de los objetos del futuro.
Entonces digo que esto es un ERROR y un problema de diseño.
¿Qué? ¿Me preguntas cómo diseñar?
Lo sentimos, no hay comentarios.