We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Sending SIGTERM to parent process when running with --workers hangs indefinitely
### Checklist

<!-- Please make sure you check all these items before submitting your bug report. -->

- [x] The bug is reproducible against the latest release and/or `master`.
- [x] There are no similar issues or pull requests to fix it yet.

### Describe the bug

<!-- A clear and concise description of what the bug is. -->
When running uvicorn with multiple workers, sending a SIGTERM ie `kill -15 ppid` there's no graceful shutdown and the process hangs indefinitely on https://github.com/encode/uvicorn/blob/ff4af12d6902bc9d535fe2a948d1df3ffa02b0d3/uvicorn/supervisors/multiprocess.py#L57

### To reproduce

<!-- Provide a *minimal* example with steps to reproduce the bug locally.

1. Run `uvicorn app:app --workers 2 --log-level=debug`
2. Get the ppid in the logs `INFO:     Started parent process [38237]`
3. Send the SIGTERM `kill -15 38237`

NOTE: try to keep any external dependencies *at an absolute minimum* .
In other words, remove anything that doesn't make the bug go away.

-->

### Expected behavior

<!-- A clear and concise description of what you expected to happen. -->
A graceful shutdown

### Actual behavior

<!-- A clear and concise description of what actually happens. -->

### Debugging material

<!-- Any tracebacks, screenshots, etc. that can help understanding the problem.

