この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは。サービスグループの武田です。
AWSのリソースを操作する方法としてマネジメントコンソールやAWS CLI、そして各プログラミング言語に用意されているSDKが利用できます。人気の高いPythonでは、Boto3というライブラリが提供されています。
さてそのBoto3ですが、基本的な使い方は次のような流れになります。
first_test.py
import boto3
session = boto3.Session()
client = session.client("sts")
r = client.get_caller_identity()
print(r)
- セッション作成
- 各サービス用のクライアント作成
- API呼び出し
通常であれば、これ以上言及することは特にないのですが、マルチスレッドと組み合わせる際に少し注意が必要です。一連の流れで作成しているSession
オブジェクト(および未登場ですがResource
オブジェクト)は スレッドセーフではありません 。これはドキュメントにも明記されています。
試しにエラーを発生させてみましょう。次のようなプログラムを用意してみます。
main.py
import concurrent.futures
import boto3
def task(session):
client = session.client("sts")
client.get_caller_identity()
session = boto3.Session()
with concurrent.futures.ThreadPoolExecutor() as executor:
a = executor.submit(task, session)
b = executor.submit(task, session)
a.result()
b.result()
実行すると次のようなエラーになります。
Traceback (most recent call last):
File "/python_boto3_multithread/main.py", line 16, in <module>
b.result()
File "/usr/local/Cellar/python@3.9/3.9.1_5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 433, in result
return self.__get_result()
File "/usr/local/Cellar/python@3.9/3.9.1_5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 389, in __get_result
raise self._exception
File "/usr/local/Cellar/python@3.9/3.9.1_5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/thread.py", line 52, in run
result = self.fn(*self.args, **self.kwargs)
File "/python_boto3_multithread/main.py", line 6, in task
client = session.client("sts")
File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/boto3/session.py", line 258, in client
return self._session.create_client(
File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/botocore/session.py", line 839, in create_client
credentials = self.get_credentials()
File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/botocore/session.py", line 441, in get_credentials
self._credentials = self._components.get_component(
File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/botocore/session.py", line 941, in get_component
del self._deferred[name]
KeyError: 'credential_provider'
問題の回避策
この問題を解決する一番簡単な方法は、ドキュメントにも書かれているように各スレッドでセッションを生成することです。
@@ -2,15 +2,15 @@
import boto3
-def task(session):
+def task():
+ session = boto3.Session()
client = session.client("sts")
client.get_caller_identity()
-session = boto3.Session()
with concurrent.futures.ThreadPoolExecutor() as executor:
- a = executor.submit(task, session)
- b = executor.submit(task, session)
+ a = executor.submit(task)
+ b = executor.submit(task)
a.result()
b.result()
これであればエラーは発生しません。
次のdeepcopyを使用した方法は、Python 3.10.9、boto3 1.26.51で dictionary changed size during iteration のエラーが出ることがあり、正常に動作しないことがあるようです。素直にスレッドごとにセッションを作成するのがお勧めです。
一方で、使用するSession
オブジェクトが一時クレデンシャルを利用する等複雑な構築をしている場合などはどうでしょうか。そのようなケースではSession
オブジェクトをコピーすることでエラーを回避できました。プログラムは次のようになります。
@@ -1,5 +1,6 @@
import concurrent.futures
import boto3
+import copy
def task(session):
@@ -9,8 +10,8 @@
session = boto3.Session()
with concurrent.futures.ThreadPoolExecutor() as executor:
- a = executor.submit(task, session)
- b = executor.submit(task, session)
+ a = executor.submit(task, copy.deepcopy(session))
+ b = executor.submit(task, copy.deepcopy(session))
a.result()
b.result()
copy.deepcopy()
がミソで、copy.copy()
(シャローコピー)だとダメでした。
まとめ
マルチスレッドプログラミングはスレッドセーフ性や排他制御、競合状態など特有の問題に対処しなければいけませんが、コストに対するリターンは大きいです。ぜひみなさんもたくさん落とし穴にはまって、いいシステムを作っていってください。