GoでGoな非同期処理を試してみよう!
非同期処理、皆さんどうコードを書いてますか?
少し前のこと。弊社の若手(?)の方が、『非同期処理めんどくさい!』みたいなことを騒いでいたのですが、そもそも皆さんどうやって非同期処理のソースコードを書いているのだろう?とふと思ったわけです。そもそも『非同期』の定義ってなんだろうという話もでてきそうですが、ぱっと思いつくのは以下のような書き方でしょうか?
- スレッド作成してそこで実行
- async/await を使用して実行
最近の主流は async/await でしょうかね。こちらは多くの言語に実装されています。
ただ私の場合は仕事ではほとんどPythonを書いているのですが、Pythonでのasync/awaitはやや手順が面倒です。他の言語でも似たりよったりでしょうか。
非同期処理が得意なプログラミング言語、それがGo!
ところが、そんなめんどくさい非同期処理を簡単に実現してしまうプログラミング言語が存在するんです。
それは、言わずとしれたGoogle製、Goですね!
場合によっては、『Go言語』とか『GoLang』などとも呼ばれています。検索する場合はそうしないと全然別のものが引っかかってしまいそうですし。。
今回はそのGoを使用して、どのように非同期処理を書いていくかを紹介していきます。
続きを読むRustで設定ファイルを複数読み込む
Rustでの標準的・簡易的な設定ファイルの実装方法と問題点
開発したソフトの実行環境が変わるたびにコーディング・コンパイルをしなおすことのないよう、設定情報をソースコードとは別ファイルにしたり、実行時のOSの環境変数から取得したりする設計は常套手段です。
例えば、Rustの標準クレート「std::env」を使うと、「let var = env::var("TEST").unwrap();」などとするだけで、当プログラムを実行したときに環境変数「TEST」に値が設定されていれば、変数「var」に環境変数「TEST」の設定値が入ります。
また、クレート「dotenv」を使って、実行プログラムと同じディレクトリに「.env」というファイルを作成し、そこに設定情報を記載しておき、読み取るコード側では定数をまとめた構造体を作って解析させる、という方法が用いられていたりします。
ですが、これらの手法では、設定情報が多くなる場合(メッセージやSQLなども設定ファイルにする場合など)に、アプリの保守がしづらくなります。
実行前に大量に環境変数に値を設定するのは現実的ではありませんし、「dotenv」で「from_path」メソッドを使っても、複数のファイルを読み込ませることができないようにみえます。
自由度が高く保守しやすい設定ファイル設計に
そこで、今回は以下のような仕組みを作りましたので、紹介します。
- 実行ファイルと同じディレクトリに、設定ファイル群を保存する「config」ディレクトリを生成しておく。
- 「config」ディレクトリには、設定ファイルを任意のファイル名でいくつ保管してもよい。配下にディレクトリをいくつ生成してもよい。
- 設定ファイルは「toml」形式とし、拡張子も「.toml」とする。(「config」ディレクトリ配下の「.toml」ファイルだけ読み込む)※Rustの依存ライブラリ定義ファイル(cargo.toml)も「toml」形式なので、合わせました。
実装
オリジナル設定ファイル例(config\config.toml)
[csv.output] file_path = "{CUR}//data//output_%Y%m%d_%H%M%S.csv" char_code = "shift-jis" [calc] value1 = 10 value2 = 20
Rust依存関係設定ファイル(cargo.toml)
[package] name = "config-file-test" version = "0.1.0" [dependencies] lazy_static = "1.4.0" toml = "0.5.6"
Rustソース(main.rs)
以下のように設定値を呼び出します。
#[macro_use] extern crate lazy_static; extern crate toml; mod initialize; use initialize::file_config::CONFIG; fn main() { // 「as_str()」メソッドにて、文字型として呼び出しています。 // 初めて「CONFIG」が参照されるタイミングで「file_config.rs」が実行され、設定ファイルの内容がメモリに展開されます。 let char_code = CONFIG["csv"]["output"]["char_code"].as_str().unwrap(); println!("char_code = {}", char_code); // 標準出力結果「char_code = shift_jis」 // 「as_integer()」メソッドにて、数値型として呼び出しています。 // このタイミングでは、すでにメモリに展開された値を参照しているだけです。 let value1 = CONFIG["calc"]["value1"].as_integer().unwrap(); let value2 = CONFIG["calc"]["value2"].as_integer().unwrap(); println!("value1 + value2 = {}", value1 + value2); // 標準出力結果「value1 + value2 = 30」 }
読み込む設定の定数の型を予め構造体(struct)で定義しておく手段もありますが、定数が増えるごとに構造体も増やしていく作業が面倒なので、上記のような参照方法を紹介しました。 ただ、参照の都度「as_~()」メソッドで型変換するので、処理性能を追求する必要がある場合は、構造体で型を定義しておいたほうがいいかもしれません。
Rustソース(initialize\mod.rs)
本編には関係ないですが、設定ファイルは一度読めばいいので、主処理と切り離しやすいようモジュールにしています。
pub mod file_config;
Rustソース(initialize\file_config.rs)
設定ファイル(.toml)を解析してメモリ上に展開し、上記のように呼び出せるように準備する処理です。 設定ファイルが存在しなければ、新たに生成します。 その場合の初期値は、以下の定数「DEFAULT_CONFIG」に書いています。
use std::io::{BufReader, Read, BufWriter, Write}; use std::fs; use std::fs::{File, DirBuilder}; use std::path::Path; use std::env; use toml::Value; lazy_static! { pub static ref CONFIG:Value = { return load_config(); }; } const DEFAULT_CONFIG: &'static str = r#" [csv.output] file_path = "{CUR}//data//output_%Y%m%d_%H%M%S.csv" char_code = "shift-jis" [calc] value1 = 10 value2 = 20 "#; // toml形式の設定ファイルを読み込む fn load_config() -> Value { // 当プログラムのディレクトリ let cur_path_str = env::current_exe().unwrap().clone(); let cur_path = Path::new(&cur_path_str); let cur_dir = cur_path.parent().unwrap().display(); // 当プログラムのディレクトリ配下に存在する該当拡張子のファイルパスを取得 let file_paths = get_path(Vec::new(), &cur_dir.to_string(), "toml"); // toml形式変換前の設定文字列 let mut conf_toml_str = String::new(); // 全ファイルをテキストで読み込み for path in file_paths.iter() { conf_toml_str = format!("{}{}", conf_toml_str, get_text_file(Path::new(&path), "toml")); } // 設定を1件も取得できていなければ if conf_toml_str.len() < 1 { // 設定ファイルを保存するディレクトリを生成 let path_str = format!("{}//config//config.toml", &cur_dir); let path = Path::new(&path_str); let dir = path.parent().unwrap(); DirBuilder::new().recursive(true).create(&dir).unwrap(); // 設定ファイルのパスを書き込みモードで開く。これは`io::Result<File>`を返す。 let file = match File::create(&path) { Err(e) => panic!("couldn't create {}: {}", path_str, &e.to_string()), Ok(file) => file, }; // バッファリングされたストリーム出力とする let mut f = BufWriter::new(file); // 設定ファイルへ記述 conf_toml_str = DEFAULT_CONFIG.to_string(); match f.write_all(conf_toml_str.as_bytes()) { Err(e) => panic!("couldn't write {}: {}", path_str, &e.to_string()), Ok(_) => println!("{} writes :{}\n", path_str, conf_toml_str), } } // 文字列内に「{CUR}」が存在すれば、当プログラムが存在するディレクトリとみなして、カレントディレクトリに置換 conf_toml_str = conf_toml_str.replace("{CUR}", &format!("{}", &cur_dir)); // 設定をtoml形式に変換して返す conf_toml_str.parse::<Value>().expect(&format!("couldn't parse config file to toml format.{}", &conf_toml_str)) } fn get_path (mut files: Vec<String>, target:&str, path_ext:&'static str) -> Vec<String> { for file_path in fs::read_dir(target).unwrap() { let file_path = file_path.unwrap().path(); if file_path.is_dir() { files = get_path(files, &file_path.display().to_string(), path_ext); } else { match file_path.extension() { _path_ext => files.push(file_path.display().to_string()), } } } files } fn get_text_file (path: &Path, extension: &'static str) -> String { let display = path.display(); match path.extension() { None => return "".to_string(), Some(ext) => { if ext != extension { return "".to_string(); } }, }; // pathを読み込み専用モードで開く let f = match File::open(&path) { Err(e) => panic!("couldn't open {}: {}", display, &e.to_string()), Ok(f) => f, }; // バッファリングされたストリーム入力とする let mut br = BufReader::new(f); // ファイルの中身を文字列に読み込み let mut conf_toml_str = String::new(); match br.read_to_string(&mut conf_toml_str) { Err(e) => panic!("couldn't read {}: {}", display, &e.to_string()), Ok(_) => println!("{} contains:\n{}", display, conf_toml_str), } conf_toml_str }
これで拡張子「.toml」のファイルを「config」フォルダに入れておけば、実行の都度全パラメータを自動で読みこみ、Rust内どのモジュールでも「use initialize::file_config::CONFIG;」だけ記述すれば参照できる状態になります。 もし設定ファイルがない場合でも、自動で「config」フォルダとその中に「config.toml」を生成し、ハードコードした初期値を書き込みます。
「rustc 1.42.0」でコンパイルできることを確認しています。
The Continuing Story of Error Correction Code 2
単一パリティ符号とハミング符号
お約束
これからエラー検出・訂正について考えていく訳ですがエラー検出・訂正をするからには何か入力があって、それをどこかに送るなり保存するなりして、受け取った、あるいは読みだしたら間違えているかもしれないからエラーがあるか検査して、その結果エラーがあれば訂正できるなら訂正するわけです。
このシステムは実際には、線でつながっているシリアル通信かもしれないし、衛星通信かもしれないし、CPUにつながっているメモリかもしれないし、はたまたハードディスクに対する読み書きかもしれません。
ただ、エラー検出・訂正についてのみ考えたいのでこれからは以下のような単純なモデルを考えてみます。
送信語
送信者が送信したいデータのビット列。符号器
送信語に誤り検出・訂正のための加工を施す装置。符号語
符号器により送信語に誤り検出・訂正のための加工を施したビット列。通信路
送信側から受信側へ符号語を送る経路。符号語は通信路を通過する際に誤りが加わる可能性がある。受信語
通信路を通って受信した符号語のビット列。
通信路でエラーが発生していなければ符号語と同一となる。復号器
受信語から送信語と思われるビット列を取り出し受信者に渡す。
シリアル通信をイメージしている感じですが通信路をメモリやハードディスクと考えればコンピューターシステムでしょうし、電波だと考えれば無線システムと考えることができます。
単一パリティ符号
送信語の各ビットの中で1であるビットの数が偶数になるように検査ビットを付加する方法を偶数パリティ、1であるビットの数が奇数になるように検査ビットを付加する方法を奇数パリティといいます。
RS232Cで使用されているのがこの単一パリティ符号ですね。
偶数パリティとか奇数パリティと呼ばれているやつです。
偶数パリティであれば、送信符号の1のビット数を数えた結果が奇数であれば検査ビットとして1、偶数であれば検査ビットとして0を付加するだけです。符号語の1であるビット数は必ず偶数となります。奇数パリティなら符号語の1であるビットが奇数になるように検査ビットが付加されます。
受信側では受信符号の1の数を数えてこれが偶数であれば受信符号は正しいと認識し、1の数が奇数であれば間違えていると認識します。
(厳密に言えば奇数個の誤りは検出できるが偶数個の誤りは検出できない、となるため偶数ビット誤った場合は正しいと認識してしまいます)
単一パリティ符号は送信語のビット数が何ビットでも構わない、という特徴があります。1のビット数を数えるだけなので送信語は何ビットでも構わないんですね。
ただ、間違ってることはわかってもどこのビットが間違えているのかは分かりません。どこのビットが間違えているのか分からないのだから訂正することもできません。(もし、どこのビットが間違えているのか分かるのならそのビットを反転(1だったら0に、0だったら1に)すれば訂正することができます)
まとめると単一パリティ符号は
- 奇数個の誤りを検出できる。
- 送信語のビット数に制約がない。
- 誤りの場所は分からない。
となります。
検査符号の計算方法
単一パリティ符号の検査符号は偶数パリティであれば、
- 送信語の1のビット数が奇数個であれば1。
- 送信語の1のビット数が偶数個であれば0。
でしたが、これを計算で求められるようにしておきましょう。
各ビットの値は0と1しかとりませんから送信語の各ビットを足していき、その合計が偶数か奇数かを判断すればよい訳です。
合計を2進数で考えると最下位のビットが0なら偶数、1なら奇数ですから合計の最下位だけを取り出せば偶数パリティとなります。
最下位ビットしか必要でないのだから最下位ビット以外は捨てて足し算する、ということにすると計算のルールは以下のようになります。
2進数では となりますが最下位ビット以外は捨てる、としたので となります。他は普通の足し算ですね。
この計算ルールでパリティ符号を計算してみましょう。
例として以下のような送信符号3ビット(、、)、検査符号1ビット()の単一パリティ符号を考えます。
送信符号3ビット(、、)を全部足した値が検査符号となるのですから偶数パリティの検査符号は
と表すことができます。
また、奇数パリティであれば
となります。
奇数パリティの検査符号は
- 送信語の1のビット数が奇数個であれば0。
- 送信語の1のビット数が偶数個であれば1。
ですから偶数パリティとは逆になります。だから偶数パリティを反転させればよい→1を足せばよい、ということです。
送信符号が何ビットあっても各ビットをひたすら上記足し算ルールで足していった値が検査符号になる、ということですね。
単一パリティ符号からハミング符号へ
単一パリティ符号はどこかに1ビットの誤りがあればそれを検出できるが、その誤りがどのビットなのかはわからない、という符号です。 誤りがどのビットなのかわからないのですから訂正することもできませんね。
そこで、複数のパリティ符号を付加して誤ったビットの位置を特定できるようにしたものがハミング符号となります。誤ったビットがわかるのですからそのビットを反転(0なら1、1なら0)してやれば訂正できたことになります。問題はどうやって誤ったビットの位置を特定するか、です。
以下の7ビットでハミング符号を構成してみます。
でも何故7ビット?8ビットのほうがきりが良いんじゃないの・・・
ハミング符号を8ビットで構成することはできますが、7ビットのハミング符号は符号理論の中ではきりの良いビット数になります。 理由については後々、数学的考察を行った上で考えてみることにします。 とりあえず、ここではそういうもんなんだ、ということにして先に進みます。
で、どうやって誤ったビット位置を特定するの?
1ビットのパリティでは誤ったビット位置を特定することはできませんが、複数のパリティを付加して誤ったビット位置を特定できるようにします。複数のパリティはそれぞれ異なる送信語の複数のビットを担当してもらいます。それぞれのパリティだけでは担当しているビットの何れかに誤りがある、ということしかわかりませんが複数のパリティの結果から誤りのビット位置を特定します。
では、ハミング符号の作り方、はじまりはじまり・・・
を配置する
は 、、を担当してもらいます。 、、、の何れかが1ビット誤りを起こすとが1になります。
を配置する
は 、、を担当してもらいます。 の隣に、更にその隣にを配置します。 、、、の何れかが1ビット誤りを起こすとが1になります。
を配置する
は 、、を担当してもらいます。 の隣にを配置します。 、、、の何れかが1ビット誤りを起こすとが1になります。
完成
まとめるとこうなります。
4ビットの送信語と3ビットのパリティで合計7ビットの符号語ができました。3ビットのパリティは以下の式により計算できます。
4ビットの送信語の組み合わせに対して生成されるパリティをすべて計算してみましょう。
0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 1 | 1 |
0 | 0 | 1 | 1 | 0 | 0 | 1 |
0 | 0 | 1 | 1 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 0 | 1 | 0 |
0 | 1 | 0 | 1 | 1 | 0 | 1 |
0 | 1 | 1 | 0 | 0 | 1 | 1 |
0 | 1 | 1 | 0 | 1 | 0 | 0 |
1 | 0 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 1 | 0 | 0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 | 0 | 0 | 0 |
1 | 1 | 1 | 1 | 1 | 1 | 1 |
試してみる
送信語が
のときにが誤って1になった以下のような受信語の場合を考えてみます。
復号器で計算するパリティを、、とすると
となり、受信語のパリティ
とは異なっていますからどこかに誤りが発生した、ということがわかります。
を確認する
受信語のは0ですが復号器で計算したは1です。ということはの担当しているビットに誤りがあった、ということですね。
、、の何れかに誤りがあるようで、少なくともは誤っていない、とわかります。
を確認する
受信語のは0ですが復号器で計算したは1です。ということはの担当しているビットも誤りがあった、ということですね。
の担当は 、、ですからこのうちの何れかに誤りがあったということになりますが、を確認したときには誤っていない、とわかっていますから誤りは、の何れかとなります。更には誤っていない、ということもわかります。
を確認する
受信語のは0ですが復号器で計算したは1です。ということはの担当しているビットも誤りがあった、ということになります。
の担当は 、、ですからこのうちの何れかに誤りがあったということになりますが、を確認したときには誤っていない、更にを確認したときにも誤っていないとわかっていますから誤りはである、ということがわかりました。
誤ったビット位置を簡単に特定する方法
パリティビットを順番に確認しながら誤ったビット位置を特定してきましたが、実は誤ったビット位置を特定する方法があります。 受信語のパリティは
ですが復号器で計算したパリティは
です。二つのパリティの異なっているビットを1としたビット列を作るとこのケースではすべてのパリティが異なっていますからというビット列が得られます。これを2進数として考えると10進数では7ですね。よって誤りがあったのはビット7であるです。はに対応していますから誤りがあったのはである、となります。
ただ、この方法はこうなるようにパリティ符号を構成したからであってパリティ符号すべてがこういう特徴を持っている訳ではありません。この辺も後々考えていきます。
でも、なんか信じられん・・・
ではもう少し試してみましょう。 送信語が
のときにが誤って受信語が以下のようになった場合、
復号器で計算したパリティは
受信語のパリティは
二つのパリティの異なるビットを1としたビット列はであり10進数で表すと5。誤りはビット5であるからでが誤っているよ、ということで検出成功です。
いや、じゃぁパリティが誤ったら?
送信語が
のときにが誤って受信語が以下のようになった場合、
復号器で計算したパリティは
受信語のパリティは
二つのパリティの異なるビットを1としたビット列はであり10進数で表すと4。誤りはビット4であるからでが誤っているよ、となります。
2ビット間違えたらだめじゃね?
はい、ここまでの話はあくまで1ビット誤ったらその位置が特定できるよ、ということで2ビット間違えた時は考えていません。2ビット、あるいはそれ以上誤ったらどうなるのでしょうか?誤りが検出できる?訂正は可能?
つっこみどころは色々ありますが、今回の話はここまで。
次回は、ハミング距離を考えてみましょう。
適応フィルタを作ってみる
信号処理の分野では、適応フィルタが色々な目的で使用されています。ちょっとした理由で適応フィルタのプログラムを作ってみました。
- ADF【Adaptive Filter】
- 適応フィルタの構成
- 適応アルゴリズム
- サンプルコード:NLMSで作ってみる
- FIRフィルタ
- サンプルコード
- ADF
- サンプルコード
- FIRフィルタ
ADF【Adaptive Filter】
適応フィルタ【Adaptive Filter】とは、出力が目的とする信号に近づく(収束する)ように係数を自動で更新していく適応信号処理で、応用例として代表的なものはエコーキャンセラやノイズキャンセラだと思います。
適応フィルタの構成
適応フィルタの構成は、適応アルゴリズムとフィルタ部です。 フィルタ部でd(n)に似たy(n)を合成、
入力x(n)と誤差e(n)に基づいて、フィルタ係数h(n)を最適係数に近づけます。
誤差e(n)のパワーを最小にする。
続きを読む思考のサルベージ(その7)
各工程で心がけたい思想を掘り起こしてみる
システムテスト工程末期、客先リリース直前での不具合修正と性能変化ついて考えます。
ちょっとした判定ミス
SWの動作としてはよくあるやつで、メインループで、実行命令を受信し、命令内容に従いHWを設定して後は命令実行完了の応答送信までHW任せ、次の実行命令が来ていれば同じことを繰り返し、来ていなければいったんsleep状態に入ります。 今回は、「特別なケース」の考慮が漏れていて、不具合につながっていました。
影響範囲
客先リリース直前です、不具合が直ればそれで良いとはなりません。修正による影響範囲を明確にしなければなりません。処理性能の側面でも考慮が必要です。今回は、メインループの中に判定分を1つ追加し、「特別なケース」の場合に関数コールをするという数ステップの修正で、他の不具合を引き起こすような修正ではありません。また、「特別なケース」は性能測定対象外なので、追加した判定分1つのオーバーヘッドは乗りますが、大きな性能劣化には繋がらないと判断されました。
思わぬ結果
不具合は解消されましたが、処理速度としては不利となるオーバーヘッドが増える修正なのに、何故か処理性能が数パーセント向上してしまいました。修正の結果必要な処理がスキップされている可能性も考えられます。逆に性能が劣化していれば、よけな処理が活性化している場合もあります。 調べてみると、オーバーヘッドが効いて、ループ処理の中の実行命令の受信タイミングが変わり、毎周期行われるようになり、sleepに入ることがなくなっていました。結果的に全体としての処理性能が向上していたのです。
HW依存
HW依存が大きい処理系では、ちょっとしたタイミングのずれで、処理性能に影響が出てしまいます。テスト工程初期であれば、さほど問題視されませんが、客先リリース前ともなると、例え性能が向上したとしても、もちろん劣化したとしてもですが、原因の究明は必須です。大切なことは、事前にあらゆる可能性を考慮し、起こりうることを見極めることです。
何か掘り起こせた?
- HW依存の大きい処理系では処理タイミングを考慮することが重要。
- 性能の変化点では、要因の究明は必須。
必要な修正は実装しなければいけないのだから、性能目標さえクリアしていて、論理的な説明さえできればなんの問題もありません。
おしまい
誤ってループ処理千回余計にやってました、なんて言ったら大目玉ですけどね。