Java言語で学ぶデザインパターン 〜マルチスレッド編〜 読書メモ (1)

結城先生のデザパタ本マルチスレッド編がサクッと読めそうだったので読み進めているので、自分用メモ。ほとんど知っている内容ではあったけど、体系的に知識が入っていなかったので、よい確認になる…。

Java 言語のスレッド

スレッドの起動方法

スレッドを起動する方法は2つある

  1. Thread クラスのサブクラスのインスタンスを使ってスレッドを起動する
  2. Runnable インターフェースの実装クラスのインスタンスを使ってスレッドを起動する

それぞれ run() メソッドを実装している。これらを直接呼び出した場合、呼び出したスレッドで処理が実行される。別のスレッドで実行させたい場合は、start() を呼び出す必要がある。以下のテストを実行すると、run() の呼び出しはメインスレッドから行なわれているのが観測できる。

スレッドの生成には ThreadFactory なるものを使うこともできる。これにより Threadnew せずに済む。

Thread の排他制御

  1. synchronized メソッドを使う
  2. synchronized ブロックを使う

あるスレッドから synchronized インスタンスメソッドが呼ばれている間は、
他のスレッドは同じインスタンスの synchronized メソッドを呼び出すことができない。
非 synchronized インスタンスメソッドについては問題なく呼び出すことができる。
ロックはインスタンスごとにされる点に注意。

次の synchronized メソッド と synchronized ブロックを使ったメソッドは等価になります。

また、次の synchronized クラスメソッドと synchronized ブロックを使ったクラスメソッドは等価になります。

スレッドの排他制御をする仕組みを モニタ と呼びます。
また、ロックをとっていることをモニタを所有する、あるいはロックをホールドすると呼ぶこともあります。

スレッドの協調

各インスタンスはウェイトセットという仮想的な概念を持っています。
あるインスタンスがあるスレッドに wait メソッドを呼び出されたとき、インスタンスのウェイトセットにそのスレッドが追加されます。
スレッドは notify, notifyAll, interrupt の呼び出しが発生するか wait のタイムアウトが発生するまで停止します。
synchronized 文やブロックの外側で オブジェクトの wait を呼び出すと IllegalMonitorStateException が発生がスローされます。

スレッドには処理をキャンセルする機能があったり、優先度を設定できたり、スレッドの終了待ちをできたりもする。

マルチスレッドプログラミングの評価基準

  • 安全性(safety): オブジェクトを壊さないこと
  • 生存性(liveness): 必要な処理が行われること
  • 再利用性(reusability): クラスを再利用できること
  • パフォーマンス(performance): 処理を高速・大量に行えること

うち安全性と生存性を守るのは必須

ラズパイ3 にRASPBIANを入れるまで

man dd

PHPのスコープについて

自分用メモ。PHPのblockはスコープの単位とならない。この特徴を示す単純なコードは以下のようなもの。

つまり以下のようなコードは冗長だということになる(ただ、個人的には上のコードわかりづらいと思う…)。

他言語を書きながら、ふと PHP を書くと混乱するのでメモ。

無効になったSNSのエンドポイントを削除してくれる AWS Lambda コードを書いた

無効になったSNSのエンドポイント、別に放っておいてもよいのですが、定期的にお掃除したほうがよさそうだったので、お掃除用のAWS Lambdaコードを書きました。Amazon SNSは、アプリケーション(SNSのデバイストークンが登録できるやつ)に紐付いているエンドポイントの状態が変化したときに通知してくれる機能があります。その状態変化通知を受け取るトピックを適当に作っておいて、AWS Lambda コードがそいつを購読するようにしてあげればよいことになります。

ソースコード

Amazon Simple Notification Service(SNS)の基本的な仕組み

用語や概念が少し分かりづらい部分があるため、最初に基本的な仕組みや用語の説明を自分用にまとめました。

通知を送る方法

