C言語でこんなコードを書くとどうなるでしょうか?
#define NUM_THREADS 5 #define TARGET_IP "127.0.0.1" #define TARGET_PORT 8080 void *thread_func(void *arg) { int sockfd; struct sockaddr_in serv_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(TARGET_PORT); serv_addr.sin_addr.s_addr = inet_addr(TARGET_IP); if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("connect"); // close(sockfd); ← 通常なら閉じるが、今回はあえて漏らす pthread_exit(NULL); } printf("Thread %ld connected on socket %d\n", (long)arg, sockfd); pthread_exit(NULL); } int main() { pthread_t threads[NUM_THREADS]; pid_t pid = getpid(); printf("PID = %d\n", pid); for (long i = 0; i < NUM_THREADS; i++) { if (pthread_create(&threads[i], NULL, thread_func, (void *)i) != 0) { perror("pthread_create"); exit(1); } } for (int i = 0; i < NUM_THREADS; i++) { pthread_join(threads[i], NULL); } printf("Sleeping for 60 seconds... check with 'lsof -p %d'\n", pid); sleep(60); }
スレッドが終了してもプロセスの終了まではコネクションは残り続けます。これはCでもGoでも似たようなことがおきます。(Goだと少し違いますがclose忘れでリークするという意味で)Pythonだとどうなるでしょうか?以下のコード実行のsleep中ですでにコネクションが閉じられます!なってこった!
import asyncio import socket import os NUM_CONNECTIONS = 1 TARGET_HOST = '127.0.0.1' TARGET_PORT = 8080 def open_connection(i): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect((TARGET_HOST, TARGET_PORT)) print(f"[Thread] Connection {i} opened on fd {sock.fileno()}") except Exception as e: print(f"[Thread] Connection {i} failed: {e}") async def main(): pid = os.getpid() print(f"PID = {pid}") tasks = [asyncio.to_thread(open_connection, i) for i in range(NUM_CONNECTIONS)] await asyncio.gather(*tasks) print(f"All connections attempted. Sleeping 60 sec... check with 'lsof -p {pid}'") await asyncio.sleep(60) # この間に lsof で確認 if __name__ == "__main__": asyncio.run(main())
デストラクタ
オブジェクトが消えるとき(メモリ解放されるとき)に呼ばれる処理 のこと。今回はこれが動くことでソケットがアプリケーションコードでcloseしなくても閉じられるという感じのようです。
class MyClass: def __del__(self): print("destructor called") obj = MyClass() del obj # ← ここで __del__() が呼ばれる
socketクラス
じゃあさっきのサンプルコードをみる感じsocketクラスでデストラクタは定義されているだろうと見に行くとデストラクタは定義されていなかった。
# こんなのがいると思った def __del__(self): self.close()
Python の socket.socket クラスは C拡張の _socket.socket
クラス を継承しているようでした。なのでPython 側の socket.socket クラスには del() は定義されていないのです。
class socket(_socket.socket): """A subclass of _socket.socket adding the makefile() method.""" __slots__ = ["__weakref__", "_io_refs", "_closed"] def __init__(self, family=-1, type=-1, proto=-1, fileno=None): # For user code address family and type values are IntEnum members, but # for the underlying _socket.socket they're just integers. The # constructor of _socket.socket converts the given argument to an # integer automatically. if fileno is None: if family == -1: family = AF_INET if type == -1: type = SOCK_STREAM if proto == -1: proto = 0 _socket.socket.__init__(self, family, type, proto, fileno) self._io_refs = 0 self._closed = False
Py_tp_finalize
Py_tp_finalize は、Python C API における PyTypeObject のスロットのひとつで、オブジェクトのファイナライザを定義するために使用されます。これは Python 3.4 で導入された PEP 442 の内容に基づいて追加されたものであり、古い tp_del に代わる安全なファイナライザとして設計されているらしいです。オブジェクトの解放じに呼ばれる処理というのを書ける仕組みになっています。先ほど継承しているクラスを見に行くと以下のように定義されていました。
じゃあcloseは書かなくて良いのか
ソケットはガベージコレクション時に自動的にクローズされます。しかし、明示的に close() するか、 with 文の中でソケットを使うことを推奨します。
推奨はcloseするかwithで囲っておきましょうでした。なるほど。
なんとかしてリークさせたい!
リークしないなんてつまらない!リークさせたい!ってことで_socketを書き換えようと思ったのですが書き換えた後にPythonをビルドする必要がでてくるのでそれは面倒なので継承させて__del__
をオーバーライドさせてpassに書き換えることでうまく行きました。リーク!
class LeakingSocket(socket.socket): def __init__(self, i): super().__init__(socket.AF_INET, socket.SOCK_STREAM) self.i = i def connect_and_log(self): try: self.connect((TARGET_HOST, TARGET_PORT)) print(f"[Thread] Connection {self.i} opened on fd {self.fileno()}") except Exception as e: print(f"[Thread] Connection {self.i} failed: {e}") # デストラクタを無効化して close() を呼ばせない def __del__(self): pass