私が次のコードを持っていると仮定しましょう:
import asyncio
import threading
queue = asyncio.Queue()
def threaded():
import time
while True:
time.sleep(2)
queue.put_nowait(time.time())
print(queue.qsize())
@asyncio.coroutine
def async():
while True:
time = yield from queue.get()
print(time)
loop = asyncio.get_event_loop()
asyncio.Task(async())
threading.Thread(target=threaded).start()
loop.run_forever()
このコードの問題は、async
サイズが増加している間、queue
コルーチン内のループが最初の反復を終了しないことです。
なぜこれがこのように起こっているのですか?それを修正するために何ができますか?
私の実際のコードでは、シリアルデバイスと通信するために別のスレッドを使用しており、asyncio
を使用してそれを行う方法が見つからないため、別のスレッドを取り除くことはできません。
asyncio.Queue
スレッドセーフではありません なので、複数のスレッドから直接使用することはできません。代わりに、 janus
を使用できます。これは、スレッド対応のasyncio
キューを提供するサードパーティのライブラリです。
import asyncio
import threading
import janus
def threaded(squeue):
import time
while True:
time.sleep(2)
squeue.put_nowait(time.time())
print(squeue.qsize())
@asyncio.coroutine
def async(aqueue):
while True:
time = yield from aqueue.get()
print(time)
loop = asyncio.get_event_loop()
queue = janus.Queue(loop=loop)
asyncio.Task(asyncio.ensure_future(queue.async_q))
threading.Thread(target=threaded, args=(queue.sync_q,)).start()
loop.run_forever()
aioprocessing
(完全開示:私が書いた)もあります。これは、プロセスセーフ(および副作用としてスレッドセーフ)キューも提供しますが、それはやり過ぎです。 multiprocessing
を使用しようとしていない場合。
_BaseEventLoop.call_soon_threadsafe
_が手元にあります。詳細については、 asyncio
doc を参照してください。
threaded()
を次のように変更するだけです。
_def threaded():
import time
while True:
time.sleep(1)
loop.call_soon_threadsafe(queue.put_nowait, time.time())
loop.call_soon_threadsafe(lambda: print(queue.qsize()))
_
出力例は次のとおりです。
_0
1443857763.3355968
0
1443857764.3368602
0
1443857765.338082
0
1443857766.3392274
0
1443857767.3403943
_
別のライブラリを使用したくない場合は、スレッドからコルーチンをスケジュールできます。 _queue.put_nowait
_を次のように置き換えると正常に機能します。
_asyncio.run_coroutine_threadsafe(queue.put(time.time()), loop)
_
変数loop
は、メインスレッドのイベントループを表します。
編集:
async
コルーチンが何もしていない理由は、イベントループがコルーチンにそうする機会を決して与えないからです。キューオブジェクトはスレッドセーフではありません。cpythonコードを調べると、これは、イベントループの_put_nowait
_メソッドでfutureを使用して、_call_soon
_がキューのコンシューマーをウェイクアップすることを意味します。 _call_soon_threadsafe
_を使用させることができれば、機能するはずです。ただし、_call_soon
_と_call_soon_threadsafe
_の主な違いは、_call_soon_threadsafe
_が loop._write_to_self()
を呼び出すことによってイベントループをウェイクアップすることです。それで、それを私たち自身と呼びましょう:
_import asyncio
import threading
queue = asyncio.Queue()
def threaded():
import time
while True:
time.sleep(2)
queue.put_nowait(time.time())
queue._loop._write_to_self()
print(queue.qsize())
@asyncio.coroutine
def async():
while True:
time = yield from queue.get()
print(time)
loop = asyncio.get_event_loop()
asyncio.Task(async())
threading.Thread(target=threaded).start()
loop.run_forever()
_
その後、すべてが期待どおりに機能します。
共有オブジェクトへのアクセスのスレッドセーフな側面については、_asyncio.queue
_は内部で_collections.deque
_を使用します。これにはスレッドセーフなappend
とpopleft
があります。キューが空ではなく、popleftがアトミックではないかどうかをチェックすることもできますが、キューを1つのスレッド(イベントループの1つ)でのみ使用する場合は問題ありません。
他の提案された解決策、Huazuo Gaoの答えからの_loop.call_soon_threadsafe
_と私の_asyncio.run_coroutine_threadsafe
_はこれを行っており、イベントループを起こしています。
Asyncio.Queueでthreading.Lockを使用するのはどうですか?
class ThreadSafeAsyncFuture(asyncio.Future):
""" asyncio.Future is not thread-safe
https://stackoverflow.com/questions/33000200/asyncio-wait-for-event-from-other-thread
"""
def set_result(self, result):
func = super().set_result
call = lambda: func(result)
self._loop.call_soon_threadsafe(call) # Warning: self._loop is undocumented
class ThreadSafeAsyncQueue(queue.Queue):
""" asyncio.Queue is not thread-safe, threading.Queue is not awaitable
works only with one putter to unlimited-size queue and with several getters
TODO: add maxsize limits
TODO: make put corouitine
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lock = threading.Lock()
self.loop = asyncio.get_event_loop()
self.waiters = []
def put(self, item):
with self.lock:
if self.waiters:
self.waiters.pop(0).set_result(item)
else:
super().put(item)
async def get(self):
with self.lock:
if not self.empty():
return super().get()
else:
fut = ThreadSafeAsyncFuture()
self.waiters.append(fut)
result = await fut
return result