"signal" --- 設定非同步事件的處理函式
*************************************

**原始碼：**Lib/signal.py

======================================================================

本模組提供於 Python 中使用訊號處理程式的機制。


一般規則
========

"signal.signal()" 函式允許定義自訂的處理程式，會在收到訊號時執行。我們
安裝了少數的預設處理程式："SIGPIPE" 會被忽略 (所以管道和 socket 上的寫
入錯誤可以當作一般的 Python 例外報告)，而 "SIGINT"（如果父行程沒有改變
它的話）會被轉換成 "KeyboardInterrupt" 例外。

特定訊號的處理程式一旦被設定，就會一直被安裝，直到被明確地重設為止 (不
管底層的實作為何，Python 皆模擬出 BSD 風格的介面)，但 "SIGCHLD" 的處理
程式除外，它會跟隨底層的實作。

在 WebAssembly 平台上，訊號是模擬出來的，故行為不同。有幾個函式和訊號
在這些平台上是不可用的。


Python 訊號處理程式的執行
-------------------------

Python 訊號處理程式不會在低階（C 語言）訊號處理程式中執行。相反地，低
階訊號處理程式會設定一個旗標，告訴*虛擬機*在稍後執行相對應的 Python 訊
號處理程式（例如在下一個 *bytecode* 指令）。這會有一些後果：

* 捕捉像 "SIGFPE" 或 "SIGSEGV" 這類由 C 程式碼中無效操作所引起的同步錯
  誤是沒有意義的。Python 將從訊號處理程式中回傳到 C 程式碼，而 C 程式
  碼很可能再次引發相同的訊號，導致 Python 明顯假當機 (hang)。從 Python
  3.3 開始，你可以使用 "faulthandler" 模組來報告同步錯誤。

* 純粹以 C 實作的長時間計算（例如在大量文字上的正規表示式比對）可能會
  不間斷地運行任意長度的時間而不考慮收到的任何訊號。當計算完成時，
  Python 訊號處理程式會被呼叫。

* 如果處理程式引發例外，就會在主執行緒中「憑空」產生例外。請參閱下面的
  說明。


訊號和執行緒
------------

Python 訊號處理程式總是在主直譯器的主 Python 執行緒中執行，即使訊號是
在另一個執行緒中接收到的。這意味著訊號不能用來做為執行緒間通訊的方式。
你可以使用 "threading" 模組的同步原語 (synchronization primitive) 來代
替。

此外，只有主直譯器的主執行緒才被允許設定新的訊號處理程式。


模組內容
========

在 3.5 版的變更: 下面列出的訊號 (SIG*)、處理器（"SIG_DFL"、"SIG_IGN"）
和訊號遮罩 (sigmask)（"SIG_BLOCK"、"SIG_UNBLOCK"、"SIG_SETMASK"）的相
關常數被轉換成 "enums"（"Signals"、"Handlers" 和 "Sigmasks"）。
"getsignal()"、"pthread_sigmask()"、"sigpending()" 和 "sigwait()" 函式
會回傳可被人類閱讀的"枚舉"作為 "Signals" 物件。

訊號模組定義了三個枚舉：

class signal.Signals

   SIG* 常數和 CTRL_* 常數的 "enum.IntEnum" 集合。

   在 3.5 版被加入.

class signal.Handlers

   "SIG_DFL" 和 "SIG_IGN" 常數的 "enum.IntEnum" 集合。

   在 3.5 版被加入.

class signal.Sigmasks

   "SIG_BLOCK"、"SIG_UNBLOCK" 和 "SIG_SETMASK" 常數的 "enum.IntEnum"
   集合。

   可用性: Unix.

   更多資訊請見 *sigprocmask(2)* 與 *pthread_sigmask(3)* 線上手冊。

   在 3.5 版被加入.

在 "signal" 模組中定義的變數有：

