Pythonでウィンドウメッセージを受け取る(Windows API)

最近、Windows APIのウィンドウメッセージを取得しないといけないことがありました。
Windows APIなんか触ったことないし、Pythonでのウィンドウメッセージの取得の方法の記事なんか全然なくて、もうC++とか.NETなどを使うしかないかなと思ったのですが、どうにかPythonでできたので今回記事を書きました。(需要はほぼないと思いますが)

windowを作成し、ウィンドメッセージを処理する

コードは以下の通りです。コメントでコードの説明を入れましたのでご覧ください。
特別なことは何もやっておらず、ただウィンドウを作成してウィンドウメッセージを処理する関数を定義してあげただけです。
自分はここで作成したウィンドウハンドルを使って、APIのようなものに渡してあげて、そのAPIからメッセージを投げてもらって、メッセージを処理しました。

from ctypes import *
from ctypes.wintypes import *

# 関数プロトタイプ(CやC++の関数の宣言で使われるやつでインターフェイスを示すためのもの)
# 第一引数が戻り値の型、それ以降はPyWndProcedureの引数の型
WNDPROCTYPE = WINFUNCTYPE(c_int, HWND, c_uint, WPARAM, LPARAM)

# ウィンドウクラスの各項目を設定する際に使われる構造体
class WNDCLASSEX(Structure):
    _fields_ = [("cbSize", c_uint),
                ("style", c_uint),
                ("lpfnWndProc", WNDPROCTYPE),
                ("cbClsExtra", c_int),
                ("cbWndExtra", c_int),
                ("hInstance", HANDLE),
                ("hIcon", HANDLE),
                ("hCursor", HANDLE),
                ("hBrush", HANDLE),
                ("lpszMenuName", LPCWSTR),
                ("lpszClassName", LPCWSTR),
                ("hIconSm", HANDLE)]

class Window:

    WS_OVERLAPPEDWINDOW = 0xcf0000
    WS_CAPTION = 0xc00000

    SW_SHOW = 5

    CS_HREDRAW = 2
    CS_VREDRAW = 1

    CW_USEDEFAULT = 0x80000000

    WM_DESTROY = 2

    WHITE_BRUSH = 0

    def __init__(self, handle_data_callback):

        def PyWndProcedure(hWnd, Msg, wParam, lParam):
            # ここに受け取ったメッセージごとの処理を書く
            if Msg == self.WM_DESTROY:
                windll.user32.PostQuitMessage(0)
            # 特定のメッセージに対してやりたい処理をかませる
            elif Msg == 10000:
                handle_data_callback()
            # メッセージのデフォルトの処理は元々の処理系に任せる。
            return windll.user32.DefWindowProcW(hWnd, Msg, wParam, lParam)

        WndProc = WNDPROCTYPE(PyWndProcedure)
        # GetModuleHandleWは現在呼び出し元プロセスにロードされているexeやdllのメモリ上の位置を示すアドレスを返す
        hInst = windll.kernel32.GetModuleHandleW(0)
        wclassName = 'My Python Win32 Class'
        wname = 'My window'
        
        wndClass = WNDCLASSEX()
        wndClass.cbSize = sizeof(WNDCLASSEX)
        wndClass.style = self.CS_HREDRAW | self.CS_VREDRAW
        wndClass.lpfnWndProc = WndProc
        wndClass.cbClsExtra = 0
        wndClass.cbWndExtra = 0
        wndClass.hInstance = hInst
        wndClass.hIcon = 0
        wndClass.hCursor = 0
        wndClass.hBrush = windll.gdi32.GetStockObject(self.WHITE_BRUSH)
        wndClass.lpszMenuName = 0
        wndClass.lpszClassName = wclassName
        wndClass.hIconSm = 0
        
        # byrefは参照によってパラメータを渡すために使うためのもの(pointerと同じような働きだがこちらの方が高速)
        regRes = windll.user32.RegisterClassExW(byref(wndClass))
        
        # ウィンドウハンドルが返り値。ウィンドウハンドルとはコンピュータが各ウィンドウに割り振る管理番号のこと
        # ANSI文字列、Unicodeのどちらを使用するかという区別で関数名の最後にAかWがついている。(C言語はオーバーロードがないため別の関数になってるらしい) 今回はWで統一
        hWnd = windll.user32.CreateWindowExW(
        0,wclassName,wname,
        self.WS_OVERLAPPEDWINDOW | self.WS_CAPTION,
        self.CW_USEDEFAULT, self.CW_USEDEFAULT,
        300,300,0,0,hInst,0)
        
        
        if not hWnd:
            print('Failed to create window')
            exit(0)

        # ウィンドウの表示(表示せずただメッセージを受け取りたいだけの場合はコメントアウト)
        print('ShowWindow', windll.user32.ShowWindow(hWnd, SW_SHOW))
        print('UpdateWindow', windll.user32.UpdateWindow(hWnd))

        msg = MSG()
        lpmsg = pointer(msg)

        # GetMessageは呼び出し側スレッドのメッセージキューからメッセージを取得し、指定された構造体にそのメッセージを格納する。
        while windll.user32.GetMessageW(lpmsg, 0, 0, 0) != 0:
            windll.user32.TranslateMessage(lpmsg)
            windll.user32.DispatchMessageW(lpmsg)

def handle_message():
    print("メッセージ受け取ったよ!")

if __name__ == '__main__':
    win = Window(handle_message)

参照

https://stackoverflow.com/questions/5353883/python3-ctype-createwindowex-simple-example https://gist.github.com/mouseroot/6128651

Windows10 ProからUbuntu 20.04 LTEに乗り換えてみた

最近、趣味用PCをBTOで買ったのですが、Windows10なのでWSL2で立てたDockerからGPU使うのに色々セットアップしないといけなかったり、普段の仕事用のPCがMacなのでなんかWindows使いづらいということで、Windows10からUbuntuに乗り換えました。そこで今回参考にした記事や手順をまとめています。

下準備

Windows10ライセンスをデジタルライセンスに移行しておく

Windows10のライセンスをmicrosoftアカウントに紐付けてデジタルライセンスに移行することによって、また別のPCでWindowsを使いたいとなったときにライセンスを買うことなく同じMicrosoftアカウントを使用することによってWindows10を利用することができます。
もう一生Windowsなんか使わないという人以外は一応やっといた方が良いと思います。 詳しい移行手順はこちらをご覧ください。

Windows10 デジタルライセンスへの移行 | itcore 2019年

Windows10のライセンスを別のPCに移す。 | itcore 2019年

USBを用意する