Amazon SNSを用いて、モバイルPUSH通知を実現するためには、 トピックを利用した通知 と デバイスを直接指定した通知(直接アドレス指定) の 2 種類の方法があります。

トピックを利用した通知

  • Amazon SNSには 通知を送りたいクライアント(Publisher)と通知を受け取りたいクライアント(Subscriber)の間に入る トピック という概念が存在します。
    • 通知を送りたいクライアント(Publisher)は、トピック に対してメッセージを送信します。
    • 通知を受け取りたいクライアント(Subscriber)は、トピック を購読(Subscription)します。
  • 通知を受け取るクライアントが利用できるプロトロルは、 AWS Lambda / SQS / HTTP/S / Email / SMS と後述する Application のエンドポイントです。
  • モバイルデバイスのPUSH通知を可能にするためには、Application を作成する必要があります。
    • Application には、APNsのPUSH証明書やGCMの証明書を紐づけます。
    • Application には、対応するプラットフォームにおける デバイストークン を登録することができます。

デバイスを直接指定した通知(直接アドレス指定)

  • モバイルデバイスのPUSH通知を可能にするためには、Application を作成する必要があります。
    • Application には、各プラットフォームのデバイストークンを登録します。登録するとそのデバイストークンに対応する エンドポイント がSNSから発行されます。
    • Publisher は、この エンドポイント に直接メッセージを送ることができます。これを 直接アドレス指定 とよびます。
    • これは トピック を経由せずメッセージを送信できる例外的な仕組みです。
    • 通知を送りたいデバイスが明確に決まっている場合にこの手法を使います。
    • 利用するAPI:Amazon SNS>>Actions>>Publish / PHP DOCS
  • パラメーター: Message: String, Subject: Option[String], TargetArn: TopicArn | EndpointArn
    • ドキュメント:https://docs.aws.amazon.com/ja_jp/sns/latest/dg/SNSMobilePush.html

SNSの制限

  • トピック数上限: 100,000 トピック/アカウント
  • サブスクリプション数上限: 10,000,000 サブスクリプション/トピック
    • ただし、AWSにお問い合わせフォームから連絡を入れるだけで無料で上限解放してもらえるようです(ソース)
  • 送信データ:最大 256 KB のテキストデータ(XML、JSON、未フォーマットのテキストなど)

通知状況のモニタリング

  • 発行したメッセージ数、通知に成功した数、通知に失敗した数、発行したデータサイズを、AWS CloudWatch API経由で確認することができる

有効期限切れのトークンに対する処理

  • 自動的に無効化される
  • 無効化された際にサーバー側へ通知を送ることができる

AWS SDK for PHP を用いた Amazon SNS の操作

AWS Lambda, SQS, HTTP/S, Email, SMS, モバイルデバイスなどに対して PUSH 通知を送ることができる Amazon SNS を PHP の SDK から操作する方法についてざっくりと見ていきます。

SNSクライアントのインスタンス化

Amazon SNS の操作をするためのクライアントクラスは、以下のように直感的にインスタンス化できます。

デバイストークンの登録

APNSやGCMなどから発行されたデバイストークンは SnsClient#createPlatformEndpoint を用いて対応するアプリケーションへエンドポイント登録ができます。戻り値の 'EndpointArn' にデバイスに紐づく EndpointArn が格納されているので、個別のデバイスへの通知が必要な場合は、これを永続化する必要があります。

なお、すでに登録されているデバイストークンを何度登録しても、正常に動作します(べき等性がある)。ただし、すでに登録されているデバイストークンが、異なる attribute(具体的にはUser Data)を持っている場合は例外が発生するので注意が必要です。こういったことは、AWSコンソールとコード両方からデバッグテストなどをしているときに発生しやすいと思います。

また、エンドポイントの Enabled が false になっていることをチェックしたければ、以下のように Attributes を指定してあげれば例外のほうに流れてくれます。

トピックの作成・購読

