質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Q&A

解決済

3回答

7509閲覧

python3でsocket.recvを強制終了したい

y.nakamura

総合スコア190

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

1グッド

1クリップ

投稿2019/04/02 11:10

編集2019/04/04 05:07

説明

Python3でマルチキャスト受信するプログラムを作成しているのですが、socket.recvをしている途中で通信を終了したい場合があります。
socketにタイムアウトを設定すれば受信タイムアウト発生後に通信終了を判定することができるのですが、受信タイムアウト発生前に割り込んで受信を終了させることはできないでしょうか。
例えば以下のコードですと、理想は待機後の8秒で終了することですが、実際はタイムアウトが2回発生した後の10秒後に終了してしまいます。
どうにかして8秒で強制終了させられないでしょうか。よろしくお願いします。

コード

python3

1global is_recv 2 3 4def recv_func(addr, port, timeout): 5 global is_recv 6 7 is_recv = True 8 9 with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP) as sock: 10 sock.settimeout(timeout) 11 12 # マルチキャストJOIN 13 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 14 sock.bind('', port) 15 sock.setsockopt(socket.IPPROTO_IP, 16 socket.IP_ADD_MEMBERSHIP, 17 socket.inet_aton(addr) + socket.inet_aton('0.0.0.0')) 18 19 # 受信 20 while is_recv: 21 try: 22 data, addr = sock.recvfrom(4096) 23 print('received from={}, len={}'.format(addr, len(data))) 24 except Exception as ex: 25 print(ex) 26 finally: 27 time.sleep(0.1) 28 29 # マルチキャストLeave(略) 30 pass 31 32 33if __name__ == '__main__': 34 global is_recv 35 36 # 開始 37 print('start: time={}'.format(datetime.now())) 38 t = Thread(target=recv_func, args=('239.0.0.1', 5000, 5,)) 39 t.start() 40 41 # 待機 42 time.sleep(8) 43 44 # 終了 45 is_recv = False 46 t.join() 47 print('end: time={}'.format(datetime.now()))

追記

sleepの8秒は決められた値ではなく変動するため、終了時間を予想してtimeoutを設定する、ということはできません。
timeoutを0.1秒など小さな値にしてtry~exceptを使用すれば、is_recv=False直後にスレッド終了が可能ですが、Exceptionを大量発生させるのもスマートではないため、他の方法を模索しています。

ramin👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

y.nakamura

2019/04/02 11:41 編集

いえ、動作は以下のようになっており、10秒の受信タイムアウト(2)の後にプログラム終了しているところを、08秒の待機sleep終了直後にプログラム終了したいと思っています。 08秒のスレッド終了指示でis_recvをFalseにするのではなく、何らかのsocket操作で強制終了できないでしょうか? ・00秒  ・プログラム開始  ・スレッド開始  ・受信開始(1)  ・待機sleep開始 ・05秒  ・受信タイムアウト発生(1)  ・受信開始(2) ・08秒  ・待機sleep終了  ・スレッド終了指示 ・10秒  ・受信タイムアウト発生(2)  ・スレッド終了  ・プログラム終了
Kenji.Noguchi

2019/04/02 17:38 編集

ソケットのタイムアウトを5秒から8秒に変更して、Timeoutが発生したらis_recv=Falseすればいいのでは?現状ソケットのタイムアウトが5秒だからメインスレッドが8秒スリープしてたら2回目のsock.recvがブロッキングするので5秒x2で10秒になるのは当然ですね。
y.nakamura

2019/04/02 23:53

5秒×2で10秒になるのは承知しております。 またsleepの8秒は決められた値ではなく変動するため、終了時間を予想してtimeoutを設定する、ということはできません。 timeoutを0.1秒など小さな値にしてtry~exceptを使用すれば、is_recv=False後にすぐスレッド終了が可能ですが、Exceptionを大量発生させるのもスマートではないため、他の方法を模索しています。
Kenji.Noguchi

2019/04/03 00:33 編集

なるほど。ちょっと考えてみます。
guest

回答3

0

ベストアンサー

ご質問の内容に興味が沸きましたので試してみました。

受信タイムアウト発生前に割り込んで受信を終了させることはできないでしょうか。

一見、__main__の方のメインスレッドで socket.close()を呼び出せばrecv_funcスレッドのsocket.recv_fromでExceptionで抜けるのではないかと思って試してみたですが、その方法ではダメでした。recv_funcスレッド内のrecv_fromから抜けてきません。