signal.SIG_DFL

   這是兩種標準訊號處理選項之一；它會簡單地執行訊號的預設功能。例如，
   在大多數系統上，"SIGQUIT" 的預設動作是轉儲 (dump) 核心並退出，而
   "SIGCHLD" 的預設動作是直接忽略。

signal.SIG_IGN

   這是另一個標準的訊號處理程式，會直接忽略給定的訊號。

signal.SIGABRT

   來自 *abort(3)* 的中止訊號。

signal.SIGALRM

   來自 *alarm(2)* 的計時器訊號。

   可用性: Unix.

signal.SIGBREAK

   從鍵盤中斷 (CTRL + BREAK)。

   可用性: Windows.

signal.SIGBUS

   匯流排錯誤（記憶體存取不良）。

   可用性: Unix.

signal.SIGCHLD

   子行程停止或終止。

   可用性: Unix.

signal.SIGCLD

   "SIGCHLD" 的別名。

   可用性: not macOS.

signal.SIGCONT

   如果目前行程是被停止的，則繼續運行

   可用性: Unix.

signal.SIGFPE

   浮點運算例外。例如除以零。

   也參考: "ZeroDivisionError" 會在除法或模運算 (modulo operation) 的第二個
        引數為零時引發。

signal.SIGHUP

   偵測到控制終端機掛斷 (hangup) 或控制行程死亡。

   可用性: Unix.

signal.SIGILL

   非法指令。

signal.SIGINT

   從鍵盤中斷 (CTRL + C)。

   預設動作是引發 "KeyboardInterrupt"。

signal.SIGKILL

   殺死訊號。

   它無法被捕捉、阻擋或忽略。

   可用性: Unix.

signal.SIGPIPE

   管道中斷 (broken pipe)：寫到沒有讀取器 (reader) 的管道。

   預設動作是忽略訊號。

   可用性: Unix.

signal.SIGPROF

   Profiling timer expired.

   可用性: Unix.

signal.SIGQUIT

   Terminal quit signal.

   可用性: Unix.

signal.SIGSEGV

   記憶體區段錯誤 (segmentation fault)：無效記憶體參照。

signal.SIGSTOP

   Stop executing (cannot be caught or ignored).

signal.SIGSTKFLT

   輔助處理器 (coprocessor) 上的堆疊錯誤 (stack fault)。Linux 核心不會
   引發此訊號：它只能在使用者空間 (user space) 中引發。

   可用性: Linux.

   在訊號可用的架構上。請參閱 *signal(7)* 線上手冊以取得更多資訊。

   在 3.11 版被加入.

signal.SIGTERM

   終止訊號。

signal.SIGUSR1

   使用者定義訊號 1。

   可用性: Unix.

signal.SIGUSR2

   使用者定義訊號 2。

   可用性: Unix.

signal.SIGVTALRM

   Virtual timer expired.

   可用性: Unix.

signal.SIGWINCH

   視窗調整大小訊號。

   可用性: Unix.

signal.SIGXCPU

   CPU time limit exceeded.

   可用性: Unix.

SIG*

   All the signal numbers are defined symbolically.  For example, the
   hangup signal is defined as "signal.SIGHUP"; the variable names are
   identical to the names used in C programs, as found in
   "<signal.h>".  The Unix man page for '"signal"' lists the existing
   signals (on some systems this is *signal(2)*, on others the list is
   in *signal(7)*). Note that not all systems define the same set of
   signal names; only those names defined by the system are defined by
   this module.

signal.CTRL_C_EVENT

   與 "Ctrl"+"C" 擊鍵 (keystroke) 事件相對應的訊號。此訊號只能與
   "os.kill()" 搭配使用。

   可用性: Windows.

   在 3.2 版被加入.

signal.CTRL_BREAK_EVENT

   與 "Ctrl"+"Break" 擊鍵事件相對應的訊號。此訊號只能與 "os.kill()" 搭
   配使用。

   可用性: Windows.

   在 3.2 版被加入.

