2011-12-24
■[bzr] bzr-git で快適 github 生活
Bazaar のホスティングサイトとして、 Launchpad があります。これはプロジェクトをまたぐ Issue Tracker やオンラインの翻訳システムが含まれており、 Ubuntu の開発に大活躍しています。
しかし、 Launchpad は github や BitBucket とは異なり、ユーザー主体ではなくプロジェクト主体になっています。ユーザーディレクトリ配下にブランチを作ることも可能なのですが、それだとマージリクエストなどの管理ができないので、本当に他人に使ってもらいたいならまずプロジェクトを作るところから始めないといけなくて面倒です。
その他にも、 Ubuntu の開発には Launchpad の機能が必要なんだろうけど、個人ユーザーには github の機能のほうが魅力的という場面がよくあります。 Launchpad で満足していたとしても、 github で開発されているプロジェクトに参加するにはやはり github を使えなければなりません。
ということで、前置きが長くなりましたが、今日は bzr-git について説明して行きます。
先に要点をまとめておきます。
- push ではなく dpush を使う
- git@github.com:user/repo.git ではなく git+ssh://git@github.com/user/repo.git で場所を指定する
bzr-git のインストール
Windows であれば、インストーラーで git プラグインにチェックを入れればインストールできるはずです(未確認)。
Ubuntu の最新版をお使いであれば、 "sudo apt-get install bzr-git" で終わりです。Fedoraも、確認していませんが、きっと最新安定版が yum でインストールできると思います。
そうでない場合は、 dulwich という git リポジトリやプロトコルを扱うライブラリと、 bzr-git プラグインのインストールが必要です。
dulwich のインストールですが、 http://www.samba.org/~jelmer/dulwich/ から最新版の tar.gz をダウンロードするかURLをコピーして、 easy_install か pip install してください。
この記事を書いている12/24現在、最新版は 0.8.2 なので、次のようなコマンドでインストールできます。
$ easy_install http://www.samba.org/~jelmer/dulwich/dulwich-0.8.2.tar.gz
dulwich は pure Python でも動作しますが、C言語で書かれた拡張を利用したほうが高速なので、Debian系であれば "sudo apt-get install python-dev", RedHat系であれば "sudo yum install python-devel" のように、ヘッダファイルのインストールをしたほうが良いでしょう。
次に、bzr-git のインストールです。 https://launchpad.net/bzr-git の右側にある Downloads リンクに最新版の tar.gz へのリンクがあるので、それを先ほどの dulwich と同じように easy_install でインストールするか、ユーザーごとのプラグインディレクトリに次のような手順でインストールします。
$ mkdir -p ~/.bazaar/plugins/ $ cd ~/.bazaar/plugins/ $ wget http://launchpad.net/bzr-git/trunk/0.6.6/+download/bzr-git-0.6.6.tar.gz # 12/24時点での最新版 $ tar xf bzr-git-0.6.6.tar.gz # bzr-git-0.6.6 というディレクトリができる $ mv bzr-git-0.6.6 git # git/ というディレクトリにリネーム
インストールできているかどうか確認しましょう。
$ bzr plugins -v ... git 0.6.6 A GIT branch and repository format implementation for bzr. /home/inada-n/.bazaar/plugins/git ...
push と dpush
実際に bzr-git を使う前に、 dpush コマンドについて説明しておきます。
bzr-git や bzr-svn で問題になるのは、異なるバージョン管理システム間でのリビジョンのラウンドトリップ(可逆変換)です。
ある bzr ブランチ上でコミットすると、 bzr のリビジョンができます(=X)。それを bzr-svn で svn のブランチに push すると、 svn 上のリビジョンに変換されます(=X')。その svn 上のリビジョンを他の bzr ブランチから pull したときに、もとの X に戻ればラウンドトリップができていて、別の X'' になってしまった場合はラウンドトリップができていません。
ラウンドトリップができるかどうかは重要な問題です。手元の最新リビジョンが X だったときに、リモートの最新リビジョン X' が X に変換できないと、手元のブランチがリモートの最新リビジョンと一致しているのかどうかを判断するには、別に X と X' が一致しているという対応表を管理しないといけなくなり、この対応表が無いと作業できなくなってしまいます。全員が同じ対応表を共有しないといけないのでは、分散バージョン管理の魅力が半減してしまいます。
bzr-git では、現在のところ、 git -> bzr -> git のラウンドトリップはできていますが、 bzr -> git -> bzr ができていません。つまり、 bzr のブランチを git で管理するには、 bzr のリビジョンを全部 git のリビジョンへと非可逆変換した後、その git のリビジョンから bzr のリビジョンを作りなおす操作が必要です。
この、非可逆変換を行いながら別のバージョン管理システムへ push するコマンドが dpush です。 bzr -> git -> bzr のラウンドトリップができていないので、 git への push はできず、 dpush のみが可能です。
bzr のブランチを github に dpush しよう
サンプルとして、 TortoiseBZR のブランチを github に置いてみます。準備として、次のように bzr ブランチを用意します。
$ bzr branch lp:tortoisebzr tbzr; cd tbzr
https://github.com/ を開いてください。右下の方に New repository というボタンがありますね?なかったら github アカウントを取得してください。このボタンを押して、 Create New Repository というフォームが出てきます。今回は TortoiseBZR のブランチを github に置いてみるので、 Project Name に "TortoiseBZR" と書いて、 "Create repository" というボタンを押します。これで methane/TortoiseBZR というリポジトリができました。名前を記入するだけでリポジトリが作れちゃうお手軽さが素敵です。
リポジトリができたら、そのリポジトリの使い方を説明したページが表示されます。そこで、git の push/pull 先として表示されている "git@github.com:methane/TortoiseBZR.git" が重要です。
git から push/pull するときは相手先が git だと判っているのでこれだけでいいのですが、 bzr からアクセスするときは "ssh経由のgitプロトコル" であることを教えてあげなければなりません。先ほどのリポジトリの場所を bzr 形式にすると、 "git+ssh://git@github.com/methane/TortoiseBZR.git" になります。先頭に "git+ssh://" が付いているだけじゃなく、 github.com の後ろを : から / にしていることに注意してください。
dpush する前に、 dpush によって bzr のリビジョンが git 用に書き換えられている事を確認するために、現在の bzr のリビジョンIDを見ておきましょう。
$ bzr log --show-ids -l3 ... revision-id: xxxxxx@yyyyy.zzz-20111207050015-p8yk5czl0re057lo ...
bzr のリビジョンは、デフォルトではリビジョンIDはコミッタのメールアドレス+タイムスタンプ+乱数になっています。さて、今度こそ dpush してみましょう。
$ bzr dpush git+ssh://git@github.com/methane/TortoiseBZR.git --remember
初回だけ、すべてのリビジョンを git 形式に変換しているので、少し時間がかかります。といっても TortoiseBZR の規模で数十秒なので、ラーメンを作って待つほどではありません。
さて、リビジョンIDはどうなっているでしょうか。
$ bzr log --show-ids -l3 ------------------------------------------------------------ revno: 436 [merge] revision-id: git-v1:962e312abbe8396cacdeaad1266b0269d160556a parent: git-v1:146563a9e1c4324f190e5c3a9becc592c130a0e9 parent: git-v1:47b8db80c88b5065080397dc242294829973a326 git commit: 962e312abbe8396cacdeaad1266b0269d160556a ...
"git-v1:<gitのコミットID>" という形式になっていますね。bzrのリビジョンIDはユニークであればなんでも良いので、gitのコミットIDを使うことで簡単に対応が取れるようになっています。
このあとは、コミットして dpush するたびに、新しいリビジョンがgit用に変換されます。
注意しないといけない点として、一度 dpush してリビジョンIDがgit用になってしまうと、もう Lauchpad 上の bzr ブランチとは全くの別物になってしまっています。 TortoiseBZR のブランチを改造して github に公開して「マージして」と言われても開発者は手動でマージするしかありません。
Launchpad と github を両方使う場合は、 github 側をマスター、 Launchpad 側をミラーという扱いにして、 Launchpad 上には git 用に変換されたリビジョンだけを置くようにすると良いでしょう。そうすると、github上の pull request はそのまま処理できますし、 Launchpad 上のマージリクエストも一旦手元でマージして github に dpush したあと Launchpad に push することができます。
github 上のブランチを bzr-git でローカルに持ってくる
ここまで来たら後は簡単です。先ほどの methane/TortoiseBZR を bzr-git で取得しましょう。
$ bzr branch git+ssh://git@github.com/methane/TortoiseBZR.git tbzr
tbzr というディレクトリがローカルブランチです。あとは先程と同じく、ここで作業して dpush していくだけです。
bzr-git の制限と今後
本当は bzr-git で git の作業ツリーを操作する紹介もしたかったのですが、まだ不安定でコミット時にエラーが出たりするのでやめておきます。この問題がなくなれば、作業ツリーを git にすることで git と bzr 間のリビジョンの変換によるオーバーヘッドが無くなるので、大きいリポジトリを最初に branch してくる時間が短縮されるはずです。
また、 bzr-2.4 + bzr-git-0.6.6 の段階では、 git の master ブランチしか扱うことができません。bzr-2.5からは1つのディレクトリに複数のブランチを格納する colocated branch が実験的に導入され、bzr-git もそれに合わせて git の master 以外のブランチが扱えるように拡張される予定です。
これらの改良が行われると、bzr-git は、 git の独特のコマンド体系を覚えなくても使える、しかも Windows や Mac OS X で日本語ファイル名を使ってもちゃんと NFC の UTF-8 でファイル名をコミットしてくれる、一番使いやすい github クライアントになるでしょう。
2011-12-23
■[bzr] 本当は怖い軽量チェックアウトの話
前回は、作業ツリー上でコミットすると、別の場所にあるブランチに新しいリビジョンが追加されるチェックアウトについて説明しました。
今回はチェックアウトの別の仕組みである軽量チェックアウト (lightweight checkout) について説明します。
軽量チェックアウトの使い方
軽量チェックアウトを使う方法は、通常のチェックアウトをするときに --lightweight というオプションを付けるだけです。
$ # /path/to/branch にあるブランチの軽量チェックアウトを workingtree に作成する $ bzr checkout --lightweight /path/to/branch workingtree
軽量チェックアウトを使うときは、チェックアウト元のブランチは同じマシン上のものにしてください。
その理由と、軽量チェックアウトのメリットをこれから説明していきます。
軽量チェックアウトを理解するために、まずは、通常のチェックアウトの仕組みから理解していきましょう。
チェックアウトの仕組み
(軽量でない)チェックアウトの実体は、通常の(作業ツリー付きの)ブランチです。ただし、このブランチは、チェックアウト元のブランチに bind されています。 (bound branch)
bind されたブランチにコミットなどの操作を行うときは、先に bind 先のブランチにその操作を行い、それに成功したらローカルのブランチを bind 先のブランチに同期します。
このように自動的に同期が保たれるので、まるでSubversionのような、チェックアウト元のブランチから直接チェックアウトしているように見える挙動をするのです。
また、ローカルにブランチがあるので、最新版のコピーしか持たないSubversionのチェックアウトと異なり、オフラインでも log や annotate などの履歴閲覧操作ができます。
bind, unbind コマンドを使って、ブランチを他のブランチに bind したりそれを解除することができます。つまり、チェックアウトは unbind すると通常のブランチになり、通常のブランチを bind するとチェックアウトになるのです。
軽量チェックアウトの仕組み
軽量チェックアウトは、通常のチェックアウトと違いローカルのブランチを持ちません。チェックアウト元のブランチから直接のチェックアウトになっています。
チェックアウト: 作業ツリー <-- ローカルブランチ --(bind)--> チェックアウト元ブランチ 軽量チェックアウト: 作業ツリー <-- チェックアウト元ブランチ
作業ツリー上で作業するために絶対に必要な最新リビジョンの情報はブランチにあります。なので、過去の履歴の確認どころか、ファイルが変更されているかどうかの確認のためにもチェックアウト元ブランチを参照しなければなりません。
軽量チェックアウトにはブランチが無いので、 bind, unbind は利用できません。ただし、 switch コマンドでブランチ元を切り替えることは可能です。
軽量チェックアウトのメリット
軽量チェックアウトのメリットは、その名の通り、ブランチを持たない分容量が軽いことです。
ただし、 Bazaar で重いのはブランチではなくてリポジトリです。実際のリビジョンはリポジトリに圧縮されて格納されていて、ブランチはリポジトリの場所と最新リビジョンのIDを記録しているだけだからです。
なので、軽量チェックアウトは、ブランチそれぞれがリポジトリを持つ(standalone branch)の場合は通常のチェックアウトより軽量ですが、チェックアウト元ブランチとチェックアウトのブランチが同じ共有リポジトリを利用している場合はメリットがありません。
軽量チェックアウトが役に立つユースケースとして、ある1つのサーバーがbzrのブランチ管理とCIサーバーやデプロイ用サーバーになっている場面を考えてみましょう。
ビルドしてテストを実行するために、管理しているブランチの作業ツリーが必要です。テスト後のクリーンアップで間違ってbzrブランチを削除したりしないように、CIサーバーが動くユーザーとbzr用のユーザーは分けましょう。ディレクトリ構成は次のようになるでしょう。
/home /home/proj_bzr - bzr 用ユーザーのホーム /home/proj_bzr/repo - 共有リポジトリ /home/proj_bzr/repo/main - main ブランチ(作業ツリーなし) /home/proj_bzr/repo/1.0 - メンテナンスブランチ(作業ツリー無し) /home/proj_ci - CI サーバーユーザーのホーム /home/proj_ci/work - 軽量チェックアウト
ユーザーをまたいで共有リポジトリを使うのは難しいですが、軽量チェックアウトを使うことでリポジトリ内のデータをコピーすること無く作業ツリーを構築することが可能です。
軽量チェックアウトが便利な他のユースケースとして、 colo-checkout があります。通常は bzr-colo を使っていて1つの作業ツリーで作業していたのに、一時的に別の作業を始めたい場合に colo-checkout を使って別の作業ツリーを作ることができるのですが、これが軽量チェックアウトになっているので、最低限の容量だけで作業ツリーを作ることができます。
軽量チェックアウトの問題点
最初に、「軽量チェックアウトを使うときは、チェックアウト元のブランチは同じマシン上のものにしてください。」と言った理由を説明します。
http や bzr+ssh などで接続するリモートのブランチから軽量チェックアウトをした場合、 bzr info や bzr status なども含めてほぼすべての操作がリモートブランチを参照するために、 http サーバーや ssh サーバーに接続するので、サーバーに大きな負荷をかけてしまいます。
実際に私の会社であった例として、TortoiseBZR をインストールしたPCでbzr+sshからの軽量チェックアウトを作成してしまい、TortoiseBZR が頻繁に ssh 接続しにいくという事がありました。接続に成功する場合は単に重くなるだけで済むのですが、 pageant を常駐して鍵を登録していないために ssh 認証が失敗する状況で認証エラーが頻発したためにsshサーバーが新規接続を一時的に受け付けなくなり、多くの人の作業に支障が出てしまいました。
Bazaar Explorer が提供しているチェックアウトダイアログ(qbzr とは別のもの) がオプションも表示も無しに軽量チェックアウトを作ってしまうため、通常のチェックアウトを作るつもりで軽量チェックアウトを作ってしまう場合があります。GUIで軽量ではないチェックアウトを作成するときは、チェックアウトのダイアログではなくブランチのダイアログを利用し、 bind オプションを有効にしたほうが安全です。
2011-12-21
■ [bzr] チェックアウトとブランチの使い分け
bzr が git や hg と大きく異なるのは、メインラインの概念とチェックアウト機能だと思います。
メインラインについては昨日の Advent Calnedar で wonderful_panda さんが解説してくれました。(ブランチのメインラインのイメージについてしゃらくさい話をするよ - wonderful_pandaの日記)
今日はチェックアウト機能について紹介し、通常のブランチとの使い分けを解説します。
チェックアウトとは
チェックアウトとは作業ツリー、あるいは作業ツリーを作成するコマンドのことです。作業ツリーは常にどこかのブランチに紐づいています。
bzr のチェックアウトは、紐づくブランチが別の場所(別のディレクトリや別のサーバー上)にあってもいいところがユニークです。
$ bzr checkout lp:bzr-colo colo-co # colo-co は lp:bzr-colo に紐づいたチェックアウトになる $ bzr branch lp:bzr-colo colo-br # colo-br は lp:bzr-colo 最新版から分岐したブランチ兼そのブランチの作業ツリー
ちなみに、後者のように独立したブランチでありかつ作業ツリーでもあるディレクトリの事を、 standalone tree と呼びます。
チェックアウトのメリット・デメリット
サーバー上にあるブランチをチェックアウトした場合の、ローカルにブランチを作成する場合に比べた時のメリットとデメリットをまとめます。
メリット: push忘れがなくなる
サーバー上のブランチからローカルにチェックアウトした場合、その作業ツリー上でcommit を実行すると、自動的にサーバー上のブランチにコミットされます。
そのため、 commit してだれかに pull してと連絡したけど、じつは push を忘れていた・・・という事態を避けることができます。
デメリット: オフラインではコミットできなくなる
commit コマンドが自動的にサーバー上のブランチにコミットしようとするので、そのサーバーに接続できない状況ではコミットができなくなります。
(この場合、 bzr unbind というコマンドで、サーバー上のブランチとの紐づけを解除して、ローカルブランチに変換することができます)
デメリット: コミッタが複数人いた場合、 update 時にコンフリクトが発生する可能性がある
サーバー上のブランチをだれかが更新していた場合、コミットしようとするとチェックアウトが最新じゃないから update しろというエラーが発生します。
そこで update した場合、サーバー上の変更とローカルのまだコミットされていない変更がマージされるのですが、その時にコンフリクトが発生するおそれがあります。
この状況でコンフリクトが発生した場合、コンフリクトの解消中に操作ミスをして必要なローカルの変更を削除してしまうと、マージを最初からやり直すことはできません。
これは Subversion でも共通の問題ですね。
メリット: コミッタが複数人いてもメインラインの維持が楽
サーバー上のブランチ (a -> b) からローカルブランチを作って作業を記録し、ローカルのブランチの履歴が (a -> b -> d -> e) となっているとします。
他の人が先にサーバー上のブランチを更新し、 (a -> b -> i -> j ) となりました。ここに自分の修正をマージするときに、
$ bzr merge bzr+ssh://りもーと/ブランチ $ bzr commit # 新しいリビジョン X ができる $ bzr push bzr+ssh://りもーと/ブランチ
としてしまうと、ローカルブランチのメインラインがサーバー上に反映され、サーバー上のメインラインが (a -> b -> d -> e -> X) になってしまいます。
メインラインを意識しない git や mercurial ではこれは通常の操作ですし、bzr でも同じくメインラインを意識しない運用をしても構いません。
が、メインラインを単調増加の1本道にしておきたい場合は、マージ用のチェックアウトがあると便利です。
$ cd .. $ bzr checkout bzr+ssh://りもーと/ブランチ remote_checkout $ cd remote_checkout $ bzr merge ../ローカルブランチ $ bzr commit
こうすると、サーバー上のメインラインに、自分のローカルブランチの変更をマージするコミットを 1 つ追加した履歴 (a -> b -> i -> j -> X) になります。
ローカルブランチを作ってからサーバー上のブランチに一切更新がなかった場合、リモートのブランチの履歴が (a -> b -> X) (Xは (b -> d -> e) というサブラインのマージ)になることに注意してください。
これにより、ローカルブランチで細かい雑な変更をしていても、メインライン上の履歴では 1 つの綺麗で完結したコミットにすることができます。
一方、ローカルブランチの変更をそのまま持ってきたい場合は、mergeではなくpull を使います。 「枝分かれしてたら merge, してなかったら pull」を自動で選択実行してくれる merge --pull コマンドが便利です。
使い分け
チェックアウトのみ
Subversion から Bazaar に移行したばかりのチームメンバーが、 Bazaar をインストールしてすぐに作業を始められるのがこの運用方法です。
また、コミッタが1人しかいなくて、機能ブランチを作る必要もない場合は、 Bazaar に慣れた開発者にとっても便利です。
チェックアウト+ローカルブランチ
Bazaar に慣れてきたら、サーバー上のブランチを管理するためのチェックアウトと、実際に作業をするためのローカルブランチを使い分けると良いでしょう。
ローカルブランチのみ
サーバー上にブランチを置かないなら別にこれでいいですね。
サーバー上のブランチに push する人が自分一人のケース (例: Launchpad上の lp:~/project/branch など、個人で公開しているブランチ) でも、チェックアウトに比べると push をしないといけないのが面倒ですが、オフラインで作業できるという利点があります。
また、チーム開発しているブランチでも、 hg や git の用にメインラインを意識しないのであればこれでかまいません。
bzr-colo でチェックアウト+ローカルブランチ運用
チェックアウト+ローカルブランチ運用で、サーバー上のブランチを管理するためだけにチェックアウトを置いておくのはディスク容量的にちょっと。。。というケースがあるかもしれません。そんな場合は bzr-colo を使いましょう。
bzr-colo の基本的な使い方は wonderful_panda さんが紹介してくれているので、 (今更だけどbzr-coloの話をするよ(基本編) - wonderful_pandaの日記, 今更だけどbzr-coloの話をするよ(応用編) - wonderful_pandaの日記) ここでは チェックアウト+ローカルブランチ 運用をする方法だけ紹介します。
bzr-colo では、リモートのブランチを colo:origin/main のように origin/ で始まる名前にしてやると、colo-pull で複数のブランチを一気に pull してこれます。 (git で言うと origin よりも remote の方が意味的に近いです)
しかし、普通に使うと、 origin/main はただのブランチなので、上で説明したようなメインラインの入れ替え問題が発生します。そこで、 origin/main をサーバー上の main ブランチに紐づけましょう。
$ bzr switch colo:origin/main # 一旦、 colo:origin/main に切り替える $ bzr bind bzr+ssh://りもーと/main # リモートのブランチに紐づける $ bzr merge colo:mywork # ローカル作業ブランチをマージする $ bzr commit # サーバー上のブランチにコミットされる
bzr bind は1度実行しておくだけで、次回からは switch するだけで大丈夫です。
2011-12-18
■[python] Python の GC のデバッグ機能
Python のデーモン型のプログラム(具体的に言うと Loggerhead という bzr のリポジトリブラウザ)が大量(具体的に言うと100MB以上)メモリを食っていたので、それを調べた時のメモ。
まず、本当にそれだけのメモリを利用しているのかどうかを meliae というメモリプロファイラで調べたんだけど、18MBくらいしか使ってなかった。 meliae の使い方はまた今度に回すとして、今回はそんなにメモリを使うはずがないのにメモリを使ってしまっているケースについて。
この場合は、循環参照を大量に作ることによってメモリブロックがたくさん確保され、循環参照コレクタが実行された後も(メモリブロック内に1つでも生き残っているオブジェクトがいるなどの理由で)OSにメモリブロックが返されないという原因が考えられる。(循環参照以外の原因でメモリが大量に確保されてる可能性もあるんだけどね)
今日の話は、その循環参照の見つけ方。
とりあえず、 gc モジュールのドキュメントを読むこと。短いのですぐ読める。
gc.set_debug(gc.DEBUG_LEAK) が標準的な方法なんだけど、 list とか dict とかが大量に表示されてウザい場合は、自分で表示するオブジェクトをフィルタリングしてやる。次のコードでは、5秒ごとにGCを実行して、そこで回収されたオブジェクトのうち list, str, dict を除いたオブジェクトを表示してから開放している。
def gc_thread(): while True: time.sleep(5) gc.collect() for g in gc.garbage: if isinstance(g, list) or isinstance(g, basestring) or isinstance(g, dict): continue try: print g except Exception: pass g = None del gc.garbage[:] gc.set_debug(gc.DEBUG_LEAK) th = threading.Thread(target=gc_thread) th.daemon=True th.start()
大抵の場合、 list,dict 以外のオブジェクトが循環参照のどこかに挟まっているので、そこから循環参照の原因を探るのが早い。
list, dict だけで循環参照が構築されている場合は、まあ、なんか頑張る。
循環参照の原因を見つけて、それが短期間のうちに何度も生成してほったらかしにされている類のものだった場合は、要らなくなったタイミングで循環参照を手動で切断してやれば、メモリ使用量を削減できるかもしれない。
小さい循環参照がたくさんできるのではなく、大きな循環参照がときどきできるだけなら、手動で頑張ってもあんまりメモリ使用量は削減できない。これはそういうプログラムなんだと諦める。
2011-12-03
■[Python] Python3 Advent Calender 3日目 - New GIL を理解する
connpass-2011 Pythonアドベントカレンダー(Python3) の3日目を担当します。
Python 3 がリリースされてから、 Python の進化は主に Python 3 で行われ、そこから Python 2 にバックポートできるものがバックポートされています(例: GCのチューニング、辞書の内包表記など)。
しかし、 Python 2 は 2.7 で新規開発を終了しており、 2.7 にバックポートされなかった機能はもう Python 3 に移行しないと利用することができません。今日は、そんな機能のひとつである New GIL を紹介します。
なお、今日紹介する内容のほとんどは UnderstaindingGIL で紹介されている内容を僕なりに要約したものです。(ただし、翻訳ではありません) 著作権の扱いがわからなかったのと、代わりに自分で図を書き起こすのが面倒だったので、この記事の本文中でもこの資料をページ数指定して参照します。ぜひ見比べながら読んでみてください。
GIL とは
CPython のスレッドは、OSのスレッドと1対1対応しており(ネイティブスレッド)、例えば複数のシステムコールを並列に実行することができたり、bz2圧縮のようなC言語で書かれた重い処理を並列に実行することができます。
しかし、Pythonのコードを実行したり、Pythonのオブジェクトを操作できるスレッドは、常に1つです。これはシングルスレッド時の性能や、Pythonの実装をシンプルにするための設計です。
この、1つのスレッドしか実行できない、という制限のために存在しているのが GIL = Giant Interpreter Lock です。読んで字のごとく、インタプリタ全体にまたがるロックです。このロックを取得しているスレッドだけが、Pythonのコードを実行したりPythonのオブジェクトを操作できるわけです。
Old GIL
Python 2.x や Python 3.1 までの GIL を、 New GIL に対して Old GIL と呼ぶことにします。Old GIL の動作は至ってシンプルです。
- ブロックするシステムコールを実行する場合などは、GILを開放してから実行し、完了してからGILを再取得する。 (10ページ)
- Python のコードを実行し続ける場合は、 checkinterval で決められた期間だけコードを実行した後、一旦GILを開放してから再取得する。 (11-14ページ)
Old GIL の問題点を説明する前に、GILの実装についてもう少し説明しておきます。(18ページ) プラットフォーム毎に実装の詳細は異なるのですが、ざっくり言うと、 GIL はロック変数と通知機構でできています。ロック変数は 1 から 0 にするデクリメントと、 0 から 1 にするインクリメントをアトミックに行うことができるものとします。
GILを取得する場合は、ロック変数のデクリメントを試みて、失敗した(ロック変数の値が0だった)ら通知を待ってスリープし、通知が来たら再度ロック変数のデクリメントを試みる、という動作を、デクリメントができるまで繰り返します。
GILを解放する場合は、ロック変数をインクリメントします。この時は、自分が GIL を持っているのでインクリメントは確実に成功します。インクリメントした後に通知を待っているスレッドの1つに対して通知を送ります。(どうせGILを取得できるスレッドは1つなので、複数のスレッドに通知を送っても意味がないからです)
この機構によって、どのようにスレッドの実行権が切り替わるかを考えます。
まず、GILを持っているスレッドが、ブロックするシステムコールを実行する前にGILを開放したとします。この場合は、通知を受けてスリープ状態から励起されたスレッドがGILの取得に成功します。他にだれもGILの取得を試みていないからです。(19-21ページ)
次に、 checkinterval が経過したためにGILを開放してすぐに再取得する場合について考えてみます。この場合は、 GIL の取得を試みるスレッドは、GILを開放したスレッドとGILを奪いあうことになります。 (22ページ)
シングルコアの場合は、同時に実行できるスレッドが1つだけなので、ロックを開放して通知を送った時に、通知元と通知先のどちらのスレッドが実行されるかはOSが判断します。(24-26ページ) OSのスケジューラが賢ければ、今までGILを持っていた通知元のスレッドがまだ短時間しか実行していないなら、コンテキストスイッチの回数をむやみに増やさないためにそのまま実行を続けるでしょうし、すでに長時間実行していたならすぐに通知先のスレッドにスイッチするでしょう。こうして、OSに優先されたスレッドがGILを取得することになります。(32ページ)
しかし、時代はマルチコアです。マルチコアでは、同時に実行できるスレッドが2つ以上あるので、ロックを解除し通知を送ったスレッドはそのまま実行を続け、並列して通知を受け取ったスレッドが実行を再開します。スレッドが通知を受けて実行を再開するのにはタイムラグがあるので、たいていはGILを開放した方のスレッドがそのままGILを確保してしまいます。(33ページ) なので、長時間Pythonコードを実行スレッドがあると、他のスレッドはなかなかGILを取得できなくなってしまいます。(34ページ)
他にもいくつか問題があります。
ファイルからの読み込みで、すでにファイルがOSにキャッシュされていた場合など、ブロック「するかもしれない」システムコールがすぐに返ってくることがよくあります。このとき、システムコールの前後でGILの開放と取得を行っているので、大量に通知が送られて無駄に負荷が増える場合があります。(35ページ)
checkinterval が時間ではなくバイトコード単位で期間を指定していたため、チェック間隔が短すぎてオーバーヘッドが大きくなったり、逆に長すぎてレイテンシが悪くなったりする可能性があります。
シングルコアでCPUバウンドなスレッドがたくさんある場合はOSがどんどんコンテキストスイッチを発生させて、スループットが低下する可能性もあります。
New GIL
Python 3.2 から新しいGILが搭載されました。 New GIL では Old GIL の checkinterval 周りの動作が一新されています。
checkinterval はなくなり、代わりに 後述するタイムアウト時間を指定する switchinterval が導入されました。 (sys.getswitchinterval(), sys.setswitchinterval() で参照と変更が可能.デフォルトは5ms)
New GIL は、基本的なロック機構は Old GIL と同じなのですが、長時間 Python コードを実行し続けるスレッドからGILを奪う仕組みが異なります。 checkinterval 毎に GIL を開放、再取得するのではなく、 gil_drop_request というフラグをがあると強制的にGILを解放することになります。 (39ページ)
GIL を取得したいスレッドは、タイムアウト付きで、 Old GIL と同じ方法でロックの取得を待ちます。(42ページ) タイムアウトするまでに、今まで実行していたスレッドがI/O処理などでGILを開放してスリープしたときは、今まで通りにGILを取得します。(43ページ)
タイムアウトが発生した場合、そのスレッドは gil_drop_request を設定して、さらに待ちます。 (44ページ) このフラグを見た実行中のスレッドがGILを開放するのですが、Old GILと違ってすぐにはGILの再取得をしません。なので、マルチコアでもきちんとGILが移譲されることになります。(45ページ) そのかわり、新たにGILを取得したスレッドが、GILを取得できたことを、GILを開放したスレッドに通知します。 GILを開放したスレッドは、この通知を受けてから、GILの再取得待ちに入ります。 (46-47ページ)
また、このタイムアウトはバイトコード数などではなくて時間になったので、バイトコード当たりの実行時間がバラバラでも切り替え間隔が長すぎたり短すぎたりはしません。
CPUバウンドなスレッドがたくさんある場合も、このタイムアウト時間分は1スレッドが専有できるので、スループットの低下も起こりにくくなっています。
New GIL の欠点
残念ながら、これで全て解決!とは行きません。むしろ New GIL が Old GIL に劣る場面があります。その典型的な例が、CPUバウンドのスレッドとIOバウンドのスレッドの組み合わせです(50ページ)
IO処理が完了して、GILを取得しようとした時、タイムアウト時間分はGILを取得できないので、レイテンシが低下します。(51ページ) すぐに返ってくるIO処理を繰り返す場合は、このタイムアウト時間の積み重ねが大幅なパフォーマンス低下に繋がります。(53ページ)
他にも、実行待ちのスレッドが複数あった場合に、先に待っていたスレッドに実行権が移らないという問題もあります。これは、タイムアウト付きのシグナル待ちを繰り返すときに、シグナル待ちのキューの最後に回されてしまうために、後からGIL待ちを始めた方がシグナル待ちの順序では先になってしまうからです。 (52ページ)
これらの欠点を改良するために、現在、スレッドに優先度を付ける提案や、よりていねいにブロックしないIOを判別してGILの開放をしない提案がされています。 Python 3.3 までに間にあうと良いですね。
最後に
Old GIL と New GIL の簡単な解説と、その欠点の紹介をしました。特に大きい問題は、CPUバウンドなスレッドを走らせながら他にもスレッドを実行しているケースで発生するので、マルチスレッドの Python プログラムがCPU を 100% 利用している場合はCPUバウンドな処理を multiprocessing.Process などを使って別プロセスに切り出したほうが良いでしょう。応答性能の問題が回避できるだけじゃなく、マルチコアを利用して並列計算できるようにもなります。
さて、 connpass-2011 Pythonアドベントカレンダー(Python3) の4日目ですが、「エキスパートPythonプログラミング」著者の Tarek による新しいパッケージングシステムを紹介してくれるらしいので、 id:rudi さんにお願いしたいと思います。