他にデータが入っていない8GB以上のUSBメモリを用意してください。このあとのボータブルUSBを作成する過程でパーティションを作成して、Ubuntuインストール後パーティションし直すので詳しくない方は最悪壊れてもいいUSBを用意したほうがいいかもしれません。

Ubuntuのインストールメディアを作る

Ubuntu Desktop 20.04 LTSのインストール

以下のリンクから通常のUbuntu desktopか、もしくは日本語Remix版のUbuntu desktopをインストールしてください。
いろんな記事を見ると基本的に日本語Remix版の方が推奨されているようですが、余計なものが入っているのが嫌で、調べている限りでは通常版でも特に問題は起こらなそうだったので、自分は通常のUbuntuのほう(2021/4時点ではUbuntu Desktop 20.04.2.0 LTS)をインストールしました。

Ubuntuの入手 | Ubuntu Japanese Team

f:id:okiyasi:20210419211825p:plain
Ubuntu Desktop 20.04.2.0 LTSを選択
もしくは
f:id:okiyasi:20210419213810p:plain
Ubuntu 20.04.1 LTS - 2025年4月までサポ ートのubuntu-ja-20.04.1-desktop-amd64.iso(ISOイメージ)を選択

Rufusのダウンロード

以下のURLにてexeファイルをダウンロードします

Rufus - 起動可能なUSBドライブを簡単に作成できます

RufusUbuntuのブータブルUSBを作る

USBをPCに挿し、先程ダウンロードしたexeファイルをクリックしRufusを起動して、インストールしたUbuntuのisoファイルを選択します。 「スタート」を押すとインストールメディアが作成されます。

ポップアップが出た場合や手順がわからない場合は以下の記事を参考にしてください。 私はこの記事とはisoファイルを入れると自動で決まるフォーマットオプションのファイルシステムクラスターサイズが違いましたが特に問題ありませんでした。 diagnose-fix.com

Ubuntuの起動

Windowsの再起動からのUbuntuのインストール

Windows10でShiftを押しながらメニューの再起動ボタンを押すことで、再起動した時に「オプションの選択」というメニューが現れます。 「デバイスの使用」をクリックし、挿しているブータブルUSBのボタンを選択すると、Ubuntuのインストールが始まります。

セットアップ

Ubuntuが起動したら、ナビゲーションに沿って設定を行っていきます。 以下の記事に沿って設定していきました。GPUの設定のチェックはGPUがある方は忘れずにおこなってください。 これでUbuntuの起動は完了です。

linuxfan.info

後処理

USBメモリを元に戻す

ブータブルUSBを作るとUSBメモリ内はブート用パーティションで区切られてしまうため、元に戻すための作業を行います。 自分はUbuntuの「ディスク」から使用したUSBを選んで初期化し、パーティションを再設定しましたが調べた限りではあまりメジャーな方法ではないかもしれません。 Windowsを持っている方は以下の方法がよく試されているのでこちらを実行してみてください。

ブータブルUSBを元のUSBメモリに戻す - Qiita

終わりに

Ubuntuへの乗り換え方法の記事や手順を紹介しました。
操作していると偶にChrome等のアプリがフリーズしてちょっと不安定でしたが、もう一度アプリをインストールし直すと治ったので現状問題は起きてないです。また今回、日本語Remix版でない方をインストールしましたが、特に文字化けしたり、日本語入力ができないということには今のところなってません。問題がおこったら随時追記していきます。 まだUbuntuになれてないので便利さはそれほど感じてませんが、これから自分用にカスタマイズして便利にしていこうと思います。

Node.jsで動くMQTTブローカーモジュールのAedesを試す

最近、MQTTのプロトコルを使いイントラネット下においてIoT機器と通信してデータを取得し、electronを使って可視化を行うということをしました。
その時に、MQTTブローカーをelectronの使ってるNode.js上で立てれたら楽だな思ったので、Node.jsのモジュールを探し、moscaというMQTTブローカーモジュールを見つけました。
しかし、このmoscaは2018年以降メンテナンスされておらず、require('mosca')をするとmoscaの関連モジュールに起因するエラーが出る状態でした。
そこで、他に使えるモジュールがあるか調べていくとmoscaのgithubレポジトリのREADMEにしれっとPlease move to Aedesと書いてあったので、このAedesというモジュールを使うと無事にMQTTブローカーが立って通信できたので、今回そのAedesについて紹介していこうと思います。

github.com

MQTTの概用

おそらく、このページにたどりついてる方はMQTTについてよくご存知だとは思いますが、一応概用だけ説明させていただきます。
MQTTとはMessage Queue Telemetry Transportの略で、Publish/Subscribe型の軽量でシンプルなメッセージプロトコルです。
1対多や多対多で通信することができ、通信時における消費電力が少なく、スループットが高い点において、HTTPなどのプロトコルより優れています。そのため、IoTやM2Mの分野でよくMQTTが使用されています。 MQTTではPublisherSubscriberBrokerという3つの登場人物が存在します。
Publisherがメッセージを生成しBrokerに対して送信します。Brokerはそのメッセージを受け取りSubscriberに対して送信して仲介の役割を果たします。Subscriberはそのメッセージを受け取り、通信完了です。この時Subscriberは複数存在することが可能です。

その他のMQTTについての機能などに関してはこちらの記事がわかりやすかったので載せておきます。

iot-gym.com

Aedesについて

AedesはBrokerを立てるためのNode.jsのモジュールです。

以下のコマンドでイントールします。(※ Node.jsをあらかじめインストールしてください)

> npm install aedes

AedesでのMQTTブローカーの実装例

Aedesを組み込み実行するコードは以下の通りです。これだけでとても簡単にMQTTブローカーを立てることができます。

// モジュールの読み込み
const aedes = require('aedes')()
const server = require('net').createServer(aedes.handle)
const port = 1883

server.listen(port, function () {
  console.log('server started and listening on port ', port)
})

TSLを使って暗号化通信をしたい場合はこちらです。

const fs = require('fs')
const aedes = require('aedes')()
const port = 8883

const options = {
  key: fs.readFileSync('YOUR_PRIVATE_KEY_FILE.pem'),
  cert: fs.readFileSync('YOUR_PUBLIC_CERT_FILE.pem')
}

const server = require('tls').createServer(options, aedes.handle)