signal.NSIG

   比最高編號訊號的編號多一。使用 "valid_signals()" 來取得有效的訊號編
   號。

signal.ITIMER_REAL

   即時減少間隔計時器 (interval timer)，並在到期時送出 "SIGALRM"。

signal.ITIMER_VIRTUAL

   僅在執行行程時遞減間隔計時器，並在到期時傳送 SIGVTALRM。

signal.ITIMER_PROF

   當行程執行或系統代表行程執行時，都會減少間隔計時器。與
   ITIMER_VIRTUAL 配合，這個計時器通常用來分析 (profile) 應用程式在使
   用者空間與核心空間 (kernel space) 所花費的時間。SIGPROF 會在到期時
   傳送。

signal.SIG_BLOCK

   "pthread_sigmask()" 的 *how* 參數的可能值，表示訊號將被阻檔。

   在 3.3 版被加入.

signal.SIG_UNBLOCK

   "pthread_sigmask()" 的 *how* 參數的可能值，表示訊號將被解除阻檔。

   在 3.3 版被加入.

signal.SIG_SETMASK

   "pthread_sigmask()" 的 *how* 參數的可能值，表示訊號遮罩 (signal
   mask) 要被取代。

   在 3.3 版被加入.

"signal" 模組定義了一個例外：

exception signal.ItimerError

   當 "setitimer()" 或 "getitimer()" 底層實作發生錯誤時引發訊號。如果
   傳給 "setitimer()" 的是無效的間隔計時器或負數時間，則預期會發生此錯
   誤。這個錯誤是 "OSError" 的子型別。

   在 3.3 版被加入: 此錯誤過去是 "IOError" 的子型別，現在是 "OSError"
   的別名。

"signal" 模組定義了下列函式：

signal.alarm(time)

   如果 *time* 非零，則此函式會要求在 *time* 秒後傳送 "SIGALRM" 訊號給
   該行程。任何先前排程 (scheduled) 的警報都會被取消（任何時候都只能排
   程一個警報）。回傳值是先前設定的警報原本再等多久就會被傳送的秒數。
   如果 *time* 為零，則不會去排程任何警報，且已排程的警報會被取消。如
   果回傳值為零，則代表目前未排程任何警報。

   可用性: Unix.

   更多資訊請見 *alarm(2)* 線上手冊。

signal.getsignal(signalnum)

   回傳訊號 *signalnum* 的目前訊號處理程式。回傳值可以是一個可呼叫的
   Python 物件，或是特殊值 "signal.SIG_IGN"、"signal.SIG_DFL" 或
   "None" 之一。這裡的 "signal.SIG_IGN" 表示訊號先前被忽略，
   "signal.SIG_DFL" 表示訊號先前使用預設的處理方式，而 "None" 表示先前
   的訊號處理程式並未從 Python 安裝。

signal.strsignal(signalnum)

   回傳訊號 *signalnum* 的描述，例如 "SIGINT" 的 "Interrupt"。如果
   *signalnum* 沒有描述，則回傳 "None"。如果 *signalnum* 無效則會引發
   "ValueError"。

   在 3.8 版被加入.

signal.valid_signals()

   回傳此平台上的有效訊號編號集合。如果某些訊號被系統保留作為內部使用
   ，則此值可能小於 "range(1, NSIG)"。

   在 3.8 版被加入.

signal.pause()

   使行程休眠，直到接收到訊號；然後呼叫適當的處理程式。不會回傳任何東
   西。

   可用性: Unix.

   更多資訊請見 *signal(2)* 線上手冊。

   也請見 "sigwait()"、"sigwaitinfo()"、"sigtimedwait()" 和
   "sigpending()"。

signal.raise_signal(signum)

   傳送訊號至呼叫的行程。不會回傳任何東西。

   在 3.8 版被加入.