私自身、C言語での生なソケットプログラミングで別のスレッドから強制的にソケットをcloseして処理を抜けさせるような(少々乱暴な)方法を使うことがあり、それでできるかと思ったのですが、PythonではNGのようです。

Python-3.5.7 のソースコードを読むと、ソケットのシステムコールを呼び出すC言語の関数 sock_call_exで、以下のようなコメントがありました。

// Python-3.5.7/Modules/socketmodule.c /* Call a socket function. ...省略 sock_call_ex() must be called with the GIL held. The socket function is called with the GIL released. */

sock_call_ex関数内でのシステムコール呼び出し部分では、以下のように呼び出し前後でガードされています。

C

1Py_BEGIN_ALLOW_THREADS 2res = sock_func(s, data); 3Py_END_ALLOW_THREADS

PythonのGIL(Global Interpreter Lock)で守られているようで、他スレッドで実行中のソケットのシステムコール(質問者さんの例ではrecv_from)を、他スレッドからキャンセルすることは無理そうです。

代替案として、元のコードとほとんど同じですがselect.selectを使って一定時間ごとにフラグをチェックする方法をご紹介します。質問者さんは「受信タイムアウトの例外を利用してフラグをチェックする方法がスマートではない」かんじがするとのことでしたが、selectを使えば意味的にも少しスマートです。また、ソケットに受信タイムアウトをセットする必要もなくなります。

Python3

1# t1b.py - select 2import socket 3import time 4import datetime 5import threading 6import select 7import sys 8 9global is_recv 10 11def recv_func(addr, port, timeout): 12 global is_recv 13 14 is_recv = True 15 16 with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP) as sock: 17# sock.settimeout(timeout) 18 19 # マルチキャストJOIN 20 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 21 sock.setsockopt(socket.IPPROTO_IP, 22 socket.IP_ADD_MEMBERSHIP, 23 socket.inet_aton(addr) + socket.inet_aton('0.0.0.0')) 24 25 # 本コードではブロックしてもしなくても同じ 26 #sock.setblocking(0) 27 rfds = [sock] 28 29 # 受信 30 while is_recv: 31 try: 32 # 0.5秒ごとにselectタイムアウトで is_recvフラグチェックの機会を与える 33 r, _, _ = select.select(rfds, [], [], 0.5) 34 for rs in r: 35 data, addr = sock.recvfrom(4096) 36 print('received from={}, len={}'.format(addr, len(data))) 37 except Exception as ex: 38 # print(ex) 39 print('Exception: time={}, desc={}'.format(datetime.datetime.now(), ex)) 40 finally: 41 time.sleep(0.1) 42 43 # マルチキャストLeave(略) 44 # pass 45 print("recv_func leaves.") 46 47 48if __name__ == '__main__': 49 global is_recv 50 51 # 開始 52 print('main start: time={}'.format(datetime.datetime.now())) 53 t = threading.Thread(target=recv_func, args=('239.0.0.1', 5000, 5,)) 54 t.start() 55 56 # 待機 57 time.sleep(8) 58 is_recv = False 59 60 # スレッド終了待ち 61 t.join() 62 63 # 終了 64 print('main end: time={}'.format(datetime.datetime.now()))

Ubuntu16.04(x64) / Python3.5.2 での実行結果です。ほぼ8秒できれいに終わります。

bash

1@ubuntu1604-x64:/mnt/share$ sudo python3 t1b.py 2main start: time=2019-04-03 15:30:53.343439 3received from=('192.168.11.100', 0), len=36 4received from=('192.168.11.100', 0), len=36 5received from=('192.168.11.100', 0), len=36 6received from=('192.168.11.100', 0), len=36 7recv_func leaves. 8main end: time=2019-04-03 15:31:01.818659 9user01@ubuntu1604-x64:/mnt/share$ 10

もうひとつ、今度はスレッドを使わない方法に直してみたものをご案内します。シグナルを使ったものです。recv_func関数は__main__で実行してしまい、sys.alarmで8秒後にシグナル(SIGALRM)を発生させ、非同期で動くシグナルハンドラーの中でソケットをcloseします。同時にis_recvフラグをOFFにし、抜けるようにするものです。

なお、with構文の終わりで自動でソケットをcloseする流れになるかと思いますが、テスト用のコードなので流用する際は適当に直してください。

Python3

