Python: マルチプロセスで並列処理をさせるには multiprocessing.Pool が超便利
独立した並列処理を高速化したい
ふつう並列処理といえば、GUI と処理を切り離すなどが思い当たります。
GUI と処理を切り離すといっても、相互にメッセージなどをやり取りして協調動作する必要があります。
今回やりたいのは、そういう協調動作を必要としない、独立した並列処理です。
たとえば、大量のファイルをすべて別個に圧縮するという場合、それぞれのファイルは独立に処理することができます。
これはスレッドを分けても意味が無いので、プロセスを分けて処理する必要があります。
こんなときに、Python の multiprocessing.Pool がとても便利だったので記録として残しておきます。
課題
20 個の処理をマルチプロセスで並列処理したいとします。
CPU がクアッドコアであれば、4 個を同時に走らせることができます。
普通に考えると、プロセスの終了状況を監視して、終了したら次のタスクを割り当てるという流れが必要になります。
実際にこういう方法でプログラムを書いていたのですが、これはけっこう面倒でミスをしやすいプログラムでした。
multiprocessing モジュールの Pool クラスは、なんとこれを 1 行でやってくれます。
はじめから知っておきたかった...
サンプルプログラム
1 秒かけて与えられた数字の 2 乗を返すだけのサンプルプログラムです。
数字は range(20) で与えるようにしていますが、ここはリストなどでも大丈夫です。
出力結果は以下のようになります。
param 0 is being processed param 2 is being processed param 4 is being processed param 6 is being processed param 1 is being processed param 3 is being processed param 5 is being processed param 7 is being processed param 8 is being processed param 10 is being processed param 12 is being processed param 14 is being processed param 9 is being processed param 11 is being processed param 13 is being processed param 15 is being processed param 16 is being processed param 18 is being processed param 17 is being processed param 19 is being processed [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]
プロセスを並列で走らせ、返り値は順番どおりに返してくれる、ほんとに便利なモジュールです!
注意点としては、Pool.map は range(20) の値を 0 から順番に与えるわけではない、ということです。
出力結果を見ると、0,2,4,6 が最初に来ていることがわかります。
この順番を 1 ずつにしたい!という場合は、Pool.map にもう一つ引数を追加する必要があります。
results = pool.map(parafunc, range(20), 1)
この最後の引数 "1" は chunksize と呼ばれる引数で、まとまりを意味するようです。
これを 1 にすると 0,1,2,3,...、2 だと 0,2,4,6,...、3 なら 0,3,6,9... という順番で値が渡されます。
なお、ここを変えたとしても、渡される結果の順番は同一なので安心です。
Raspberry pi を NAT ルーター兼 DHCP + DNS サーバーにしたい
やっぱり外にもつなぎたい...
前回の記事で、クローズネットワーク内に Raspberry pi を置いて、DHCP + DNS サーバーにしました。
当然ながらクローズネットワークなので、外には出られません。
しかし開発をしていると、どうしても外につなぎたいことがあります。
pip とか apt-get とか...
そのときにいちいち回線をつなぎ直すのは面倒です。
なので、前回使った Raspberry pi に NAT ルーターの機能を追加することにしました。
ネットワークの完成イメージ
すでに外につながる PPPoE ルーターがあり、その配下に Raspberry pi を置くことにします。
そのまた下にクローズネットワークを構築します。
このとき、Raspberry pi 配下にある端末へのアクセスに対しては Raspberry pi が DNS サーバーとして動作し、
そうでない端末 (google.com とか) には、Google DNS (8.8.8.8) を使うことにします。
必要な部品
Raspberry pi は Ethernet の口が一つしかありません。
しかし上記のようなネットワークを組むには、LAN 側と WAN 側の 2 つの Ethernet が必要になります。
そこで、WAN 側には USB-Ethernet 変換を使うことにしました。
下記の製品であれば、ドライバーがすでに Raspbian の中に入っているので、これにしました。
BUFFALO Giga USB3.0対応 有線LANアダプター【 Nintendo Switch 動作確認済 】LUA4-U3-AGT
- 出版社/メーカー: バッファロー
- 発売日: 2014/06/25
- メディア: Personal Computers
- この商品を含むブログ (2件) を見る
自分の環境では、USB ポートにさすだけで eth1 として認識されました。
Raspberry pi の設定
それでは Raspberry pi の設定をします。
前回設定した Dnsmasq の一部書き換えと、IP 転送の設定をします。
Dnsmasq の書き換え
一行だけ書き換えれば OK です。
DHCP でアドレスを配るときに、アクセスすべき DNS サーバーのアドレスを追加するようにします。
/etc/dnsmasq.more.conf を開いて、前回の記事では以下のようになっているところを...
# DHCPクライアントに通知するDNSサーバのIPアドレス dhcp-option = option:dns-server, 192.168.0.1
192.168.0.1 のあとに 8.8.8.8 を追加するだけです。
# DHCPクライアントに通知するDNSサーバのIPアドレス dhcp-option = option:dns-server, 192.168.0.1, 8.8.8.8
そして Dnsmasq サービスを再起動しましょう。
$ sudo service dnsmasq restart
IP 転送の許可
Stack overflow の回答を参考にしました。
まずは /etc/sysctl.conf を開きます。
$ sudo emacs /etc/sysctl.conf
この中で、#net.ipv4.ip_forward=1 となっているところがあるので、その # を消します。
# Uncomment the next line to enable packet forwarding for IPv4 net.ipv4.ip_forward=1 # <-- コメントアウトを解除する
次に、以下のように IPv4 の転送を有効にします。
$ sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
iptables の設定
最後に iptables の設定をします。
なお以下に記載する設定は、eth0 がクローズネットワーク側、eth1 が外側なので、ご留意ください。
まず iptables に残っているエントリを全部消去します。
$ sudo iptables -F $ sudo iptables -X $ sudo iptables -t nat -F $ sudo iptables -t nat -X $ sudo iptables -t mangle -F $ sudo iptables -t mangle -X $ sudo iptables -P INPUT ACCEPT $ sudo iptables -P FORWARD ACCEPT $ sudo iptables -P OUTPUT ACCEPT
次に eth0 - eth1 間の通信を有効にします。
$ sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT $ sudo iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT $ sudo iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
これで、外側への通信ができるようになったはずです。
このままではリブートしたあと設定が消えてしまうので、この設定をセーブし、起動時に読み込めるようにします。
まず、設定をファイルに書き出します。
$ sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"
このファイルを起動時に読み込むようにするために、/etc/rc.local の exit 0 の上に以下の記述を追加します。
sudo iptables-restore < /etc/iptables.ipv4.nat
これで再起動しても大丈夫になりました。
Raspberry pi を DHCP + DNS サーバーにしたい
クローズネットワークの台数増加
クローズネットワークを組んで使っています。
最初は台数が少なかったので固定 IP でよかったのですが、台数増加に伴い DHCP サーバーが欲しくなってきました。
ついでに名前もつけてあげれば IP を気にする必要がなくなります。
そこで、Raspberry pi を DHCP 兼 DNS サーバーに仕立て上げることにしました。
使用したのは Raspberry pi 3 Model B です。
Raspberry Pi3 Model B ボード&ケースセット 3ple Decker対応 (Element14版, Clear)-Physical Computing Lab
- 出版社/メーカー: TechShare
- メディア: エレクトロニクス
- この商品を含むブログ (4件) を見る
OS は Raspbian Jessie Lite の April 2017 バージョンを使用しました。
Raspberry pi の初期設定
SSH を有効にする
通常 Raspberry pi はディスプレイと USB マウス/キーボードを接続して操作しますが、DHCP + DNS サーバーにそんなものは贅沢です。
どうせローカルネットワークに入るので、SSH を有効にして SSH 経由で操作できるようにしました。
以下のリンクを参考に。
qiita.com
IP アドレスの設定
SSH でつなぐには IP アドレスを知らないといけません。
というわけで Raspberry pi に固定アドレスを振ることにしました。
2017/07/06 追記
OS Raspbian Jessie からは、/etc/network/interfaces ではなく /etc/dhcpcd.conf をいじるのが正しいようです。
なぜか /etc/network/interfaces でうまくいっていたのですが、Raspberry pi を 2 台導入して同じことをやろうとしたら、できませんでした。
なので /etc/dhcpcd.conf の書きかえ方も記載しておきます。
sudo nano /etc/dhcpcd.conf
最後の方に以下の記述を入れます。
interface eth0 static ip_address=192.168.0.1/24 static domain_name_servers=192.168.0.1
こちらは古い記述です。
アドレスは /etc/network/interfaces の編集で行います。
sudo nano /etc/network/interfaces
eth0 インターフェースに 192.168.0.1 を割り当てます。
auto eth0 iface eth0 inet static address 192.168.0.1 network 192.168.0.0 netmask 255.255.255.0 broadcast 192.168.0.255 dns-nameservers 127.0.0.1 192.168.0.1 dns-search local
emacs のインストール
ここからは、いろいろとテキストをいじっていくことになります。
nano は使い慣れていないので、大好きな emacs をインストールしておきます。
$ sudo apt-get install emacs
Caps lock を Ctrl にする
以下のサイトのまんまです。
/etc/default/keyboard というファイルを書き換えます。
$ sudo emacs /etc/default/keyboard
途中に XKBOPTIONS という部分があるので、そこを以下のように書き換えます。
XKBOPTIONS="ctrl:nocaps"
このあと再起動すれば OK。
DHCP + DNS サーバーの導入
大部分は以下のリンクを参考にしました。
一部、DNS の設定や運用部分を追記しています。
設定
設定ファイルを編集します。
$ sudo emacs /etc/dnsmasq.conf
/etc/dnsmasq.conf の下のほうにある
#conf-file=/etc/dnsmasq.more.conf
の#を削除します。
# Log lots of extra information about DHCP transactions. #log-dhcp # Include another lot of configuration options. conf-file=/etc/dnsmasq.more.conf #conf-dir=/etc/dnsmasq.d # Include all the files in a directory except those ending in .bak #conf-dir=/etc/dnsmasq.d,.bak
この /etc/dnsmasq.more.conf を作成して編集します。
$ sudo emacs /etc/dnsmasq.more.conf
# ローカルホスト名を上位DNSに転送しない domain-needed # ローカルIPアドレスの逆引きを上位DNSに転送しない bogus-priv # ショートドメイン名を補完するドメイン名(hoge -> hoge.local) local = /local/ # ローカルドメイン名 domain = local # ショートホスト名を補完する expand-hosts # DHCPで割り当てするアドレスの範囲とリース時間 dhcp-range = 192.168.0.50, 192.168.0.200, 12h # DHCPクライアントに通知するルータのIPアドレス dhcp-option = option:router, 192.168.0.1 # DHCPクライアントに通知するDNSサーバのIPアドレス dhcp-option = option:dns-server, 192.168.0.1 # 固定アドレスをふって、名前をつける dhcp-host = aa:aa:aa:aa:aa:aa, pc1, 192.168.0.10, infinite # 名前だけつける dhcp-host = bb:bb:bb:bb:bb:bb, pc2
これでクライアントに関しては DNS/DHCP ともに動作します。
ただこの方法だと、dnsmasq が DHCP を発行した場合のみ名前解決がされるようで、固定アドレス (Raspberry pi 自身) を最初から持っているものは名前解決できませんでした。
これを解決するには、/etc/hosts に名前を書けば良いようです。
$ sudo emacs /etc/hosts
Raspberry pi 自身のアドレスを書きます。
127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters 127.0.1.1 raspberrypi 192.168.0.1 raspi.local raspi
設定ができたので dnsmasq を再起動します。
$ sudo service dnsmasq restart
運用
DHCP リース記録の確認
どのように DHCP がリースされたかが気になったので調べてみました。
/var/lib/misc/dnsmasq.leases を見ることで実現できます。
$ cat /var/lib/misc/dnsmasq.leases 0 aa:aa:aa:aa:aa:aa 192.168.0.10 pc1 01:aa:aa:aa:aa:aa:aa:aa 1506215087 bb:bb:bb:bb:bb:bb 192.168.0.50 pc2 01:bb:bb:bb:bb:bb:bb
一番左がリース時間のようです。
不要な DHCP リースの削除
dnsmasq の設定をいろいろ試していると、リース時間無限大でいらないエントリが残ってしまいました。
そういうものを消すには、単に上記の /var/lib/misc/dnsmasq.leases の不要な行を消せば良いようです。
その場合は、いったん dnsmasq のサービスを止めたほうが良さそうです。
$ sudo service dnsmasq stop $ sudo emacs /var/lib/misc/dnsmasq.leases
0 aa:aa:aa:aa:aa:aa 192.168.0.10 pc1 01:aa:aa:aa:aa:aa:aa:aa
$ sudo service dnsmasq start
これでローカルネットワークの使い勝手が向上しました。
続編
今回はクローズネットワークを Raspberry pi で作りました。
ただこのあと、結局このネットワークを外につなぎたくなりました。
そのときの続編はこちらからどうぞ。