デバイストークンをアプリケーションに登録するとエンドポイントが発行されます。エンドポイントへ個別に通知を送るのも良いですが、一斉におなじ内容の通知を送りたいこともあると思います。そんなときにはトピックというものが便利です。トピックにエンドポイントを紐付けておけば、トピック宛に通知を送るだけで、紐付いているエンドポイントに一斉にメッセージを送ることができます。トピックを作る場合は SnsClient#createTopic を使います。

続いてエンドポイントが TopicArn を購読するように処理を走らせます。

このようにして、エンドポイントにトピックを購読させることができます。すでに無効になっているエンドポイントについても購読処理は正常に行うことができます。

プッシュ通知の送信

Amazon SNSを利用すれば、トピック、アプリケーション、各デバイスなど様々な粒度でPUSH通知を送ることができます。通知には SnsClient#publish を使います。

無効なエンドポイントに通知を行った場合、例外が発生します。ただし、トピックへ送った場合で、サブスクライバーの中に無効なエンドポイントがあっても正常に処理が行われます。JSONでメッセージを送る際には MessageStructure => 'json' をパラメタに追加してください。

エンドポイントの整理

デバイストークンは一定条件を満たすと失効します。失効したデバイストークンに対応するエンドポイントは Enabled = false となりますが、自動で消えてくれるわけではありません。Amazon SNSにはエンドポイントの状態変化を通知してくれる機能がありますので、それを利用して無効になったエンドポイントを削除する処理を走らせます。

まずは、エンドポイントの状態変化を通知するためのトピックを作成しましょう。そして、AWSコンソール SNS内の Applications で今回の処理を追加したい対象のアプリケーションを選択し、Actions -> Configure events へと進みます。そして「Endpoint updated」内に先ほど作成したトピックのARNを入力します。これで Endpoint の状態が変化したときに通知が飛ぶようになりました。データはJSONで飛んできますので、コンソールからよしなに処理先への購読処理を行ってください。

通知が来たら、エンドポイントの状態確認を行い、Enable = false だったらエンドポイントの削除処理を行います。

Cの基本的なコードとアセンブリコード

if文