NOTE:
- Please list tracebacks in full (don't truncate them).
- If relevant, consider turning on DEBUG or TRACE logs for additional details (see the Logging section on https://www.uvicorn.org/settings/ specifically the `log-level` flag).
- Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).
-->

### Environment

- OS / Python / Uvicorn version: just run `uvicorn --version`
`Running uvicorn 0.12.2 with CPython 3.8.6 on Linux`
- The exact command you're running uvicorn with, all flags you passed included. If you run it with gunicorn please do the same. If there is a reverse-proxy involved and you cannot reproduce without it please give the minimal config of it to reproduce.

### Additional context

<!-- Any additional information that can help understanding the problem.

Eg. linked issues, or a description of what you were trying to achieve. -->

originally discovered in https://github.com/encode/uvicorn/issues/364 and mistakenly taken for a docker issue, it's in fact a SIGTERM issue afaiu

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `uvicorn/supervisors/basereload.py`
Content:
```
1 import logging
2 import os
3 import signal
4 import threading
5 from socket import socket
6 from types import FrameType
7 from typing import Callable, Dict, List, Optional
8 
9 import click
10 
11 from uvicorn.config import Config
12 from uvicorn.subprocess import get_subprocess
13 
14 HANDLED_SIGNALS = (
15     signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.
16     signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.
17 )
18 
19 logger = logging.getLogger("uvicorn.error")
20 
21 
22 class BaseReload:
23     def __init__(
24         self,
25         config: Config,
26         target: Callable[[Optional[List[socket]]], None],
27         sockets: List[socket],
28     ) -> None:
29         self.config = config
30         self.target = target
31         self.sockets = sockets
32         self.should_exit = threading.Event()
33         self.pid = os.getpid()
34         self.reloader_name: Optional[str] = None
35 
36     def signal_handler(self, sig: signal.Signals, frame: FrameType) -> None:
37         """
38         A signal handler that is registered with the parent process.
39         """
40         self.should_exit.set()
41 
42     def run(self) -> None:
43         self.startup()
44         while not self.should_exit.wait(self.config.reload_delay):
45             if self.should_restart():
46                 self.restart()
47 
48         self.shutdown()
49 
50     def startup(self) -> None:
51         message = f"Started reloader process [{self.pid}] using {self.reloader_name}"
52         color_message = "Started reloader process [{}] using {}".format(
53             click.style(str(self.pid), fg="cyan", bold=True),
54             click.style(str(self.reloader_name), fg="cyan", bold=True),
55         )
56         logger.info(message, extra={"color_message": color_message})
57 
58         for sig in HANDLED_SIGNALS:
59             signal.signal(sig, self.signal_handler)
60 
61         self.process = get_subprocess(
62             config=self.config, target=self.target, sockets=self.sockets
63         )
64         self.process.start()
65 
66     def restart(self) -> None:
67         self.mtimes: Dict[str, float] = {}
68 
69         self.process.terminate()
70         self.process.join()
71 
72         self.process = get_subprocess(
73             config=self.config, target=self.target, sockets=self.sockets
74         )
75         self.process.start()
76 
77     def shutdown(self) -> None:
78         self.process.join()
79         message = "Stopping reloader process [{}]".format(str(self.pid))
80         color_message = "Stopping reloader process [{}]".format(
81             click.style(str(self.pid), fg="cyan", bold=True)
82         )
83         logger.info(message, extra={"color_message": color_message})
84 
85     def should_restart(self) -> bool:
86         raise NotImplementedError("Reload strategies should override should_restart()")
87
```

Path: `uvicorn/supervisors/multiprocess.py`
Content:
```
1 import logging
2 import os
3 import signal
4 import threading
5 from multiprocessing.context import SpawnProcess
6 from socket import socket
7 from types import FrameType
8 from typing import Callable, List, Optional
9 
10 import click
11 
12 from uvicorn.config import Config
13 from uvicorn.subprocess import get_subprocess
14 
15 HANDLED_SIGNALS = (
16     signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.
17     signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.
18 )
19 
20 logger = logging.getLogger("uvicorn.error")
21 
22 
23 class Multiprocess:
24     def __init__(
25         self,
26         config: Config,
27         target: Callable[[Optional[List[socket]]], None],
28         sockets: List[socket],
29     ) -> None:
30         self.config = config
31         self.target = target
32         self.sockets = sockets
33         self.processes: List[SpawnProcess] = []
34         self.should_exit = threading.Event()
35         self.pid = os.getpid()
36 
37     def signal_handler(self, sig: signal.Signals, frame: FrameType) -> None:
38         """
39         A signal handler that is registered with the parent process.
40         """
41         self.should_exit.set()
42 
43     def run(self) -> None:
44         self.startup()
45         self.should_exit.wait()
46         self.shutdown()
47 
48     def startup(self) -> None:
49         message = "Started parent process [{}]".format(str(self.pid))
50         color_message = "Started parent process [{}]".format(
51             click.style(str(self.pid), fg="cyan", bold=True)
52         )
53         logger.info(message, extra={"color_message": color_message})
54 
55         for sig in HANDLED_SIGNALS:
56             signal.signal(sig, self.signal_handler)
57 
58         for idx in range(self.config.workers):
59             process = get_subprocess(
60                 config=self.config, target=self.target, sockets=self.sockets
61             )
62             process.start()
63             self.processes.append(process)
64 
65     def shutdown(self) -> None:
66         for process in self.processes:
67             process.join()
68 
69         message = "Stopping parent process [{}]".format(str(self.pid))
70         color_message = "Stopping parent process [{}]".format(
71             click.style(str(self.pid), fg="cyan", bold=True)
72         )
73         logger.info(message, extra={"color_message": color_message})
74
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/uvicorn/supervisors/basereload.py b/uvicorn/supervisors/basereload.py
--- a/uvicorn/supervisors/basereload.py
+++ b/uvicorn/supervisors/basereload.py
@@ -75,6 +75,7 @@
         self.process.start()
 
     def shutdown(self) -> None:
+        self.process.terminate()
         self.process.join()
         message = "Stopping reloader process [{}]".format(str(self.pid))
         color_message = "Stopping reloader process [{}]".format(
diff --git a/uvicorn/supervisors/multiprocess.py b/uvicorn/supervisors/multiprocess.py
--- a/uvicorn/supervisors/multiprocess.py
+++ b/uvicorn/supervisors/multiprocess.py
@@ -64,6 +64,7 @@
 
     def shutdown(self) -> None:
         for process in self.processes:
+            process.terminate()
             process.join()
 
         message = "Stopping parent process [{}]".format(str(self.pid))

{"golden_diff": "diff --git a/uvicorn/supervisors/basereload.py b/uvicorn/supervisors/basereload.py\n--- a/uvicorn/supervisors/basereload.py\n+++ b/uvicorn/supervisors/basereload.py\n@@ -75,6 +75,7 @@\n         self.process.start()\n \n     def shutdown(self) -> None:\n+        self.process.terminate()\n         self.process.join()\n         message = \"Stopping reloader process [{}]\".format(str(self.pid))\n         color_message = \"Stopping reloader process [{}]\".format(\ndiff --git a/uvicorn/supervisors/multiprocess.py b/uvicorn/supervisors/multiprocess.py\n--- a/uvicorn/supervisors/multiprocess.py\n+++ b/uvicorn/supervisors/multiprocess.py\n@@ -64,6 +64,7 @@\n \n     def shutdown(self) -> None:\n         for process in self.processes:\n+            process.terminate()\n             process.join()\n \n         message = \"Stopping parent process [{}]\".format(str(self.pid))\n", "issue": "Sending SIGTERM to parent process when running with --workers hangs indefinitely\n### Checklist\r\n\r\n<!-- Please make sure you check all these items before submitting your bug report. -->\r\n\r\n- [x] The bug is reproducible against the latest release and/or `master`.\r\n- [x] There are no similar issues or pull requests to fix it yet.\r\n\r\n### Describe the bug\r\n\r\n<!-- A clear and concise description of what the bug is. -->\r\nWhen running uvicorn with multiple workers, sending a SIGTERM ie `kill -15 ppid` there's no graceful shutdown and the process hangs indefinitely on https://github.com/encode/uvicorn/blob/ff4af12d6902bc9d535fe2a948d1df3ffa02b0d3/uvicorn/supervisors/multiprocess.py#L57\r\n\r\n### To reproduce\r\n\r\n<!-- Provide a *minimal* example with steps to reproduce the bug locally.\r\n\r\n1. Run `uvicorn app:app --workers 2 --log-level=debug`\r\n2. Get the ppid in the logs `INFO:     Started parent process [38237]`\r\n3. Send the SIGTERM `kill -15 38237`\r\n\r\nNOTE: try to keep any external dependencies *at an absolute minimum* .\r\nIn other words, remove anything that doesn't make the bug go away.\r\n\r\n-->\r\n\r\n### Expected behavior\r\n\r\n<!-- A clear and concise description of what you expected to happen. -->\r\nA graceful shutdown\r\n\r\n### Actual behavior\r\n\r\n<!-- A clear and concise description of what actually happens. -->\r\n\r\n### Debugging material\r\n\r\n<!-- Any tracebacks, screenshots, etc. that can help understanding the problem.\r\n\r\nNOTE:\r\n- Please list tracebacks in full (don't truncate them).\r\n- If relevant, consider turning on DEBUG or TRACE logs for additional details (see the Logging section on https://www.uvicorn.org/settings/ specifically the `log-level` flag).\r\n- Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).\r\n-->\r\n\r\n### Environment\r\n\r\n- OS / Python / Uvicorn version: just run `uvicorn --version`\r\n`Running uvicorn 0.12.2 with CPython 3.8.6 on Linux`\r\n- The exact command you're running uvicorn with, all flags you passed included. If you run it with gunicorn please do the same. If there is a reverse-proxy involved and you cannot reproduce without it please give the minimal config of it to reproduce.\r\n\r\n### Additional context\r\n\r\n<!-- Any additional information that can help understanding the problem.\r\n\r\nEg. linked issues, or a description of what you were trying to achieve. -->\r\n\r\noriginally discovered in https://github.com/encode/uvicorn/issues/364 and mistakenly taken for a docker issue, it's in fact a SIGTERM issue afaiu\n", "before_files": [{"content": "import logging\nimport os\nimport signal\nimport threading\nfrom socket import socket\nfrom types import FrameType\nfrom typing import Callable, Dict, List, Optional\n\nimport click\n\nfrom uvicorn.config import Config\nfrom uvicorn.subprocess import get_subprocess\n\nHANDLED_SIGNALS = (\n    signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.\n    signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.\n)\n\nlogger = logging.getLogger(\"uvicorn.error\")\n\n\nclass BaseReload:\n    def __init__(\n        self,\n        config: Config,\n        target: Callable[[Optional[List[socket]]], None],\n        sockets: List[socket],\n    ) -> None:\n        self.config = config\n        self.target = target\n        self.sockets = sockets\n        self.should_exit = threading.Event()\n        self.pid = os.getpid()\n        self.reloader_name: Optional[str] = None\n\n    def signal_handler(self, sig: signal.Signals, frame: FrameType) -> None:\n        \"\"\"\n        A signal handler that is registered with the parent process.\n        \"\"\"\n        self.should_exit.set()\n\n    def run(self) -> None:\n        self.startup()\n        while not self.should_exit.wait(self.config.reload_delay):\n            if self.should_restart():\n                self.restart()\n\n        self.shutdown()\n\n    def startup(self) -> None:\n        message = f\"Started reloader process [{self.pid}] using {self.reloader_name}\"\n        color_message = \"Started reloader process [{}] using {}\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True),\n            click.style(str(self.reloader_name), fg=\"cyan\", bold=True),\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n\n        for sig in HANDLED_SIGNALS:\n            signal.signal(sig, self.signal_handler)\n\n        self.process = get_subprocess(\n            config=self.config, target=self.target, sockets=self.sockets\n        )\n        self.process.start()\n\n    def restart(self) -> None:\n        self.mtimes: Dict[str, float] = {}\n\n        self.process.terminate()\n        self.process.join()\n\n        self.process = get_subprocess(\n            config=self.config, target=self.target, sockets=self.sockets\n        )\n        self.process.start()\n\n    def shutdown(self) -> None:\n        self.process.join()\n        message = \"Stopping reloader process [{}]\".format(str(self.pid))\n        color_message = \"Stopping reloader process [{}]\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True)\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n\n    def should_restart(self) -> bool:\n        raise NotImplementedError(\"Reload strategies should override should_restart()\")\n", "path": "uvicorn/supervisors/basereload.py"}, {"content": "import logging\nimport os\nimport signal\nimport threading\nfrom multiprocessing.context import SpawnProcess\nfrom socket import socket\nfrom types import FrameType\nfrom typing import Callable, List, Optional\n\nimport click\n\nfrom uvicorn.config import Config\nfrom uvicorn.subprocess import get_subprocess\n\nHANDLED_SIGNALS = (\n    signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.\n    signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.\n)\n\nlogger = logging.getLogger(\"uvicorn.error\")\n\n\nclass Multiprocess:\n    def __init__(\n        self,\n        config: Config,\n        target: Callable[[Optional[List[socket]]], None],\n        sockets: List[socket],\n    ) -> None:\n        self.config = config\n        self.target = target\n        self.sockets = sockets\n        self.processes: List[SpawnProcess] = []\n        self.should_exit = threading.Event()\n        self.pid = os.getpid()\n\n    def signal_handler(self, sig: signal.Signals, frame: FrameType) -> None:\n        \"\"\"\n        A signal handler that is registered with the parent process.\n        \"\"\"\n        self.should_exit.set()\n\n    def run(self) -> None:\n        self.startup()\n        self.should_exit.wait()\n        self.shutdown()\n\n    def startup(self) -> None:\n        message = \"Started parent process [{}]\".format(str(self.pid))\n        color_message = \"Started parent process [{}]\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True)\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n\n        for sig in HANDLED_SIGNALS:\n            signal.signal(sig, self.signal_handler)\n\n        for idx in range(self.config.workers):\n            process = get_subprocess(\n                config=self.config, target=self.target, sockets=self.sockets\n            )\n            process.start()\n            self.processes.append(process)\n\n    def shutdown(self) -> None:\n        for process in self.processes:\n            process.join()\n\n        message = \"Stopping parent process [{}]\".format(str(self.pid))\n        color_message = \"Stopping parent process [{}]\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True)\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n", "path": "uvicorn/supervisors/multiprocess.py"}], "after_files": [{"content": "import logging\nimport os\nimport signal\nimport threading\nfrom socket import socket\nfrom types import FrameType\nfrom typing import Callable, Dict, List, Optional\n\nimport click\n\nfrom uvicorn.config import Config\nfrom uvicorn.subprocess import get_subprocess\n\nHANDLED_SIGNALS = (\n    signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.\n    signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.\n)\n\nlogger = logging.getLogger(\"uvicorn.error\")\n\n\nclass BaseReload:\n    def __init__(\n        self,\n        config: Config,\n        target: Callable[[Optional[List[socket]]], None],\n        sockets: List[socket],\n    ) -> None:\n        self.config = config\n        self.target = target\n        self.sockets = sockets\n        self.should_exit = threading.Event()\n        self.pid = os.getpid()\n        self.reloader_name: Optional[str] = None\n\n    def signal_handler(self, sig: signal.Signals, frame: FrameType) -> None:\n        \"\"\"\n        A signal handler that is registered with the parent process.\n        \"\"\"\n        self.should_exit.set()\n\n    def run(self) -> None:\n        self.startup()\n        while not self.should_exit.wait(self.config.reload_delay):\n            if self.should_restart():\n                self.restart()\n\n        self.shutdown()\n\n    def startup(self) -> None:\n        message = f\"Started reloader process [{self.pid}] using {self.reloader_name}\"\n        color_message = \"Started reloader process [{}] using {}\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True),\n            click.style(str(self.reloader_name), fg=\"cyan\", bold=True),\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n\n        for sig in HANDLED_SIGNALS:\n            signal.signal(sig, self.signal_handler)\n\n        self.process = get_subprocess(\n            config=self.config, target=self.target, sockets=self.sockets\n        )\n        self.process.start()\n\n    def restart(self) -> None:\n        self.mtimes: Dict[str, float] = {}\n\n        self.process.terminate()\n        self.process.join()\n\n        self.process = get_subprocess(\n            config=self.config, target=self.target, sockets=self.sockets\n        )\n        self.process.start()\n\n    def shutdown(self) -> None:\n        self.process.terminate()\n        self.process.join()\n        message = \"Stopping reloader process [{}]\".format(str(self.pid))\n        color_message = \"Stopping reloader process [{}]\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True)\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n\n    def should_restart(self) -> bool:\n        raise NotImplementedError(\"Reload strategies should override should_restart()\")\n", "path": "uvicorn/supervisors/basereload.py"}, {"content": "import logging\nimport os\nimport signal\nimport threading\nfrom multiprocessing.context import SpawnProcess\nfrom socket import socket\nfrom types import FrameType\nfrom typing import Callable, List, Optional\n\nimport click\n\nfrom uvicorn.config import Config\nfrom uvicorn.subprocess import get_subprocess\n\nHANDLED_SIGNALS = (\n    signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.\n    signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.\n)\n\nlogger = logging.getLogger(\"uvicorn.error\")\n\n\nclass Multiprocess:\n    def __init__(\n        self,\n        config: Config,\n        target: Callable[[Optional[List[socket]]], None],\n        sockets: List[socket],\n    ) -> None:\n        self.config = config\n        self.target = target\n        self.sockets = sockets\n        self.processes: List[SpawnProcess] = []\n        self.should_exit = threading.Event()\n        self.pid = os.getpid()\n\n    def signal_handler(self, sig: signal.Signals, frame: FrameType) -> None:\n        \"\"\"\n        A signal handler that is registered with the parent process.\n        \"\"\"\n        self.should_exit.set()\n\n    def run(self) -> None:\n        self.startup()\n        self.should_exit.wait()\n        self.shutdown()\n\n    def startup(self) -> None:\n        message = \"Started parent process [{}]\".format(str(self.pid))\n        color_message = \"Started parent process [{}]\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True)\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n\n        for sig in HANDLED_SIGNALS:\n            signal.signal(sig, self.signal_handler)\n\n        for idx in range(self.config.workers):\n            process = get_subprocess(\n                config=self.config, target=self.target, sockets=self.sockets\n            )\n            process.start()\n            self.processes.append(process)\n\n    def shutdown(self) -> None:\n        for process in self.processes:\n            process.terminate()\n            process.join()\n\n        message = \"Stopping parent process [{}]\".format(str(self.pid))\n        color_message = \"Stopping parent process [{}]\".format(\n            click.style(str(self.pid), fg=\"cyan\", bold=True)\n        )\n        logger.info(message, extra={\"color_message\": color_message})\n", "path": "uvicorn/supervisors/multiprocess.py"}]}