テクノロジー・リーダーシップ
不変と所有権管理によるソフトウェア品質向上
2018年4月16日
カテゴリー テクノロジー・リーダーシップ
記事をシェアする:
著者: 花井 志生
グローバル・ビジネス・サービス Cloud Application Developmen Consulting IT Specialist, Super Developer
インテルの創始者であるムーアが、ムーアの法則を1965年に提唱してから50年以上経過した。Wikipediaに掲載されているグラフを見ると、トランジスタ数がきれいに対数目盛に乗っており、今後もCPUの性能が向上し続けていくことが期待される。一方で近年CPUの動作クロックが伸び悩んでいる。
これは単純な物理法則に支配されている。光の進む速さは真空中なら秒速30万kmだが、物質の中では遅くなる。仮に半導体内では15万kmだとしよう。現在のハイエンドのCPUクロックは4GHzくらいなので、1クロックの間に光が進める距離を計算すると、15,000,000,000 / 4,000,000,000 = 3.75cm となる。
一番速度の速い光でさえ、1クロックの間に4cm弱しか進めない。そんな世界にCPUは到達してしまったのだ。このためここ数年、CPUのアーキテクチャに大きな変化が生じている。1つのCPU上に多くのコアを集積する、あるいはGPUや周辺チップを混載する方向へと向かっているのだ。
マルチスレッド・プログラミングの難しさ
アプリケーションで多くのコアを活用するには、アプリケーションを並列化する必要がある。現在最もポピュラーなのがスレッドを用いた高速化だろう。しかしマルチスレッド・プログラミングは非常にやっかいである。
import java.util.ArrayList; import java.util.List; public class Foo { public static void main(String... args) throws Exception { final List list = new ArrayList(); Thread t0 = new Thread(() -> list.add("Hello")); Thread t1 = new Thread(() -> list.add("World")); t0.start(); t1.start(); t0.join(); t1.join(); System.out.println(list); } }
リスト1の問題は、ArrayListがスレッド・セーフでないのに、マルチスレッドで更新している点にある。Javaにおけるマルチスレッド・プログラミングには、以下のような問題がある。
– スレッド安全性の問題がコンパイル時に発見できない
– テストでも発見が難しい
– 本番環境で発生しても、テスト環境で同一事象を再現することが困難な場合が多い
Immutableなオブジェクトを用いてスレッド安全性に関する問題を解決する
スレッド安全性に関する問題の多くが、複数のスレッドでMutable(変更可能)なオブジェクトを更新することにより発生する。このためImmutable(変更不可)なオブジェクトを用いることで、こうした問題の多くが回避できる。
import scala.concurrent.{Future, Await} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration object Foo { def main(args: Array[String]) { val list = List() val f0 = Future { "Hello"::list } val f1 = Future { "World"::list } val result = for { l0 <- f0 l1 <- f1 } yield l0 ++ l1 println(Await.result(result, Duration.Inf)) } }
リスト2は、Immutableなオブジェクトを用いたプログラムの例である。ListはImmutableなので、複数スレッドで共有しても安全である(実際のところ、List()自体がシングルトンなので、listを共有する必要は無いが、ここではJavaのコードとの対比のために、敢えてこのように記述している)。またfor内包表記を用いることで、複数のスレッドの実行結果を簡単に組み合わせることが可能で、マルチスレッド処理を安全にかつ柔軟に記述することが可能となっている。
Immutableなオブジェクトを用いると、オブジェクトのライフサイクルが単純になり、また副作用を伴わないためプログラムの見通しが良くなる。また不変であることを利用して、思い切った最適化を行うことが可能となるケースがある。一方で、プログラムが外界とデータをやりとりするには、何らかの副作用が必要である。このため、現実のプログラムは全てをImmutableなオブジェクトと副作用の無い処理のみで構築できるわけではないし、時としてImmutableなデータ構造では効率が悪くなるケースもある。それでもImmutableなオブジェクトを用いたアプローチは、マルチスレッド・プログラミングにおける強力な手法の1つだ。
Rustの型システムと所有権管理を組み合わせたアプローチ
マルチスレッド・プログラミングの諸問題に対して、別のアプローチで取り組んでいるのがRustである。Rustは2006年にグレイドン・ホアレにより開発され、今はMozilla Researchの公式プロジェクトとして活発に開発が続けられている。リスト3は、Rustでリスト1と同様のプログラムを書いた例である。
use std::thread; fn main() { let mut vec = vec![]; let t0 = thread::spawn(move || { vec.push("Hello"); }); let t1 = thread::spawn(move || { vec.push("World"); }); t0.join().unwrap(); t1.join().unwrap(); println!("{:?}", vec); }
しかし、このコードはコンパイルできない。
error[E0382]: capture of moved value: `vec` --> src/main.rs:11:9 | 6 | let t0 = thread::spawn(move || { | ------- value moved (into closure) here ... 11 | vec.push("World"); | ^^^ value captured here after move | = note: move occurs because `vec` has type `std::vec::Vec`, which does not implement the `Copy` trait
moveというキーワードにより、vecの所有権がクロージャ側に移る。このため、11行目でメインスレッドからアクセスしようとしても、所有権が無いためにエラーになるのだ。Rustは、型システムと所有権の規則を巧みに用いることで、スレッド安全性の問題をコンパイル時に発見することができる。vecが複数の場所でアクセスできるように、スマートポインタ(Rc)を用いた例がリスト4である。
use std::thread; use std::cell::RefCell; use std::rc::Rc; fn main() { let vec = Rc::new(RefCell::new(Vec::::new())); let cln0 = vec.clone(); let t0 = thread::spawn(move || { cln0.borrow_mut().push("Hello"); }); let cln1 = vec.clone(); let t1 = thread::spawn(move || { cln1.borrow_mut().push("World"); }); t0.join().unwrap(); t1.join().unwrap(); println!("{:?}", vec.borrow()); }
しかし、これもエラーとなる。
error[E0277]: the trait bound `std::rc::Rc<std::cell::RefCell<std::vec::Vec>>: std::marker::Send` is not satisfied in `[closure@src/main.rs:9:28: 11:6 cln0:std::rc::Rc<std::cell::RefCell<std::vec::Vec>>]` --> src/main.rs:9:14 | 9 | let t0 = thread::spawn(move || { | ^^^^^^^^^^^^^ within `[closure@src/main.rs:9:28: 11:6 cln0:std::rc::Rc<std::cell::RefCell<std::vec::Vec>>]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<std::cell::RefCell<std::vec::Vec>>` | = note: `std::rc::Rc<std::cell::RefCell<std::vec::Vec>>` cannot be sent between threads safely = note: required because it appears within the type `[closure@src/main.rs:9:28: 11:6 cln0:std::rc::Rc<std::cell::RefCell<std::vec::Vec>>]` = note: required by `std::thread::spawn`
Rcは参照カウントを管理することで、オブジェクトのライフサイクルを管理するが、カウンタの更新がスレッド・セーフではないため(スレッド・セーフにするためには、パフォーマンス上のペナルティがあるため、Rcはスレッド・セーフにしないという設計上の選択がされている)、クロージャに渡すことはできないのだ。これはSendと呼ばれるマーカ・トレイトを使用することで実現している。最後に正しく動作する例を見てみよう(リスト5)。
use std::thread; use std::sync::{Mutex, Arc}; fn main() { let vec = Arc::new(Mutex::new(Vec::::new())); let cln0 = vec.clone(); let t0 = thread::spawn(move || { cln0.lock().unwrap().push("Hello"); }); let cln1 = vec.clone(); let t1 = thread::spawn(move || { cln1.lock().unwrap().push("World"); }); t0.join().unwrap(); t1.join().unwrap(); println!("{:?}", *vec.lock().unwrap()); }
スマートポインタとして、スレッド・セーフ(アトミック)なArcを用い、ミューテックスを入れ物とすることでコンパイル、実行できるようになる。Arcは、リファレンス・カウンタの更新をスレッド・セーフに行うため、スレッドのクロージャに安全に渡すことができる。またMutexはアクセスの際に排他的ロックを必須とすることで、複数スレッドでオブジェクトを共有することができる。このようにRustではスレッド安全性の問題をコンパイラによって発見することが可能となる。もちろんRustの仕組みを用いたとしても、全ての同時並行処理の問題が発見できるわけではない(例:デッドロックやライブロックなど)。それでも、従来悩まされてきた多くのスレッド安全性の問題がコンパイラによって発見できるという点は画期的であろう。
まとめ
CPUのトランジスタ数は、ムーアの法則に従って伸び続けているが、シングル・コアあたりの性能は頭打ちとなっている。このため今後はマルチスレッド・プログラミングの必要性が高まっていくことが予想される。従来、マルチスレッド・プログラミングは難易度の高いものであったが、その解決策として、Immutableなオブジェクトを用いるケース(Scala)と、Rustの型システムと所有権管理を組み合わせた取り組みを紹介した。今後は、こういったマルチスレッド・プログラミングを、生産性高く、かつ高品質に行える言語の重要性が増していくだろう。
女性技術者がしなやかに活躍できる社会を目指して 〜IBMフェロー浅川智恵子さんインタビュー
ジェンダー・インクルージョン施策と日本の現状 2022年(令和4年)4⽉から改正⼥性活躍推進法が全⾯施⾏され、一般事業主⾏動計画の策定や情報公表の義務が、常時雇用する労働者数が301人以上の事業主から101人以上の事業主 […]
Qiskit Runtimeで動的回路を最大限に活用する
私たちは、有用な量子コンピューティングのための重要なマイルストーンを達成しました: IBM Quantum System One上で動的回路を実行できるようになったのです。 動的回路は、近い将来、量子優位性を実現するため […]
Qiskit Runtimeの新機能を解説 — お客様は実際にどのように使用しているか
量子コンピューターが価値を提供するとはどういうことでしょうか? 私たちは、価値を3つの要素から成る方程式であると考えます。つまりシステムは、「パフォーマンス」、「機能」を備えていること、「摩擦が無く」ビジネス・ワークフロ […]