signal.pidfd_send_signal(pidfd, sig, siginfo=None, flags=0)

   傳送訊號 *sig* 到檔案描述器 *pidfd* 所指的行程。Python 目前不支援
   *siginfo* 參數；它必須是 "None"。*flags* 引數是提供給未來的擴充；目
   前沒有定義旗標值。

   更多資訊請見 *pidfd_send_signal(2)* 線上手冊。

   可用性: Linux >= 5.1, Android >= "build-time" API level 31

   在 3.9 版被加入.

signal.pthread_kill(thread_id, signalnum)

   將訊號 *signalnum* 傳送給與呼叫者在同一行程中的另一個執行緒
   *thread_id*。目標執行緒能執行任何程式碼（無論為 Python 與否）。但是
   ，如果目標執行緒正執行 Python 直譯器，Python 訊號處理程式將會由主直
   譯器的主執行緒來執行。因此，向特定 Python 執行緒發送訊號的唯一意義
   是強制執行中的系統呼叫以 "InterruptedError" 方式失敗。

   使用 "threading.get_ident()" 或 "threading.Thread" 物件的 "ident"
   屬性來取得 *thread_id* 的適當值。

   如果 *signalnum* 為 0，則不會傳送訊號，但仍會執行錯誤檢查；這可用來
   檢查目標執行緒是否仍在執行。

   引發一個附帶引數 "thread_id"、"signalnum" 的稽核事件
   "signal.pthread_kill"。

   可用性: Unix.

   更多資訊請見 *pthread_kill(3)* 線上手冊。

   另請參閱 "os.kill()"。

   在 3.3 版被加入.

signal.pthread_sigmask(how, mask)

   擷取和/或變更呼叫執行緒的訊號遮罩。訊號遮罩是目前阻擋呼叫者傳送的訊
   號集合。將舊的訊號遮罩作為一組訊號集合回傳。

   呼叫的行為取決於 *how* 的值，如下所示。

   * "SIG_BLOCK"：被阻檔的訊號集合是目前訊號集合與 *mask* 引數的聯集。

   * "SIG_UNBLOCK"：將 *mask* 中的訊號從目前阻檔的訊號集合中移除。嘗試
     為未被阻檔的訊號解除阻檔是允許的。

   * "SIG_SETMASK"：將被阻檔的訊號集合設定為 *mask* 引數。

   *mask* 是一組訊號編號（例如 {"signal.SIGINT", "signal.SIGTERM"}）的
   集合。使用 "valid_signals()" 來取得包含所有訊號的完整遮罩。

   例如，"signal.pthread_sigmask(signal.SIG_BLOCK, [])" 會讀取呼叫執行
   緒的訊號遮罩。

   "SIGKILL" 和 "SIGSTOP" 不能被阻檔。

   可用性: Unix.

   更多資訊請見 *sigprocmask(2)* 與 *pthread_sigmask(3)* 線上手冊。

   另請參閱 "pause()"、"sigpending()" 與 "sigwait()"。

   在 3.3 版被加入.

signal.setitimer(which, seconds, interval=0.0)

   設定由 *which* 指定的間隔計時器（"signal.ITIMER_REAL"、
   "signal.ITIMER_VIRTUAL" 或 "signal.ITIMER_PROF" 之一）並在*seconds*
   （接受浮點數，與 "alarm()" 不同）之後啟動，在之後的每 *interval* 秒
   啟動一次（如果 *interval* 非零）。*which* 指定的間隔計時器可透過將
   *seconds* 設定為零來清除它。

   當間隔計時器啟動時，一個訊號會被傳送給行程。傳送的訊號取決於使用的
   計時器；"signal.ITIMER_REAL" 會傳送 "SIGALRM"，
   "signal.ITIMER_VIRTUAL" 會傳送 "SIGVTALRM"，而 "signal.ITIMER_PROF"
   會傳送 "SIGPROF"。

   舊值會以一個元組回傳：(delay, interval)。

   嘗試傳入無效的間隔計時器會導致 "ItimerError"。

   可用性: Unix.