1# t1d.py 2import socket 3import time 4import datetime 5import threading 6import sys 7import signal 8 9global is_recv 10global g_sock 11 12def signal_handler(signal, frame): 13 global is_recv 14 global g_sock 15 print("SIGALRM!\n") 16 g_sock.close() 17 is_recv = False 18 19 20def recv_func(addr, port, timeout): 21 global is_recv 22 global g_sock 23 24 is_recv = True 25 26 with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP) as sock: 27# sock.settimeout(timeout) 28 g_sock = sock 29 30 # マルチキャストJOIN 31 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 32 sock.setsockopt(socket.IPPROTO_IP, 33 socket.IP_ADD_MEMBERSHIP, 34 socket.inet_aton(addr) + socket.inet_aton('0.0.0.0')) 35 36 # 受信 37 while is_recv: 38 try: 39 data, addr = sock.recvfrom(4096) 40 print('received from={}, len={}'.format(addr, len(data))) 41 except Exception as ex: 42 print(ex) 43 finally: 44 time.sleep(0.1) 45 46 # マルチキャストLeave(略) 47 pass 48 print('recv_func leaves.') 49 50 51if __name__ == '__main__': 52 global is_recv 53 54 signal.signal(signal.SIGALRM, signal_handler) 55 56 # 開始 57 print('main-start: time={}'.format(datetime.datetime.now())) 58 # スレッドは使わない 59 #t = threading.Thread(target=recv_func, args=('239.0.0.1', 5000, 5,)) 60 #t.start() 61 62 # 8秒後にシグナルで通知 63 signal.alarm(8) 64 recv_func('239.0.0.1', 5000, 5) 65 #time.sleep(8) 66 67 # 終了 68 print('main-end: time={}'.format(datetime.datetime.now()))

同、実行結果です。これもきれいにほぼ8秒で終わります。

bash

1user01@ubuntu1604-x64:/mnt/share$ sudo python3 t1d.py 2main-start: time=2019-04-03 15:29:46.341175 3received from=('192.168.11.100', 0), len=44 4received from=('192.168.11.100', 0), len=44 5SIGALRM! 6 7[Errno 9] Bad file descriptor 8recv_func leaves. 9main-end: time=2019-04-03 15:29:54.445209 10user01@ubuntu1604-x64:/mnt/share$

投稿2019/04/03 06:42

dodox86

総合スコア9183

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

y.nakamura

2019/04/04 17:52

recv中の他操作はコード上で明確にロックされているのですね。 selectモジュールを使用することで、期待通りの動作を実現することができました。 シグナルを使用したものも、別のシーンで利用することができそうです。 大変勉強になりました。ありがとうございます。
guest

0

終了 でsys.exit()でもしてやれば強制的に終わりませんか。

投稿2019/04/02 13:28

編集2019/04/02 13:29
hayataka2049

総合スコア30933

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dodox86

2019/04/03 06:51

別回答の流れで試させていただいたのですが、興味深いことにメイン(__main__)のスレッドは終わるもののrecv_funcスレッドの方は終わらず、動いたままでした。(Ubuntu 16.04 64ビット / Python 3.5.2)
y.nakamura

2019/04/04 04:15 編集

ご回答ありがとうございます。 スレッド作成の際にdaemon=Trueを指定することで、メインスレッドと共にrecv_funcスレッドを強制終了することができました。 t = Thread(target=receive_func, args=('239.0.0.1', 5000, 5,), daemon=True) その場合マルチキャストのLeaveをスキップしてしまいますが、強制終了可能ということで一案とさせていただきます。
guest

0

コメントに書きましたが、ソケットのタイムアウトを8秒にして、タイムアウトが発生したらループから抜ければOKでしょう。

Python3

1 while is_recv: 2 try: 3 data, addr = sock.recvfrom(4096) 4 print('received from={}, len={}'.format(addr, len(data))) 5 except socket.timeout: 6 is_recv = False 7 except Exception as ex: 8 print(ex) 9 finally: 10 time.sleep(0.1) 11 12(中略) 13 14 t = Thread(target=recv_func, args=('239.0.0.1', 5000, 8,))

結果

start: time=2019-04-02 10:59:18.920342 end: time=2019-04-02 10:59:26.925506

投稿2019/04/02 17:59

Kenji.Noguchi

総合スコア358

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

y.nakamura

2019/04/04 04:07

ご回答ありがとうございます。 条件等を質問内容に追記させていただきました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問