絶対値を返す簡単な関数について見てみる。まずは最適化なし(-O0

cmp, jle, jmp 命令などによって if 文のふるまいが表現されていることがわかる。符号を反転させる部分は以下のような感じ(Swift)

続いて最適化あり(-O2

cmovl は conditonal move if less だそうで、つまり第1オペランドが第2オペランドより小さかったら第1オペランドへロードするというものらしい。だいぶ処理が軽くなっているのがわかる。

末尾呼び出しの最適化

末尾呼び出しでない階乗関数

Cのコードはこんなかんじで。

アセンブリコードを見てみる。

再帰が深い場合、スタックを食いつぶすことがわかる。続いて末尾再帰版。

最適化あり(-O1)で生成したコード。-O2 だとちょっと読むのが厳しいくらい最適化されたコードが生成されてしまったので…。

末尾呼び出しがジャンプ命令に置き換わっているのがわかる。コールスタックを消費しない形式となっている。

Hello, world

Cのコードを掲載するまでもないと思うんですが、まあ一応掲載

-O2 で生成したコード。

あんまり面白くなかった。”hello, work!” という文字列への参照を rdi レジスタにぶち込んで _puts よんでるだけだった。最後に xor eax, eax しているのは main 関数の return 0 を表現しているのだろう…。

参考文献

  • http://kira000.hatenadiary.jp/entry/2014/08/26/052447
  • https://codezine.jp/article/detail/485

関数呼び出しとアセンブリコード

関数呼び出しとアセンブリコード

次のような簡単な関数について見ていく。return_twotwice 関数を呼び出している。

こいつはどのようなアセンブリコードになるのだろうか。とりあえず clang -S -mllvm --x86-asm-syntax=intel call.c をしてみる。

_return_two はいつも通り、rbp レジスタの内容を退避させる。その後、twice 関数を呼び出しをおこなう。twice 関数は引数をひとつ取る関数で、どうやら edi レジスタに格納することにより、引数の引き渡しを実現しているようだ(mov edi, 1)。次に call 命令により _twice ラベルへと制御を引き渡している。

_twice でもいつも通り、rbp レジスタの内容を退避させる。それから、edi レジスタから引数を受け取り、ローカル変数と同じような感じで rbp-4へ格納する。その後の edi 領域を同じ値で上書きしている処理が見えるが、この意図はよく分からない。最適化をかけるとこういった処理はなくなるので一旦無視。次に edi レジスタの値を 1bit左シフトした値を、eax レジスタに格納して、この手続きは終了する。

最適化をかけた状態のアセンブリコードも見てみよう。clang -O2 -S -mllvm --x86-asm-syntax=intel call.c という感じでやってみると以下のような具合。

_return_two のほうに関しては、関数の戻り値が常に 2 になるために、もはや _twicecall すら走らない形に最適化されている。

_twice についてみてみると lea という命令が目につく。lea <src>, <dest> 命令は、scr のアドレスを計算し、dest にロードするというものです。lea はアドレス計算に使われるものではあるが、足し算を実現するのにも使われるようだ。64bit汎用レジスタ rdi の値を足し合わせて eax レジスタに格納している。なお、lea vs add についてはこの記事が詳しそう。

One significant difference between LEA and ADD on x86 CPUs is the execution unit which actually performs the instruction. Modern x86 CPUs are superscalar and have multiple execution units that operate in parallel, with the pipeline feeding them somewhat like round-robin (bar stalls). Thing is, LEA is processed by (one of) the unit(s) dealing with addressing (which happens at an early stage in the pipeline), while ADD goes to the ALU(s) (arithmetic / logical unit), and late in the pipeline. That means a superscalar x86 CPU can concurrently execute a LEA and an arithmetic/logical instruction.

複数の引数を取る関数

複数の引数を取る関数についても見ていこう。

このCコードはどのようになるだろうか。-O2 で最適化をかけたアセンブリコードを見てみる。

_add2 は引数が edi, esi に格納されて渡されるようだ。_add3edi, esi, edx から引数を受け取っている。

グローバル変数とローカル変数とアセンブリコード

次のようなグローバル変数がどのように扱われるかを確認する

clang -S -mllvm --x86-asm-syntax=intel global.c で以下のようなコードが生成される。

なるほど、グローバル変数 aDATA セクションにラベル _a が振られているようです。2倍の演算は shl によって左ビットシフトすることにより実現しているようです。

未定義のグローバル変数に関しては .comm という擬似命令を使って表現されるようです。次のCコードで確認してみます。

.comm についてリファレンスには以下のように記載してあります。

.comm name, size,alignment
The .comm directive allocates storage in the data section. The storage is referenced by the identifier name. Size is measured in bytes and must be a positive integer. Name cannot be predefined. Alignment is optional. If alignment is specified, the address of name is aligned to a multiple of alignment.

今回 add 関数は a1 を3倍した値を返すという内容にしてみました(名前変えるのわすれてた)。すると imul という命令が登場しました。こいつは単純なSigned Multiply を実現する命令です。dword ptr [rip + _a1] の値を 3倍して eax レジスタに格納するといったことをやっています。どうやら2のべき乗のときだけ shl 命令を使い、それ以外のときは imul 命令が使われる雰囲気がある。

ローカル変数

対してローカル変数はどう扱われるのか。簡単なCコードで確認してみる。

最適化をかけずにアセンブリコードを生成。

.cfiを省いたアセンブリコード。各行にコメントを付与した。_add で関数のおきまりの処理(push rbp, mov rbp, rsp)を行ったあと、rbp はスタックポインタと同じ位置を指しているはずだ。int b = 10; というコードはスタック領域に積む形で値 10 が格納されることにより実現されていることがわかる。ただし rsp は進んでいないため、スタックにプッシュしたことにはならない。すなわち、関数の外側からはローカル変数には(通常)アクセスできないような仕組みになっている。なるほど…という感じだ。あとは eax に演算処理結果を突っ込んでいって ret するという流れのようです。

ところで、このCコードにおけるローカル変数 b は明らかに無駄なコードです。最適化オプションをオンにして生成されるアセンブリコードを見てみます。

今度は、rbp-4 へ 10 の格納を行わず、直接 eax レジスタへ定数 10 を加算しているのが見て取れる。なるほどなぁ…って感想です。

参考文献

  • https://ja.wikibooks.org/wiki/X86%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9/GAS%E3%81%A7%E3%81%AE%E6%96%87%E6%B3%95
  • http://www.mztn.org/slasm/arm07.html
  • http://milkpot.sakura.ne.jp/note/x86.html
  • http://www7b.biglobe.ne.jp/~robe/pf/pf001.html
  • https://docs.oracle.com/cd/E26502_01/html/E28388/eoiyg.html
  • http://x86.renejeschke.de/html/file_module_x86_id_138.html
  • https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-39/index.html

定数を返すだけの関数のアセンブリコード

いろいろあって、必要に迫られアセンブラの勉強を始めた。基本的に知識が全くないので非常に低レベルな自分用のまとめです。

定数を返すだけの関数

とりあえず定数を返すだけの関数を定義してみる。

clang -S -mllvm --x86-asm-syntax=intel const.c で生成されたファイルが以下のような具合。

.globl はリンカに渡す名前を定義する擬似命令。.align 4, 0x90 については、4の倍数のアドレスに配置してくれという命令になるようです。_one はC言語にもあるようなラベルで使い方もだいたい同じ。

cfi_startproc は全然わからなかったので調べたらなんとなく説明があるページをみつけた。

.cfi_startproc is used at the beginning of each function that should have an entry in .eh_frame. It initializes some internal data structures. Don’t forget to close the function by .cfi_endproc.

ふむ。とりあえず cfi から始まる擬似命令は Call Frame Information とよばれるものに関する何かなようだ。よくわかっていない。stack overflow にそれらしき内容の質問があった。特定のプラットフォーム下では例外処理の際に Call Frame Information を利用しているそうな。とりあえず cfi ディレクティブを外してみていくのが良さそうなので、一旦削ったものを以下に示す。

(1) の push rbp では、rbp レジスタの内容をスタックに push している。(2) の mov rbp, rsp は、スタックポインタ rsp の値を rbp レジスタにセットしている。これらは何のために行なわれているのでしょうか。

関数の処理に入る前に rbp レジスタがどのように使われていたのかはわからないのですが、関数内ではこのレジスタを使います。ということは関数の処理を終える際に、rbp レジスタの値をもとに戻せないと困ります。もとに戻す処理は実際 (4) で行われています。

実際、pushpop の命令は次のようなものとおなじになります。

スタックポインタの値は push するとマイナス方向へ進み、pop するとプラス方向に進みます。

(2) ではスタックポインタ rsp の値を rbp に格納しています。これは mov 命令の [] を用いたアドレス指定に rsp レジスタを指定できない決まりになっているためらしいです。したがって、このように rbp に一旦移し、それを使って処理を記述していくことになります。

C言語では関数の戻り値を eax レジスタで返すことになっているため、(3) の処理では、eax に定数 1 を突っ込んでいます。(4) の処理で rbp の値をもとに戻して、 (5) の ret でスタックの値をみて制御を関数が呼ばれる前に記憶した位置に戻します。

【残ってる疑問】このケースの場合、rbp レジスタを利用していないので、(1)(2)(4)の処理は外せるのでは?

参考ページ

  • http://d.hatena.ne.jp/suu-g/20080510/1210408956
  • https://sourceware.org/binutils/docs-2.24/as/CFI-directives.html#CFI-directives
  • http://msumimz.hatenablog.com/entry/2014/02/19/214605