signal.getitimer(which)

   回傳由 *which* 指定之間隔計時器的目前值。

   可用性: Unix.

signal.set_wakeup_fd(fd, *, warn_on_full_buffer=True)

   設定喚醒檔案描述器為 *fd*。當接收到其處理程式已有被註冊的訊號時，訊
   號編號會以單一位元組寫入 fd。如果你沒有為你在意的訊號註冊處理程式，
   就不會寫入到喚醒 fd。這可被函式庫用來喚醒輪詢 (wakeup a poll) 或
   select 呼叫，讓訊號得以完全處理。

   回傳舊的喚醒 fd（如果檔案描述器喚醒未啟用，則回傳 -1）。如果 *fd*
   為 -1，則會停用檔案描述器喚醒。如果不是 -1，*fd* 必須是非阻塞的。在
   再次呼叫輪詢或 select 之前，由函式庫來決定是否移除 *fd* 中的任何位
   元組。

   當啟用執行緒時，這個函式只能從主直譯器的主執行緒來呼叫；嘗試從其他
   執行緒呼叫它將會引發 "ValueError" 例外。

   使用這個函式有兩種常見的方式。在這兩種方法中，當訊號抵達時，你都要
   使用 fd 來喚醒，但它們的不同之處在於如何判斷*哪個或哪些*訊號有抵達
   。

   在第一種方法中，我們從 fd 的緩衝區中讀取資料，而位元組值則提供訊號
   編號。這個方法很簡單，但在少數情況下可能會遇到問題：一般來說，fd 的
   緩衝區空間有限，如果太多訊號來得太快，那麼緩衝區可能會滿，而有些訊
   號可能會遺失。如果你使用這種方法，那麼你應該設定
   "warn_on_full_buffer=True"，這至少會在訊號丟失時將警告印出到 stderr
   。

   在第二種方法中，我們*只會*將喚醒 fd 用於喚醒，而忽略實際的位元組值
   。在這種情況下，我們只在乎 fd 的緩衝區是空或非空；即便緩衝區滿了也
   不代表有問題。如果你使用這種方法，那麼你應該設定
   "warn_on_full_buffer=False"，這樣你的使用者就不會被虛假的警告訊息所
   混淆。

   在 3.5 版的變更: 在 Windows 上，此功能現在也支援 socket 處理程式。

   在 3.7 版的變更: 新增 "warn_on_full_buffer" 參數。

signal.siginterrupt(signalnum, flag)

   改變系統呼叫重新啟動的行為：如果 *flag* 是 "False"，系統呼叫會在被
   訊號 *signalnum* 中斷時重新啟動，否則系統呼叫會被中斷。不會回傳任何
   東西。

   可用性: Unix.

   更多資訊請見 *siginterrupt(3)* 線上手冊。

   請注意，使用 "signal()" 安裝訊號處理程式，會透過隱式呼叫
   "siginterrupt()" 來將重新啟動的行為重設為可中斷，且指定訊號的
   *flag* 值為 true。

signal.signal(signalnum, handler)

   將訊號 *signalnum* 的處理程式設定為函式 *handler*。*handler* 可以是
   帶兩個引數的可呼叫 Python 物件（見下面），或是特殊值
   "signal.SIG_IGN" 或 "signal.SIG_DFL" 之一。先前的訊號處理程式將會被
   回傳（請參閱上面 "getsignal()" 的說明）。（更多資訊請參閱 Unix 線上
   手冊 *signal(2)*）。

   當啟用執行緒時，這個函式只能從主直譯器的主執行緒來呼叫；嘗試從其他
   執行緒呼叫它將會引發 "ValueError" 例外。

   *handler* 被呼叫時有兩個引數：訊號編號和目前的堆疊 frame（"None" 或
   一個 frame 物件；關於 frame 物件的描述，請參閱型別階層中的描述或
   "inspect" 模組中的屬性描述）。

   在 Windows 上，"signal()" 只能在使用 "SIGABRT"、"SIGFPE"、"SIGILL"
   、"SIGINT"、"SIGSEGV"、"SIGTERM" 或 "SIGBREAK" 時呼叫。在其他情況下
   會引發 "ValueError"。請注意，並非所有系統都定義相同的訊號名稱；如果
   訊號名稱沒有被定義為 "SIG*" 模組層級常數，則會引發 "AttributeError"
   錯誤。