server.listen(port, function () {
  console.log('server started and listening on port ', port)

WebブラウザなどTCPではなくover WebSocketで通信したい場合は以下の実装で実現できます。

onst aedes = require('aedes')()
const httpServer = require('http').createServer()
const ws = require('websocket-stream')
const port = 8888

ws.createServer({ server: httpServer }, aedes.handle)

httpServer.listen(port, function () {
  console.log('websocket server listening on port ', port)
})

クラスター機能

Aedesにはクラスター機能があり、subscriptionのステータスを複数のブローカーで共有・同期することで、複数のブローカーを論理的には1つのブローカーとして機能させることを可能にしています。
このクラスター機能はRedisやMongoDBを使うことによって実現しており、追加で以下のライブラリを使用することによってクラスター機能を実現することができます。

終わりに

今回はNode.jsのブローカーモジュールであるAedesについて紹介しました。Aedesを使えば簡単に自分のPCでMQTTブローカーを立てることができます。 PublisherSubscriberなどのクライアントはAedesではなくMQTT.jsを使って実装します。そちらに関しては記事がたくさんあるので探してみてください。 今後もIoTに関しては市場が広がっていくと思うし、MQTTもまだまだ発展していく技術だと思うので注目していきたいですね。

Pythonにおけるyieldやジェネレータについて調べてみた

先日バックエンドの開発をしていてyieldを使うと便利な場面があり、他にも使えるところないかなと思って調べたので、ついでにyieldの周りの知識や使い所についてまとめてみました。

どんな場面で使えるか

yield(というよりはジェネレータ)の使える状況を簡単に説明すると、主に次の1点だと思われます。

何らかの状態(ステート)を保持する必要があるが、クラスを作るまでもない、すぐにいらなくなるようなものを保持しないといけない時

具体的なケースでいうと

  • メモリを圧迫するぐらい大容量のcsvファイルを一行ずつ扱いたい時
  • 無限データストリームの生成
  • 再帰処理

といったことに使うことができます。


yieldとは

英語で「産み出す」や「もたらす」という意味があります。
yieldは、関数においてreturnの代わりにyieldを使用することによってその関数をジェネレータ関数にすることができます。
またasync def 関数で使用するとそのコルーチン関数は非同期ジェネレータになります。(今回非同期ジェネレータの話は割愛します)

# ジェネレータ関数
def generator_func():
    yield 1
    yield 2
    yield 3

# 非同期ジェネレータ
async def AsyncGenerator(num: int):
    for item in range(num):
        await asyncio.sleep(2)
        yield item

ジェネレータという言葉が出てきました。 ジェネレータの説明が必要ですね。

ジェネレータとは

Pythonにおける「ジェネレータ」とはジェネレータ関数を指す場合と、ジェネレータイテレータを指す場合があります。(言葉の定義があいまい。。)

ジェネレータ関数とは

ジェネレータ関数イテレータを返す、特殊な種類の関数のことです。
ジェネレータ関数によって作成されたイテレータのことを特にジェネレータイテレータと呼びます。
ジェネレータ関数はイテレータを簡単に作るためのものと言われています。(それだけではないですが)

# ジェネレータ関数
def generator_func():
    yield 1
    yield 2

# ジェネレータイテレータ
generator_iter = generator_func()
print(type(generator_iter)) # <class 'generator'>
print('__iter__' in dir(generator_iter)) # True
print('__next__' in dir(generator_iter)) # True

# 組み込みメソッドnext()もしくは特殊メソッド.__next__()で値を取得
print(next(generator_iter)) # 1
print(generator_iter.__next__())  # 2
# 要素を出し切ってからnextするとStopIterationエラーがraiseされる
print(next(generator_iter)) # raise StopIteration

# リストにも変換可能
# generator_iterとgenerator_iter_2の内部状況は違いに独立
generator_iter_2 = generator_func()
print(list(generator_iter_2))  # [1, 2]
# ジェネレータは一度使うとリストのように複数回使うことは不可
print(list(generator_iter_2)) # []

イテレータとは

イテレータ(オブジェクト)は「データの流れを表現」するオブジェクトで、値を 1 つずつ返してくれるオブジェクトのことです。
簡単には自身を戻り値とする__iter__()と次の要素を返す __next__() の 2 つのメソッドを持っています。
イテレータはリストやタプルなどのイテレータブルから作ることもできますし、クラスからも作ることができます。
イテレータイテレータブルの違いはこちら(Python公式Docs)

# イテレータブル(リスト)からのイテレータの生成
tmp_list = [1, 2, 3, 4]
print(type(tmp_list)) # <class 'list'>
new_iter = iter(tmp_list)
print(type(new_iter)) # <class 'list_iterator'>
print('__iter__' in dir(new_iter)) # True
print('__next__' in dir(new_iter))  # True
# クラスでのイテレータの定義
class PowTwo:
    def __init__(self, max=0):
        self.n = 0
        self.max = max

    def __iter__(self):
        return self

    def __next__(self):
        if self.n > self.max:
            raise StopIteration

        result = 2 ** self.n
        self.n += 1
        return result

pow_two_iter = PowTwo(2)
print('__iter__' in dir(pow_two_iter)) # True
print('__next__' in dir(pow_two_iter)) # True
print(next(pow_two_iter)) # 1
print(next(pow_two_iter)) # 2
print(next(pow_two_iter)) # 4
print(next(pow_two_iter)) # raise StopIteration

上と同様のものをジェネレータで実装するととても簡潔です。

def PowTwoGen(max=0):
    n = 0
    while n <= max:
        yield 2 ** n
        n += 1

pow_two_gen = PowTwoGen(2)
print(type(pow_two_gen)) # <class 'generator'>
print('__iter__' in dir(pow_two_gen)) # True
print('__next__' in dir(pow_two_gen)) # True
print(next(pow_two_gen)) # 1
print(next(pow_two_gen)) # 2
print(next(pow_two_gen)) # 4
print(next(pow_two_gen)) # raise StopIteration

ジェネレータイテレータとは(別名:ジェネレータオブジェクト)

ジェネレータ関数によって作成されたイテレータのことで、イテレータと同様__iter__()と次の要素を返す __next__() の 2 つのメソッドを持っています。
しかし、通常のイテレータとは異なる特徴として、yield文までの実行された、局所実行状態を保存して、処理を一時的に中断することができます。(ローカル変数、プログラムカウンタや評価スタック、そして例外処理のを含むすべてのローカルの状態が保持されます。)
そしてまたジェネレータイテレータが再開されると、中断した位置を取得し実行を再開します。
これはコルーチンとよく似た機構です。

このようにジェネレータは必要に応じて、計算を止めたり、再開することで、リストのように全ての要素を保持するのではなく、一つずつの要素をその都度、計算し生成するのでメモリを節約することができます。

その他の知識

ジェネレータについての簡単な説明は終わりますが、その他にもPythonのジェネレータには色々な機構があるのでそちらを紹介します。興味のない方は使用例まで飛ばしていただいて大丈夫です。

ジェネレータとコルーチン

ジェネレータ(イテレータ)は、中断と実行をしながら一時的にローカル状態を保存しつつ、値を返していくという性質を持っていて、それがコルーチンの機構と似ているということでした。
しかし、コルーチンはただ値を返すことだけではなくコルーチンに対して値を送信する機構があります。
Pythonのジェネレータではコルーチンと同様に値を送信することができ、これをジェネレータベースのコルーチンと言います(他の言語ではジェネレータにこの機構は存在しないのでこの特別な名称がついていると思われます。)

ジェネレータベースのコルーチン

ジェネレータに値を送信する場合は、next() 関数ではなくsend()メソッドを利用します。

※現在、ジェネレータベースのコルーチンはPython 3.10で廃止予定で、代わりにネイティブコルーチンの使用が推奨されています。
https://docs.python.org/ja/3/library/asyncio-task.html#generator-based-coroutines

def sumup():
    n = 0
    while True:
        n += yield n


coroutine = sumup()
# next() 関数を実行して、ジェネレータを実行状態に遷移
print(next(coroutine)) # 0

print(coroutine.send(1)) # 1
print(coroutine.send(2)) # 3
print(coroutine.send(3)) # 6

coroutine.close()

値を渡すsendのほかにもPythonのジェネレータはthrowとcloseというコルーチン特有のメソッドを使うことができます。

yield from

yield fromPython 3.3から登場した構文で、ジェネレータを作る時のfor文をyield fromを使うことでより簡潔に書くことができます。

# for文を使う場合
def generator_func():
    for i in range(1, 3):
        yield i

# yield forを使用(上と等価)
def generator_func_2():
    yield from range(1, 3)

しかしyield fromは上の例のようにfor文のシンタックスシュガーとして機能するだけではありません。
yield from を使うことでジェネレータの入れ子を簡単に実現することができ、ジェネレータオブジェクトをそのまま結合したようなふるまいをさせることができます。

def generator_A():
    for n in 'ABCDE':
        yield n

def generator_B():
    for n in '12345678':
        yield n

def generator_all():
    # 他のジェネレータ関数で作られたジェネレータオブジェクトが
    # yield した値がそのままyieldされる
    yield from generator_A()
    yield from generator_B()

print(list(generator_A())) # ['A', 'B', 'C', 'D', 'E']

print(list(generator_B())) # ['1', '2', '3', '4', '5', '6', '7', '8']

print(list(generator_all())) # ['A', 'B', 'C', 'D', 'E', '1', '2', '3', '4', '5', '6', '7', '8']

ジェネレータ式

ジェネレータ式とは簡単なジェネレータを作る時に使うことのできる書き方で、yieldを使用することなくジェネレータを作成できます。

# 普通のジェネレータの生成方法
def sample_generator():
    for x in range(5):
        yield x * 2
# ジェネレータ式を使った場合
generator_formula = (x * 2 for x in range(5))

print(next(generator_formula)) # 0
print(next(generator_formula)) # 2

ジェネレータ式はよくリスト内包表記と比較されます。ジェネレータ式はジェネレータイテレータを生成するのでリストとは違い、必要に応じてだけ値を算出するので、無限長ストリームや膨大なデータを返すようなイテレータを扱うことができます。

# https://docs.python.org/ja/3/howto/functional.html#generator-expressions-and-list-comprehensions

# 大規模データ
line_list = ['  line 1\n', 'line 2  \n', ...]

# ジェネレータ式
stripped_iter = (line.strip() for line in line_list)

# リスト内包表記
stripped_list = [line.strip() for line in line_list]

使い所

ここから本格的に使い方について紹介していきます。

大容量データの処理

すでにジェネレータ式で大容量のテキストデータを扱う例を書きましたが、よく使われるのでもう一度例をあげておきます。

# 大容量のファイルの場合以下はMemoryErrorが発生する場合がある。
def csv_reader(file_name):
    file = open(file_name)
    result = file.read().split("\n")
    return result

# ジェネレータ関数での書き換えを行い一行ずつ処理することができる
def csv_reader(file_name):
    for row in open(file_name, "r"):
        yield row

# ジェネレータ式の場合
csv_gen = (row for row in open(file_name))

無限のデータストリームの生成

あまり使うことはないかもしれませんが、無限長のデータストリームを生成することができます。 以下の例では理論的には全ての偶数を生成することができます。

def all_even():
    n = 0
    while True:
        yield n
        n += 2

処理のパイプライン化

複数のジェネレータを使って処理をパイプライン化することができます。よくわからないと思うので具体的な例を示します。

フィボナッチ数列を生成するジェネレータと数字を2乗するための別のジェネレータがあるとします。 フィボナッチ数列の二乗和を求めたい場合は、ジェネレータ関数の処理をパイプライン化することで、以下のように求めることができます。

def fibonacci_numbers(nums):
    x, y = 0, 1
    for _ in range(nums):
        x, y = y, x+y
        yield x

def square(nums):
    for num in nums:
        yield num**2

print(sum(square(fibonacci_numbers(15)))) # 602070

パイプライン化することによってきちんと処理が明示的に分割された可読性の高いコードが書くことができます。

再帰処理

ジェネレータはリスト全体を保持することなく、逐次処理ができるので、再帰処理にも利用されます。
以下の例はジェネレータを再帰的に使用し、木の走査を行う例です。

class Node:
    def __init__(self,val):
        self.val = val
        self.left = None
        self.right = None

# ジェネレータを使った木の走査
def inorder(node):
    if node:
        for x in inorder(node.left):
            yield x
        yield t.val
        for x in inorder(node.right):
            yield x

yield fromを使用するとより簡潔になります。

def inorder(node):
    if node:
        yield from inorder(node.left):
        yield t.val
        yield from inorder(node.right):

終わりに

コルーチンやら、非同期やらジェネレータとは関係なさそうな言葉が色々出てきました。言葉の定義の境目があいまいなものが多かったので、正確でない部分があるかもしれません。間違っていましたらフィードバックお願いします。 今回出てきた非同期ジェネレータなど非同期処理に関する内容が出てきたので、また自分の知識の整理のためにも非同期についての記事を書こうと思います。

参照一覧

https://www.lifewithpython.com/2015/11/python-create-iterator-protocol-class.html
https://docs.python.org/ja/3/howto/functional.html#generator-expressions-and-list-comprehensions
https://postd.cc/python-generators-coroutines-native-coroutines-and-async-await/
https://qiita.com/koshigoe/items/054383a89bd51d099f10
https://qiita.com/keitakurita/items/5a31b902db6adfa45a70#%E5%86%8D%E5%B8%B0
https://www.programiz.com/python-programming/generator

AWS ソリューションアーキテクト-プロフェッショナルに合格したのでその勉強方法まとめ

f:id:okiyasi:20210218150304p:plain

AWS ソリューションアーキテクト-プロフェッショナルに合格したので備忘録として勉強方法をまとめます。
(※勉強にかまけてブログの記事の作成をサボってました)

私の経歴

社会人3年目のWebエンジニアで、フロントからサーバーサイドからインフラまでなんでもやりますエンジニアやってます。

  • サーバーサイドはもうすぐ3年
  • フロントエンドは2年弱ぐらい
  • インフラは基本AWSを使っていて、業務でも設計・運用をやっている。(運用歴は1年ちょっと)
  • AWS Solution Architect Associateは1年半ぐらい前に取得済み
  • 主要サービスは大体使ってる(EC2はあんまり使ってない)

試験の概用

AWSの認定試験の中で一番難しいと言われてる試験で実際結構難しかったです。
試験時間は180分 試験の料金は3万円です。 いつでも何回でも受けるのは良いんですが、試験の料金が高いので、しっかり準備してやらないと3万吹っ飛ぶという、金銭的なリスクがあります。
※記事を書いてて気づいたのですが、そういえばアソシエイト合格の時に50%のバウチャーもらってたのに完全に使うの忘れてました。(死)

最近では家でも試験が受けられるようになったらしいですが、自分は子供に邪魔される未来しか見えなかったので素直にテストセンターで受けました。
テストセンターの中でも遠隔でオンライン監督がみるやつは自分はアソシエイトの時の苦い経験があるので(英語は問題なかったのですが、会場のカメラのピントが合わなくて証明書がちゃんと見れないとかなんとかで20分ぐらい、カメラにパスポートを掲げてました)、 テストセンターの受付で身分証明書を見てくれる場所で受験しました。

私の試験結果

f:id:okiyasi:20210218145006p:plain
750点で合格で873点取れてたので、割と安全に受かったぽいです。
また5つの分野で問題が構成されていて、一応全部の分野が合格ラインでした。
でも試験中は受かった確信は全くなかったです。

勉強期間について

勉強時間は4ヶ月半ぐらいで 基本的に3~5時間ほど土日に勉強して、平日は気が向いたらblackbeltの動画を視聴してました。
トータルの勉強時間は130時間ほどじゃないかと思います。
※途中、なんでAWSばっかり勉強してるんだと虚無を感じて半月ぐらい何もしてなかった時期もありました。。

効率の良い人は2ヶ月半から3ヶ月ぐらいでも受かるかと思います。

勉強する意義

普段からAWSを運用している人からしたら、勉強する過程で実際の運用におけるよくあるハマりどころが知れたり。(VPC内に配置したLambdaはそのままだとインターネットを介して通信できないとか ※勉強する前にハマってたけどね)
もっとこのサービスに置き換えればコスト抑えられたり、効率的じゃんみたいなことが発見できたので割と個人的には有意義だったんではないかと思います。

もちろん、資格を取ることによって報奨金もらえたりする会社もあるので(前職はそうだったけど、今の会社はもらえない笑)勉強する意義は人それぞれありそうです。

試験の難しさについて

この試験には一般の試験とは違う、独特な難しさがあります。

  • まとまった情報が少ない(後述の本が出たので比較的マシになりました)
  • 単純に勉強範囲が大きすぎるので使ったことのないサービスは細かいとこまで覚えてないし、マイナーサービスは調べてもそんなに情報が出てこない
  • 選択肢がどれもほぼ正解だが、その中からベストを選ばないといけないという正解の確信がない問題が多い
  • 問題文・選択肢が割と長文で、ちょっと日本語がおかしい問題が75問もあり、3時間ギリギリかかる
  • 複数選択肢選ぶ時に「または」の選択肢を選べば良いのか「かつ」の選択肢を選べば良いのかわかりにくい

というような、試験中に問題を解いていても「本当に受かるかな」と不安になるような感じでした。
確実に取れてるだろうと思った問題は75問中20問ぐらいです。 最近の他の合格記見てても同じ感じなのでみんな同じぐらいだと思います。

おすすめの勉強方法

まずはこちらのトレーニングでざっくりとした試験の全体像を掴みます。

AWS training and certification

そして次に、去年出たこの有名な本を解いていきます。 www.amazon.co.jp

この本は割と解説がしっかりしてるのでわかりやすいのですが、メジャーだけどあまり使ったことのないサービス(Glue,Config,System Manager等) の全体像は掴みづらいので
下のBlackBeltのサービス別資料の動画などを見て理解して、自分なりにそれぞれのサービスについてのまとめを作っていきました。

aws.amazon.com

そして本の後ろの模擬試験を解きます。 問題の演習量が少ないと思ったら、Udemyの問題集を課金して解きます。(一回目の点数は大体50%ぐらいでした。。) 自分は通しで解くのは見直しが大変すぎて嫌になったので15問ずつぐらいで区切って解いてました。

www.udemy.com

基本的には問題を解きながら気になったところを調べて、ホワイトペーパーだったりクラスメソッドさんのDevelopers.IOの記事をまとめながら理解する作業を繰り返します。(マイナーサービスはホワイトペーパーの訳がとてもひどく全然頭に入ってこないので、クラスメソッドさんが救世主でした。)
udemyやって忘れたころに本の模擬試験やると良い感じでした。(本の問題の方が本番に生きる問題は多かったように感じます。)

本の模擬試験やudemyの練習問題を2回目解いて大体75~90%を取れるようになったらほぼ受かるのではないかと思います。(ただ、問題集に出てくる問題で本番とほぼ同じような問題は20問ぐらいしか出てこなかった印象なので油断は禁物。)

試験の解き方

個人的には以下の解き方でやっていきました。

  1. まず、問題文を割としっかりに読む。問題文の最後の方にどのような選択肢を選べば良いのか書いてあるのでそこに注目
    可用性なのか、コストなのか、スピードなのか
  2. 全ての選択肢をざっくり読んで違いを見つける 単純に使用しているサービスが違うのか、どこに注目
  3. なんとなく合ってそうな選択肢を一つ読んでみて、内容を理解する
  4. 選択肢を比較しながら問題文に合った選択肢を選択する
  5. 書いてる内容が複雑でわからない問題や日本語がとても怪しい問題はもらえる紙に番号だけ書いてあとで見直せるようにしておく
  6. 25問50分のペースで解いていき、30分ほど余らせる。
  7. わからなかった問題で日本語が怪しい問題を英語で見てみる (環境によっては切り替えにやたらと時間がかかる場合があるので一通り解いて見直しの時にやる方がおすすめ)
    自分の時は日本語訳がひどいやつが2問あって、英語でみたら一瞬で解けたりしました(選択肢1の訳になぜか選択肢2の訳が混じってたりしました笑)

合格したら

この記事の一番上にあるAWSのバッチがもらえます。特に使えるところもないのでこの記事に貼らせていただきました。 あと、また50%offバウチャーもらえました。

最後に・あとがき

AWS認定プログラムの規約で試験の内容についてはあんまり言ってはいけないので、突っ込んだ話はできなかったですが、以上で自分の体験談は終わりです。
次はAWSみたいな1サービスの勉強じゃなくて、もっと汎用的なものの勉強をしたいので、IPAデータベーススペシャリストを取ろうと思いますが、如何せんまだ8ヶ月あるので、 AWS機械学習スペシャリストでも取ろうかなと思ってます(一応AI系のサービス作ってるし、50%offバウチャー2枚余ってるので)


参考

なんとなく問題集を解きながらAWSサービスの構造を理解する上でわかりやすかったドキュメントたちを置いていきます。

マルチアカウント

https://d0.awsstatic.com/events/jp/2017/summit/slide/D4T2-2.pdf
「組織の複雑さに対応する設計」でマルチアカウントの問題はよく出てくるし、おそらくそこまで大規模組織の運用を経験したことある人はいないと思うのでこれは一通り目を通した方が良いと思います。

APIGatewayのREST APIとHTTP APIの違い

APIGatewayはREST APIしかなかった頃に触ったことある程度で、RESTってあのRESTだよな?と思いながら一体何が違うねんとなったのでこの記事ありがたかったです。(名称が機能と一致してない気が。。)
最近はHTTP APIも機能が強化されてるみたいで、HTTP APIを使えることが多くなったそうですね。
Amazon API Gatewayは「HTTP API」と「REST API」のどちらを選択すれば良いのか? #reinvent | DevelopersIO

DynamoDBのグローバルセカンダリインデックスとローカルセカンダリインデックス

Dynamoの基本です。でも、理解しにくいのでとても参考になりました DynamoDBの概要 - Qiita

AWS公式

自分はEC2じゃなくてECSのFargateでばっかり運用してるのでEC2の運用をサポートしてるサービスはあんまり触れてなかったし、System Managerは機能が多すぎてわけわからないので役立ちました。
BlackBelt AWS Config
20190618 AWS Black Belt Online Seminar AWS Config
BlackBelt AWS System Manager
https://www.youtube.com/watch?v=UXSbh4Wsp7c&feature=youtu.be
サポートされないVPCピアリング(マイナー問題ですが、問題集で図なしで解説されても全くなんのこっちゃわからなかったのでこの図で理解しました。)
サポートされていない VPC ピア接続設定 - Amazon Virtual Private Cloud
あとはAI系のサービスやConnectとかAppStream2.0とかほぼ使わないマイナー系のサービスはまじで印象なさすぎて覚えられないのでBlackBeltの動画をみて無理矢理頭の中に入れました。

Blobって一体何者?使い方まとめ(JavaScript/TypeScript)

フロントでファイルを扱おうとして、JavaScriptやTypescriptを書いているとnew Blobしたり型でBlobを書いたりすることが必要になったりするのですが、このBlobについてあまりよくついて知らないなと思ったので今回調べてみてみることにしました。

Blobとは

BlobとはBinary Large OBjectの略で、単にバイナリデータの塊を表現したものです。
ウェブブラウザ(WEB API)ではデータを保持する役割を担うBlobクラスが実装されています。
BlobにはWEB APIFileが継承されていて、プロパティにはデータサイズやMIMEタイプを持っています。

ちなみに他にもJavaScriptにはバイナリデータを扱うクラスが用意されていてArrayBuffer / TypedArrays(型付き配列)などがありますが、ArrayBuffer / TypedArraysは主に直接操作できるバイナリデータを扱うのに用いられるのに対し、Blobはイミュータブル(不変)なバイナリデータを扱います。
そのためこのBlobのバイナリデータは、File APIを介してのみアクセスされることが想定されています。

Blobの作成・Blobのコンテンツの読み込み

new Blob(source, option)でBlobオブジェクトを生成します。
sourceにはテキストやバイナリのデータを指定することができ、optionにはMIMEタイプを指定します。

stackblitz.com

Blobの読み込みにはFileReaderを用います。FileReaderはFile APIの機能の一つです。(File APIHTML5の機能) FileReaderを使用することで、ユーザーのPCに保存されているファイル(またはデータバッファ)のコンテンツを非同期に読み取ることができます。 ここでは読み込みで使用していますが、ファイルのアップロードでも使用可能です。

Blob URL Scheme

バイナリデータを保持するURLの一種で、BlobからBlob URL Schemeに変換することが可能です。
URL.createObjectURL()を使用することで簡単に変換できます。
Blob URL Schemeにおいてバイナリデータ自身はBlob URL Schemeの文字列に埋め込まれているのではなく、ブラウザで保持されています。
そのため、バイナリデータをHTMLへ直接埋め込むようなことはできませんが、大容量のバイナリデータを扱うことが可能です。

用途としてはURLなのでaタグに指定してファイルをダウンロードさせたり、imgタグに指定して画像表示したりすることができます。以下がその例です。

stackblitz.com

blob:https://js-thmf3i.stackblitz.io/a3f2610b-3462-404f-af65-471f6dc73743

Blob URL Schemeでは先頭にblob:がついていて、https://js-thmf3i.stackblitz.io/の後の文字列部分にはUUIDのようなものしか記述されていないことが分かります。

比較対象としてのData URL Scheme(おまけ)

よくBlob URL Schemeと比較されるのがData URL Schemeです。
blob:ではなくdata:が文字列の先頭に付きます。
こちらはBlob URL Schemeと同様にimgタグのsrcに指定することが可能ですが、Blob URL Schemeと違い直接バイナリデータをHTMLに埋め込むことが可能です。
バイナリデータをBase64の変換方式で文字列に変換しています。 そうすることによって、通常別データに別れている画像などのデータを一度の通信で取得できるメリットがあります。
しかし、

  • キャッシュされないためリロードするたび画像データを読み直さないといけない
  • 100Mを超えるようなサイズだと、非常に処理が重たい
  • HTMLにとても長い文字列を記述しないといけない

などの結構なデメリットがあり、あまり使われてはいないようです。

以下がData URL Schemeの例です。

<!-- 赤い小さな点の画像を表示するだけのData URL Scheme -->
<img src="data:image/png;base64,iVBORw0KGgoAAA
ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU
5ErkJggg==" alt="Red dot" />

終わりに

今回はBlobについて少し掘り下げてみました。バイナリデータの扱いに関しては普段コードを書いていてそこまで意識することがないので、また記事を書いて深堀りしていきたいです。

参考

Blob - Web API | MDN

What are Blobs used for in JavaScript? | by <Andrew Rymaruk /> | JavaScript in Plain English

ワクガンス | JavaScriptによるファイルとバイナリデータの扱い

Data URI scheme - Wikipedia

Pythonにおける並行・並列処理について調べてみた

f:id:okiyasi:20200922031123p:plain 開発をしていると要求されている処理時間より時間がかかってしまうことがあり、処理を高速化しないといけない場面に遭遇すると思います。

その場合は、まずボトルネックとなっている処理を探し、ボトルネックとなっている場所のアルゴリズムやデータ構造の改善やライブラリの使用による処理の代替を行います。

しかし、それだけでは高速化できななかったり、もっと高速化しないと要件を満たせないことがあります。その時に一つの高速化の手段として並列処理や並行処理があります。 今回はその並列処理・並行処理について解説していきたいと思います。

並行処理と並列処理とは

まずは、並行処理と並列処理の定義です。

並行処理と並列処理は似たような言葉ですが違いがあります。細かな定義は人によってまちまちですが、以下のような違いがあります。

並行処理

システムが複数の動作を同時に実行状態に保てる状態にあること
わかりにくいので端的に言うと、2 つ以上のタスクが存在する時に、複数のタスク処理を高速に切り替えてあたかも同時に処理されているかのように見せることです。

並列処理

2 つ以上のタスクが存在する時、それらを実際に同時に処理すること。


これらの並列処理や並行処理を利用して、プログラムを高速化しますが、ここでボトルネックなっている処理の種類が重要となってきます。

ボトルネックとなる処理の種類

ボトルネックとなる処理は主に以下の2つの種類に分けることができます。

CPUバウンドな処理

数値計算のようにCPUに負荷をかけるようなような処理のことで、処理速度がCPUの計算速度に依存する処理です。

I/Oバウンドな処理

ファイルの読み書き、ネットワーク通信、DBへの接続などの処理のことで、ディスクの読み出しなど、主にCPUとは関係ないI/O部分に負荷がかかる処理のことです。


CPUバウンドな処理は並列処理により、複数のタスクを同時に処理することによって高速化することができます。(※並行処理では高速化できません。)

一方で、I/Oバウンドな処理はCPUはディスクの処理(I/O処理)が終わるまで待ち状態になります。そこでI/Oバウンドな処理では、並行処理により、その待ち時間の間にCPUが他のタスクをこなすことで高速化を実現します。(もちろん並列処理でも高速化できます。)

並列処理・並行処理を行う際には複数のタスク処理を実行する・実行できる状態にすることが必要になりますが、このタスク処理を行ってくれるものについて理解する必要があります。それがプロセススレッドです。

プロセスとスレッド

プロセス

プロセスとは、OSが実行しているプログラム(のインスタンス)のことです。

スレッド

スレッドとは、プロセス内の「実行単位」のことで、このスレッドを使用して、CPUのコアに命令を与えて計算処理を行っています。


おそらく、ざっくり過ぎてなんのことを言っているのかわからないかもしれないので、詳しくは こちらの記事を読んでいただけると幸いです。

このスレッドやプロセスを複数用意し、並列的に同時に動かすことによって、並列処理を実現し高速化します。

GoやJavaなどの言語ではメモリの節約の観点などから、プロセスよりも、スレッドを複数用意して並列処理を行うことで高速化を実現するのですが、Pythonの場合は少し勝手が違います。

Pythonにおけるマルチスレッド

マルチスレッドで処理を行う場合は基本的にスレッドセーフである必要があります。スレッドセーフとは複数のスレッドを並列的に使用しても問題が発生しないことを意味します。

具体的にはスレッドが複数存在していても共有しているデータに対してが一度に1つのスレッドのみがアクセスするようにしておくことで、処理中に他のスレッドにデータを上書きされたりするのを防ぎます。

しかし、Python(正確にはPythonの中でC言語で実装されている部分CPython)はスレッドセーフではありません

そこでPythonGIL(Global Interpreter lock)という排他ロックを使用することによってこの問題を回避しています。GILにより、Pythonインタプリタのプロセスは1スレッドしか実行できません。

つまり1つのプロセスに複数スレッドが存在してもロックを持つ単一スレッドでしかコードが実行できずに、その他のスレッドは待機状態になります。

そのためPythonにおけるマルチスレッドでは並列処理ではなく並行処理になってしまうため、CPUバウンドな処理ではマルチスレッドでは高速化が期待できません。むしろロックの切り替えのために遅くなる場合があります。

CPUバウンドな処理での速度の比較

本当にCPUバウンドな処理でマルチスレッドで処理を行っても速度が変わらないのか検証するために、大きな数値に対してfizz_buzzを行うCPUバウンドな処理のサンプルプログラムを用意します。 リストの中の5つの大きな値について1つずつ逐次的に、1からその数値までfizz_buzzしていき、最後にどのくらいの時間がかかったのかを出力しています。

逐次実行のサンプルプログラム

用意した5つ大きな数字に対してfizz_buzzを逐次的に行っています。

import time

def fizz_buzz(num: int):
    result_list = []
    for i in range(1, num + 1):
        result = ''
        if i % 3 == 0:
            result += 'fizz'
        if i % 5 == 0:
            result += 'buzz'
        if not result:
            result = str(i)
        result_list.append(result)
    return result_list


start = time.time()
num_list = [22000000, 19000000, 25000000, 24500000, 21300000]
for n in num_list:
    fizz_buzz(n)
stop = time.time()
print(f'Sequential processing: {stop - start:.3f} seconds')

実行結果
f:id:okiyasi:20200922005229p:plain

マルチスレッドによる実装

マルチスレッドを実現するための主にthread・threading・concurrent.futuresという3つのPythonのモジュールがあります。 基本的にはPython3ではthreadingかconcurrent.futuresを使います。今回は単純にマルチスレッドを実現したいだけなので一旦threadingを使用します。

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, num):
        super().__init__()
        self.__num = num

    def fizz_buzz(self, num: int):
        result_list = []
        for i in range(1, num + 1):
            result = ''
            if i % 3 == 0:
                result += 'fizz'
            if i % 5 == 0:
                result += 'buzz'
            if not result:
                result = str(i)
            result_list.append(result)
        return result_list

    def run(self):
        self.fizz_buzz(self.__num)


start = time.time()
threads = []
num_list = [22000000, 19000000, 25000000, 24500000, 21300000]
for n in num_list:
    thread = MyThread(n)
    thread.start()
    threads.append(thread)
for th in threads:
    th.join()
stop = time.time()
print(f'multi threads: {stop - start:.3f} seconds')

実行結果
f:id:okiyasi:20200922005357p:plain

全然早くなっていません。 むしろ、逐次的に実行した時よりも少しマルチスレッドで実装した場合の方がロックの切り替え分、2秒程度時間がかかっているのがわかります。

マルチプロセスによる実装

マルチプロセスにはGIL制約などは存在しないので、並行処理ではなく並列処理になります。 マルチプロセスにはmultiprocessingと先ほどスレッドでも登場したconcurrent.futuresのどちらかのモジュールを使用しますが、今回はmultiprocessingを使用します。

from multiprocessing import Process
import time

class MyProcessor(Process):

    def __init__(self, num):
        super().__init__()
        self.__num = num

    def fizz_buzz(self, num: int):
        result_list = []
        for i in range(1, num + 1):
            result = ''
            if i % 3 == 0:
                result += 'fizz'
            if i % 5 == 0:
                result += 'buzz'
            if not result:
                result = str(i)
            result_list.append(result)
        return result_list

    def run(self):
        self.fizz_buzz(self.__num)


start = time.time()
processes = []
num_list = [22000000, 19000000, 25000000, 24500000, 21300000]
for n in num_list:
    process = MyProcessor(n)
    process.start()
    processes.append(process)
for p in processes:
    p.join()
stop = time.time()
print(f'multi process: {stop - start:.3f} seconds')

実行結果
f:id:okiyasi:20200922005430p:plain

スレッドや逐次的に処理した場合よりもかなり早くなっていることがわかります。 CPUバウンドな処理に関してはマルチプロセスを使用することで高速化できることがわかりました。

I/Oバウンドな処理の速度比較

今度はI/Oバウンドな処理に対して速度を比較していきます。

逐次実行のサンプルプログラム

サンプルプログラムとしてwebページからコンテンツをダウンロードするプログラムを用意します。

import requests
import time

def download_site(url, session):
    with session.get(url) as response:
        count_list = []
        for i in range(len(response.content)):
            count_list.append(i)


def download_all_sites(sites):
    with requests.Session() as session:
        for url in sites:
            download_site(url, session)


sites = [
    "https://www.jython.org",
    "http://olympus.realpython.org/dice",
] * 80
start = time.time()
download_all_sites(sites)
stop = time.time()
print(f"Sequential processing: {stop - start:.3f} seconds")

実行結果
f:id:okiyasi:20200922023123p:plain

マルチスレッドによる実装

前回はCPUバウンドの時はthreadingを使用しましたが、今回はconcurrent.futuresを使用します。(そこまで変わらないと思いますがconcurrent.futuresの方がメジャーらしいです) ThreadPoolというスレッド数を一定に保ちながらスレッドを使い回してくれる機能を使います。スレッド数は5を指定しています。

import threading
import concurrent.futures
import requests
import time

thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        len(response.content)


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


sites = [
    "https://www.jython.org",
    "http://olympus.realpython.org/dice",
] * 80
start = time.time()
download_all_sites(sites)
stop = time.time()
print(f"multi threads: {stop - start:.3f} seconds")

実行結果
f:id:okiyasi:20200922023106p:plain

今回はCPUバウンドの処理とは違い、格段に処理が高速化されています。スレッド数分だけ約5倍程度早くなっています。

これはI/O処理自体はGILの制約を受けないので処理が並列化されていて、尚且つCPU処理も並行化によってI/O待ち時間に次のCPU処理が行われているため早くなっています。(主にI/O処理の並列化が寄与しています)

マルチプロセスによる実装

マルチスレッド時のスレッド数と揃えるためにプロセスの数を5にしています。デフォルトではos.cup_count()の数だけProcessが起動されます。

import requests
import time
import multiprocessing

session = None

def set_global_session():
    global session
    if not session:
        session = requests.Session()


def download_site(url):
    with session.get(url) as response:
        multiprocessing.current_process().name
        len(response.content)


def download_all_sites(sites):
    with multiprocessing.Pool(processes=5, initializer=set_global_session) as pool:
        pool.map(download_site, sites)


sites = [
    "https://www.jython.org",
    "http://olympus.realpython.org/dice",
] * 80
start = time.time()
download_all_sites(sites)
stop = time.time()
print(f"multi process: {stop - start:.3f} seconds")

実行結果
f:id:okiyasi:20200922030125p:plain

マルチスレッドの時よりも速度がさらに早いという結果になりました。 これは単純にプロセスが増えたことによってCPU処理もI/O処理も両方とも並列化されるために、マルチスレッドの時よりCPU処理時間分だけ早くなったと予想されます。(スレッド数とプロセス数を同じにして比較することがナンセンス感ありますが)

マルチプロセス・マルチスレッドの問題点

今回はPythonにおける並列処理・並行処理を実現するマルチプロセス・マルチスレッドについてみていきました。

しかしマルチスレッドやマルチプロセスについても問題点があります。

それは、プロセス数やスレッド数が増えすぎると、メモリを食いつぶしたり、コンテキストスイッチするコストが増大してサーバがパンクしてしまうという問題です。(いわゆるC10K問題)

そのため、マルチスレッドやマルチプロセスを使わずに、シングルスレッドでも多くの処理を捌く必要が出てきました。

そこで登場してきたのが非同期処理です。非同期処理とはタスクを止めず(ブロックせず)に、別のタスクを実行する手法のことで、非同期処理によってより多くのI/O処理を捌くことが可能になりました。Pythonにおいてはasyncioが非同期処理モジュールとして有名です。

今回は非同期処理については書きませんが、近々、非同期処理についての記事を書こうと思います。

参考

実践Python
【図解】CPUのコアとスレッドとプロセスの違い・関係性、同時マルチスレッディング、コンテキストスイッチについて
Speed Up Your Python Program With Concurrency
Pythonをとりまく並行/非同期の話