ご質問の内容に興味が沸きましたので試してみました。
受信タイムアウト発生前に割り込んで受信を終了させることはできないでしょうか。
一見、__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
1 Py_BEGIN_ALLOW_THREADS
2 res = sock_func ( s , data ) ;
3 Py_END_ALLOW_THREADS
PythonのGIL(Global Interpreter Lock)で守られているようで、他スレッドで実行中のソケットのシステムコール(質問者さんの例ではrecv_from
)を、他スレッドからキャンセルすることは無理そうです。
代替案として、元のコードとほとんど同じですがselect.select
を使って一定時間ごとにフラグをチェックする方法をご紹介します。質問者さんは「受信タイムアウトの例外を利用してフラグをチェックする方法がスマートではない」かんじがするとのことでしたが、select
を使えば意味的にも少しスマートです。また、ソケットに受信タイムアウトをセットする必要もなくなります。
Python3
1 # t1b.py - select
2 import socket
3 import time
4 import datetime
5 import threading
6 import select
7 import sys
8
9 global is_recv
10
11 def 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
48 if __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
2 main start: time = 2019 -04-03 15 :30:53.343439
3 received from = ( '192.168.11.100' , 0 ) , len = 36
4 received from = ( '192.168.11.100' , 0 ) , len = 36
5 received from = ( '192.168.11.100' , 0 ) , len = 36
6 received from = ( '192.168.11.100' , 0 ) , len = 36
7 recv_func leaves.
8 main end: time = 2019 -04-03 15 :31:01.818659
9 user01@ubuntu1604-x64:/mnt/share$
10
もうひとつ、今度はスレッドを使わない方法に直してみたものをご案内します。シグナルを使ったものです。recv_func
関数は__main__
で実行してしまい、sys.alarm
で8秒後にシグナル(SIGALRM
)を発生させ、非同期で動くシグナルハンドラーの中でソケットをclose
します。同時にis_recv
フラグをOFFにし、抜けるようにするものです。
なお、with
構文の終わりで自動でソケットをclose
する流れになるかと思いますが、テスト用のコードなので流用する際は適当に直してください。
Python3
1 # t1d.py
2 import socket
3 import time
4 import datetime
5 import threading
6 import sys
7 import signal
8
9 global is_recv
10 global g_sock
11
12 def 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
20 def 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
51 if __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
1 user01@ubuntu1604-x64:/mnt/share$ sudo python3 t1d.py
2 main-start: time = 2019 -04-03 15 :29:46.341175
3 received from = ( '192.168.11.100' , 0 ) , len = 44
4 received from = ( '192.168.11.100' , 0 ) , len = 44
5 SIGALRM !
6
7 [ Errno 9 ] Bad file descriptor
8 recv_func leaves.
9 main-end: time = 2019 -04-03 15 :29:54.445209
10 user01@ubuntu1604-x64:/mnt/share$