signal.sigpending()

   檢查待傳送至呼叫執行緒的訊號集合（即阻檔時已被提出的訊號）。回傳待
   定訊號的集合。

   可用性: Unix.

   更多資訊請見 *sigpending(2)* 線上手冊。

   另請參閱 "pause()"、"pthread_sigmask()" 與 "sigwait()"。

   在 3.3 版被加入.

signal.sigwait(sigset)

   暫停呼叫執行緒的執行，直到送出訊號集合 *sigset* 中指定的一個訊號。
   函式接受訊號（將其從待定訊號清單中移除），並回傳訊號編號。

   可用性: Unix.

   更多資訊請見 *sigwait(3)* 線上手冊。

   另也請見 "pause()"、"pthread_sigmask()"、"sigpending()"、
   "sigwaitinfo()" 和 "sigtimedwait()"。

   在 3.3 版被加入.

signal.sigwaitinfo(sigset)

   暫停呼叫執行緒的執行，直到送出訊號集合 *sigset* 中指定的一個訊號。
   該函式接受訊號，並將其從待定訊號清單中移除。如果 *sigset* 中的一個
   訊號已經是呼叫執行緒的待定訊號，函式會立即回傳該訊號的相關資訊。對
   於已傳送的訊號，訊號處理程式不會被呼叫。如果被不在 *sigset* 中的訊
   號中斷，函式會引發 "InterruptedError"。

   The return value is an object representing the data contained in
   the "siginfo_t" structure, namely: "si_signo", "si_code",
   "si_errno", "si_pid", "si_uid", "si_status", "si_band".

   可用性: Unix.

   更多資訊請見 *sigwaitinfo(2)* 線上手冊。

   另請參閱 "pause()"、"sigwait()" 與 "sigtimedwait()"。

   在 3.3 版被加入.

   在 3.5 版的變更: 現在如果被不在 *sigset* 中的訊號中斷，且訊號處理程
   式沒有引發例外，則會重試函式（理由請參閱 **PEP 475**）。

signal.sigtimedwait(sigset, timeout)

   類似 "sigwaitinfo()"，但需要額外的 *timeout* 引數指定逾時時間。如果
   *timeout* 指定為 "0"，會執行輪詢。如果發生逾時則會回傳 "None"。

   可用性: Unix.

   更多資訊請見 *sigtimedwait(2)* 線上手冊。

   另請參閱 "pause()"、"sigwait()" 與 "sigwaitinfo()"。

   在 3.3 版被加入.

   在 3.5 版的變更: 現在如果被不在 *sigset* 中的訊號中斷，且訊號處理程
   式沒有引發例外，則會使用重新計算的 *timeout* 重試函式（理由請參閱
   **PEP 475**）。


範例
====

這是一個最小範例程式。它使用 "alarm()" 函式來限制等待開啟檔案的時間；
如果檔案是用於可能未開啟的序列裝置，這會很有用，因為這通常會導致
"os.open()" 無限期地被擱置。解決方法是在開啟檔案前設定一個 5 秒的警報
；如果操作時間過長，警報訊號就會被送出，而處理程式會產生例外。

   import signal, os

   def handler(signum, frame):
       signame = signal.Signals(signum).name
       print(f'Signal handler called with signal {signame} ({signum})')
       raise OSError("Couldn't open device!")

   # 設定訊號處理程式與五秒警報
   signal.signal(signal.SIGALRM, handler)
   signal.alarm(5)

   # 這個 open() 可能無限期地被擱置
   fd = os.open('/dev/ttyS0', os.O_RDWR)

   signal.alarm(0)          # 停用警報


