【Flutter/Dart】排他制御 synchronizedパッケージ
排他制御とは
ある共有リソースに対して複数のプロセスが同時にアクセスを試みた場合でも、一度にただ一つのプロセスだけがそのリソースを使用できるようにする調整処理のことです。
効果的に利用されるシナリオ
排他制御が効果的に発揮される例には次のようなケースです。
予約システム
航空券や映画のチケット、ホテルの部屋などを予約するシステムでも、多くのユーザーが同時に同じリソース(座席や部屋)を予約しようとした場合には排他制御が必要となります。
データベースシステム
複数のユーザーが同時に同じデータ行を更新しようとする場合、一貫性を保つためには排他制御が不可欠です。
synchronizedパッケージ
Flutter/Dartでは、非同期処理への同時アクセスを防ぐためのロックメカニズムとして、package:synchronizedがあります。
- 非同期コードの排他処理を行う。
- クリティカルセクションのような扱いができる。
- 再起的ロックが必要な場合は、オプションで有効化できる。
- ロックの取得がタイムアウトする場合は、エラーを返すことができる。これによってデッドロックを防げる。
クリティカルセクションとは
一度に一つのスレッドだけがアクセスできる部分のことです。その部分ではデータの競合を防ぐため、他のスレッドは待たされます。
一度に一人しか入れない部屋と思うと分かりやすいかもしれません。
再起的ロック
スレッドが自分自身で何度でもロックできるようなロックのことを示します。
一度に一人しか入れない部屋(クリティカルセクション)の鍵を持っていて、誰も部屋には入れないけれど自分自身は何度でも出入りできる状況と想像すると良いかもしれません。
デッドロック
「お互いが待っている状態」で、どちらも行動できなくなることを指します。
2つの部屋があり、それぞれの部屋に入ろうとしている人々が交差した状態を想像してみてください。
- 部屋Aに入りたいAさんは部屋Bの鍵を持っている。
- 一方で、部屋Bに入りたいBさんは部屋Aの鍵を持っている。
各部屋には1人ずつしか入れないのですが、お互いの部屋の鍵を持ってしまい行き詰まった状態をデッドロックと呼びます。
これを回避する方法の1つに再起的ロックがあります。
サンプルコード
排他制御のイメージが付きやすいように、図書館の本を例にしてDart言語でサンプルコードを書いてみます。
図書館で一冊のとても人気の本を読みたいとします。でも、その本は一度に一人だけが借りることができます。あなたがその本を読み始めると、図書館のルールにより他の人はその本を借りることができません。本を読み終わって返すと、次にその本を読むために待っていた人が本を借りることができます。
まずは排他制御をしない場合の処理を書きます。
class Book {
List<String> content;
Book(this.content);
// 1行1秒ずつ時間をかけて読む
Future<void> read(String reader) async {
for (var page in content) {
log('$reader) $page');
await Future.delayed(const Duration(seconds: 1));
}
}
}
void main() {
Book momotaro = Book([
"むかしむかし、あるところに、おじいさんとおばあさんが住んでいました。",
"おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。",
"おばあさんが川で洗濯をしていると、大きな桃が流れてきました。",
"おばあさんは、その桃を拾って家に持ち帰り、おじいさんが芝刈りから帰ってくるのを待ちました。",
"おじいさんが帰ってきて、桃を切ってみると、なんと中から元気な赤ちゃんが出てきました。",
"おじいさんとおばあさんは、その子を桃太郎と名付け、大切に育てました。",
]);
// 鈴木さんが桃太郎を読みます
momotaro.read('鈴木');
// 田中さんが桃太郎を読みます
momotaro.read('田中');
}
桃太郎の本を鈴木さんと田中さんが借りたいと申請しました。
排他制御していない場合は、同時に読めてしまいます。
ログの結果はこのようになります。
[log] 鈴木) むかしむかし、あるところに、おじいさんとおばあさんが住んでいました。
[log] 田中) むかしむかし、あるところに、おじいさんとおばあさんが住んでいました。
[log] 鈴木) おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。
[log] 田中) おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。
[log] 鈴木) おばあさんが川で洗濯をしていると、大きな桃が流れてきました。
[log] 田中) おばあさんが川で洗濯をしていると、大きな桃が流れてきました。
[log] 鈴木) おばあさんは、その桃を拾って家に持ち帰り、おじいさんが芝刈りから帰ってくるのを待ちました。
[log] 田中) おばあさんは、その桃を拾って家に持ち帰り、おじいさんが芝刈りから帰ってくるのを待ちました。
[log] 鈴木) おじいさんが帰ってきて、桃を切ってみると、なんと中から元気な赤ちゃんが出てきました。
[log] 田中) おじいさんが帰ってきて、桃を切ってみると、なんと中から元気な赤ちゃんが出てきました。
[log] 鈴木) おじいさんとおばあさんは、その子を桃太郎と名付け、大切に育てました。
[log] 田中) おじいさんとおばあさんは、その子を桃太郎と名付け、大切に育てました。
今度は、排他制御をして1人1冊しか読めないようにします。
class Book {
List<String> content;
Book(this.content);
// Lockクラスを利用して同時アクセスを阻止します
final _lock = Lock();
Future<void> read(String reader) async {
// synchronizedメソッドを利用して、ブロックの範囲を決めます
await _lock.synchronized(() async {
for (var page in content) {
log('$reader) $page');
await Future.delayed(const Duration(seconds: 1));
}
});
}
}
main()関数は変更せずに、もう1度実行してみます。
[log] 鈴木) むかしむかし、あるところに、おじいさんとおばあさんが住んでいました。
[log] 鈴木) おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。
[log] 鈴木) おばあさんが川で洗濯をしていると、大きな桃が流れてきました。
[log] 鈴木) おばあさんは、その桃を拾って家に持ち帰り、おじいさんが芝刈りから帰ってくるのを待ちました。
[log] 鈴木) おじいさんが帰ってきて、桃を切ってみると、なんと中から元気な赤ちゃんが出てきました。
[log] 鈴木) おじいさんとおばあさんは、その子を桃太郎と名付け、大切に育てました。
[log] 田中) むかしむかし、あるところに、おじいさんとおばあさんが住んでいました。
[log] 田中) おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。
[log] 田中) おばあさんが川で洗濯をしていると、大きな桃が流れてきました。
[log] 田中) おばあさんは、その桃を拾って家に持ち帰り、おじいさんが芝刈りから帰ってくるのを待ちました。
[log] 田中) おじいさんが帰ってきて、桃を切ってみると、なんと中から元気な赤ちゃんが出てきました。
[log] 田中) おじいさんとおばあさんは、その子を桃太郎と名付け、大切に育てました。
鈴木さんが読み終わるまで田中さんは待っていることが分かります。
この図書館のルールは、コンピュータの世界の「排他制御」に似ています。図書館の本はコンピュータの中の「共有リソース」(データやファイルなど)に相当し、本を読む人々は「プロセス」または「スレッド」(タスクや作業を行う単位)に相当します。
多くの人(プロセスやスレッド)が同時に一つの本(共有リソース)を使おうとすると混乱が起きます。そこで「排他制御」があれば、一度に一つのタスクだけが共有リソースにアクセスでき、混乱を防ぐことができます。これが排他制御が効果的に利用される典型的なシナリオです。
メリット
データの整合性と一貫性の保証
排他制御により、複数のプロセスやスレッドが同時に共有リソースを操作することを防ぎ、データの整合性や一貫性を維持することができます。
競合の解消
二つ以上のスレッドがデータやリソースを同時に操作しようとした際に問題が起きないようにします。