關於 SIGPIPE 的說明
===================

將程式的輸出管道化到 *head(1)* 之類的工具，會在你的行程的標準輸出接收
器提早關閉時，導致 "SIGPIPE" 訊號傳送給你的行程。這會導致類似
"BrokenPipeError: [Errno 32] Broken pipe" 的例外。要處理這種情況，請將
你的進入點包裝成如下的樣子來捕捉這個例外：

   import os
   import sys

   def main():
       try:
           # 模擬大量輸出（你的程式取代此迴圈）
           for x in range(10000):
               print("y")
           # 在這裡清除輸出以強制 SIGPIPE 在這個 try 區塊
           # 中被觸發
           sys.stdout.flush()
       except BrokenPipeError:
           # Python 在退出時清除標準串流；為剩下的輸出重新導向
           # 至 devnull 來避免關閉時的 BrokenPipeError
           devnull = os.open(os.devnull, os.O_WRONLY)
           os.dup2(devnull, sys.stdout.fileno())
           sys.exit(1)  # Python 在 EPIPE 時以錯誤碼 1 退出

   if __name__ == '__main__':
       main()

不要為了避免 "BrokenPipeError" 而將 "SIGPIPE" 之處置 (disposition) 設
定為 "SIG_DFL"。這樣做會導致你的程式在寫入任何 socket 連線時被中斷而意
外退出。


訊號處理程式與例外的說明
========================

如果訊號處理程式產生例外，例外會傳送到主執行緒並可能在任何 *bytecode*
指令之後發生。最值得注意的是，"KeyboardInterrupt" 可能在執行過程中的任
何時候出現。大多數 Python 程式碼，包括標準函式庫，都無法避免這種情況，
因此 "KeyboardInterrupt"（或任何其他由訊號處理程式產生的例外）可能會在
罕見的情況下使程式處於預期之外的狀態。

為了說明這個問題，請參考以下程式碼：

   class SpamContext:
       def __init__(self):
           self.lock = threading.Lock()

       def __enter__(self):
           # 如果 KeyboardInterrupt 在此發生則一切正常
           self.lock.acquire()
           # 如果 KeyboardInterrupt 在此發生，__exit__ 將不會被呼叫
           ...
           # KeyboardInterrupt 可能在函式回傳之前發生

       def __exit__(self, exc_type, exc_val, exc_tb):
           ...
           self.lock.release()

對許多程式來說，尤其是那些只想在 "KeyboardInterrupt" 時退出的程式，這
並不是問題，但是對於複雜或需要高可靠性的應用程式來說，應該避免從訊號處
理程式產生例外。它們也應該避免將捕獲 "KeyboardInterrupt" 作為一種優雅
關閉 (gracefully shutting down) 的方式。相反地，它們應該安裝自己的
"SIGINT" 處理程式。以下是 HTTP 伺服器避免 "KeyboardInterrupt" 的範例：

   import signal
   import socket
   from selectors import DefaultSelector, EVENT_READ
   from http.server import HTTPServer, SimpleHTTPRequestHandler

   interrupt_read, interrupt_write = socket.socketpair()

   def handler(signum, frame):
       print('Signal handler called with signal', signum)
       interrupt_write.send(b'\0')
   signal.signal(signal.SIGINT, handler)

   def serve_forever(httpd):
       sel = DefaultSelector()
       sel.register(interrupt_read, EVENT_READ)
       sel.register(httpd, EVENT_READ)

       while True:
           for key, _ in sel.select():
               if key.fileobj == interrupt_read:
                   interrupt_read.recv(1)
                   return
               if key.fileobj == httpd:
                   httpd.handle_request()

   print("Serving on port 8000")
   httpd = HTTPServer(('', 8000), SimpleHTTPRequestHandler)
   serve_forever(httpd)
   print("Shutdown...")
