見出し画像

プログラミング

本ドキュメントの利用は、https://github.com/kae-made/kae-made/blob/main/contents-license.md に記載のライセンスに従ってご利用ください。

まずは最初に、プログラミングに関するエッセンスを解説していきます。

プログラミングとは、プログラムを作成する作業のことです。
プログラムは、人が読み書き可能なプログラミング言語によって記述され、コンパイラによって機械が理解できる形式に変換(この過程をコンパイルと呼ぶ)され、実行環境(プラットフォーム)上で実行されます。標準入出力やファイル操作、ネットワーク通信、GUI等、様々なアプリケーションで利用可能な機能を提供する、コンパイル済みのプログラム(ライブラリと呼ぶ)があらかじめ提供されており、それを適切に選択して利用することにより、必要最小限のプログラミングで実現したい機能を提供するプログラムを開発可能です。また、実行環境(プラットフォーム)は、CPUやメモリ、各種アクセラレータ、周辺機器IOといった要素から構成されるハードウェアと、オペレーティングシステム(小規模HWの場合はファームウェアとも呼ぶ)と呼ばれるHWを使用するのに必要なOS、及びミドルウェアから構成されています。プログラムのコンパイルやライブラリとのリンクにより実行可能な形式のソフトウェアに変換する過程をビルドと呼びます。また、効率よくプログラムの記述、ビルド、デバッグ、テストを行うために、それらの作業を支援する機能を提供する開発環境が用意されています。

プログラム

プログラムは、そのプログラムが動作するための“データ構造”とそのデータ構造を使って目的を達成するための処理を記述する“ロジック”からなります。データ構造とロジックは適切な規模で複数束ねられ、パッケージ化されます。本稿ではパッケージ化されたものを“モジュール”と呼ぶことにします。

データ構造

プログラミングと言えばロジックのほうに目が行きがちですが、ロジックを描き始める前に、ロジックが依存するデータ構造を適切に設計することが非常に重要です。
プログラムが解決したい問題で扱うデータが適切に設計されてデータ構造化していれば、ロジックはシンプルかつコンパクトに記述でき、結果として、テストや運用時のバグ対応、機能拡張も含めたプログラム開発工数が削減されます。
プログラミングの容易さはデータ構造のできの善し悪しで決まるといっても過言ではありません。
データ構造設計は、

1. プログラムが解決したい問題で扱うデータをモデル化する
2. プログラミングに適したデータ構造に変換する
3. プログラム言語の特性に従ってプログラム化する

というステップで行います。最初のステップは、「Art of Conceptual Modeling ~ 概念モデリング教本」で解説している“概念情報モデル”の作成が相当します。
次のステップの、“プログラミングに適したデータ構造”ですが、プログラミングで使われるデータ構造は、大きく分けて

- 複数変数の並び
- 変数の繰り返し
- 複数変数のうちのどれか

の3種類で構成されます。

概念情報モデルと対応付けると

- 変数の並び ⇔ クラスの特徴値、データ型の複合型
- 変数の繰り返し ⇔ クラスのインスタンス、あるインスタンスに対して多重度が*、1..*でリンクされたインスタンス群
- 複数変数のうちのどれか ⇔ データ型の列挙型、スーパー・サブクラスの関係にあるサブクラス側のクラスのインスタンス

に相当します。
変数の繰り返しについては、概念情報モデルの場合のクラスのインスタンスセットは識別子の特徴値が一位になるような集合でしたが、プログラミングの場合は、同値の値が複数許される集合や、順序付けされたシーケンス、それぞれの要素にキーを付与したマップやDictionaryなどロジックの組み立てに応じて複数利用可能です。これらは、概念振舞モデルのアクション定義をプログラムに変換する際に適宜利用可能です。

概念モデリング教本」に則って概念情報モデルを作成している場合は、非機能要件を考慮して適切な実装技術を選択し、各ドメインの概念情報モデルの内容を、概念情報モデルの定義を使って作成された一定のルールに基づいて、プログラムに変換していけば、現実世界の状態を適切に保持できる、データ構造の実装が出来上がります。

データ構造はロジックの中だけでなく、ロジックを利用するためのインターフェイスや、インターフェイスを通じてロジック間で受け渡される一連のデータセットにも表れます。適切に設計されたデータ構造は適切なインターフェイスの定義にも役立ちます。
通常、ロジックのインターフェイスは使用するプログラミング言語の文法に従って宣言(define)しますが、ロジック間で受け渡されるデータの中身や一連のデータセットは、正規表現やAbstract Syntax Notation(ASN.1等)を用いて厳密に宣言を行います。

一過性と永続性

データには、ロジックが処理されている間だけ一時的に存在する、一過性のデータと、ロジックの処理が完了した後も存在し続ける永続性のデータがあります。永続性には二種類あって、ロジックが所属するアプリケーションが実行中の間だけ保持されるものと、アプリケーションの実行が完了した後も保持され、次のアプリケーション起動時にも参照可能なものです。後者には、HWの電源がONの間だけ参照可能で、電源をOFF・ONするとデータが消える場合もあるので、永続性が必要な場合は注意が必要です。

値型と参照型

プログラミングが扱うデータには、値型と参照型の二種類があります。値型は値そのものを保持しているデータで、参照型は値を保持している場所を指し示すデータです。後者は例えばC言語やC++のポインタ、C#やJavaのオブジェクト変数が該当します。プログラミング言語によって記述方法は千差万別で、特に意識しなくてもプログラムが記述できる場合もありますが、中級以下のプログラミング言語の場合には明確に意識してロジックを記述しなければなりません。

画像1

一般的な値型

通常のプログラミング言語で大抵定義されている値型は以下の通りです。

- short 16 signed/unsigned
- int 32 signed/unsigned
- long 64 signed/unsigned
- float 32
- double 64
- char 8 signed/unsigned

signed/unsigned がついているものは正負両方を保持可能、または、正のみを保持可能の二種類があります。高級言語の幾つかは、上の基本型に加え、文字列を格納できる基本型を持っています。
値型の変数が、その変数そのものが値を保持しているのに対し、参照型の変数は、他の変数がどこにあるかを指し示す情報を保持しています。参照型の変数の値を更新すると参照先が変わるだけで、それまで指示されていた情報は更新されません。参照型の変数はNULL値を代入することにより、どこも指していない状態を作りだせます。

CやC++のポインタは、参照型の変数の一例です。ポインタはその指し示す場所がメモリ上のアドレスに対応付けられており、ポインタの値を増減することにより、前後のメモリ領域に保持された値を参照可能です。この特徴を上手く使えば、ハイパフォーマンスで簡潔なコードを書くことができます。ただし、C/C++の様にメモリを直接参照可能なプログラミング言語では、参照してよいメモリ領域の範囲を超えたアクセスも許されているので、不用意に使うと、意図しない変数の更新やスタック領域を破壊してしまい、なんとも不思議な障害を発生する奇妙なプログラムを作ってしまう恐れがあるので注意が必要です。データ構造設計を適切に行うことは、この様な障害の発生を防ぐ有効な手段です。

画像2

 データ保持にかかるコスト

基本型のリストの項目それぞれに、そのデータをメモリ上に保持するのに必要なビット数を付けました。IoTの制御機器等、使用可能なメモリが潤沢でないようなHW上で動くロジックを作成する場合や、容量が限られたファイルシステムを使うような場合は、常に、このコストを考慮しなければなりません。例えば、加速度センサーから取得した値を表現する以下の二つの方法を考えてみます。

- 文字列で保持 “0.912345”
- floatで保持 0.912345

人間にとってはどちらも同じ、0.912345ですが、メモリ上では前者を保持するのに必要なメモリ量は9バイト、後者は4バイトです。前者は後者の2倍以上のメモリ量を消費します。メモリ量が枯渇するような小規模のHWではなるべくメモリ量を消費しない設計上の配慮が必要です。
また、ビット数が異なる変数を使って複合型を定義する場合にはメモリの配置にも注意が必要です。例えば、CやC++のstructで以下の様な定義を行った場合、

struct Foo {
   char a;
   short s;
   int i;
   double d;
}

このデータ型の変数がメモリ上で占める容量は15バイト(=1:char+2:short+4:int+8:double)ではなく、32ビット境界のHWでは、20バイト必要です。

画像3

IoTにおいては、機器間のシリアル通信や通信回線を通じたバイナリデータの送受信においては、送受信されるデータフォーマットからロジックで扱いやすいデータ構造に変換する処理を頻繁に記述します。CやC++の場合、structを使った変換はよく使うテクニックですが、この特性を考慮しないと正しく変換できないので注意が必要です。
加えて、シリアル通信のケースでは、データのビットの並びにも注意が必要です。コンピュータ技術の発展の過程で、

- Big Endian
- Little Endian

の二つの形式があるので、シリアル通信でバイナリデータを受信しつつロジック寄りのデータ構造に変換する場合にはそれぞれの形式に従った解釈を行うためのロジックを記述します。

画像4

※ ちゃんとしたセンサーや機器IFの仕様書ならば、大抵記載されているものですが、案外Big Endian、Little Endianは書かれてなかったりします。とってもめんどくさいので筆者はこの辺大嫌いw
※ メモリ上でデータを保持する並びとしては、なんとなくBig Endianが自然では?と思う読者も多いかと思います。これは多分、算用数字を書く順番と並びが同じだからではないでしょうか。だとすれば、数字を右から左に書く言葉を使う人たちは逆の印象を受けるのではないでしょうか。とはいえ、数字を左から右に書く習慣を持つ私たちにとってはBig Endianの並びは自然に思えるのは間違いないでしょう。実は、論理回路上では、Little Endianの方が効率よく演算ができるので、コンピュータという機械の世界からすると、Little Endianの方が自然な並びということになるでしょう。Little EndianはIntel系のCPUが採用しており、Big EndianはIntel系以外のCPUで採用されています。今我々が普通に使っているコンピュータの歴史は、Intelが開発した4ビットの4004というCPUから始まっています。その後8ビットの8008がIntelから発売され、同時期にモトローラが6800という8ビットCPUを発売しました。8008は8080、8086、80286、…と進化し今のPentiumにつながっていきます。筆者の記憶によれば、IntelのCPUは元々機器の制御が主な目的だったので数値計算に有利なLittle Endianが採用され、モトローラのCPUは汎用コンピュータでの利用を想定していたためBig Endianが採用されたようです。

データ構造圧縮はほどほどに

以上、データ保持のHWコストについて説明してきましたが、これについても設計上やりすぎた場合問題が生じるので注意が必要です。スタンドアローンで動作する小型組込み機器においては、HWコストを考慮しまくった圧縮されたデータ構造の利用は、閉じているのでそれほど影響はありませんが、IoTソリューションを構成するクラウドサービスと通信するような小型組込み機器の場合は、組込み機器側の流儀のデータ構造をクラウド側のサービスに持ち込むことはご法度です。IoTのサービスとの通信で使うデータ構造は、IoTのサービス側の流儀に従ったデータ構造の設計を推奨します。IoTのサービスは、ある意味無限のHWリソースが利用可能な環境で動いているので、一見、組込み機器側の流儀のデータ構造を、サービス流儀のデータ構造に変換する処理をIoTサービス側で行っても良いように勘違いしがちですが、無限のリソースとは言え、接続機器数が増えれば当然変換のための処理回数は増えるので、実行リソースを増やして運用コストが増えてしまうことを許容しなければなりません。運用コストをケチれば、この処理がIoTソリューション全体のボトルネックになる可能性もあります。また、IoTソリューションからすれば、接続する機種はほかにもたくさん存在するかもしれません。このケースではそれぞれの機種の特殊なデータ構造を、サービス側の流儀に従ったデータ構造に変換する処理を全て用意しなければなりません。同一メーカーの同一機種でも新製品への置き換えのタイミングでの対応もIoTサービス側で行わなければならなくなってしまいます。そもそも、機器固有の特殊なデータ構造は、機器側の事情なので、IoTのサービス側に持ち込むことは避けたほうが良いでしょう。

ロジック

プログラミングのロジックは構造化プログラミングの原則に従って設計します。設計結果に基づいてプログラミング言語でプログラムを記述していくことを“実装”と呼びます。
構造化プログラミングの流儀では、ロジックは以下の3種類の基本部品で構成することになります。

- 順接
- 条件分岐
- 反復

画像5

この三種類で構成したロジックは有限のテストケースでの実行確認が可能です。
ロジックの制御の流れを図示するダイアグラムとして、フローチャートが有名で一般的ですが、フローチャートは、構造化プログラミングに従わない制御の流れも表現できてしまうので、あまり好ましくありません。構造化プログラミングに則ったロジックを図で設計したい場合は、プログラム構造表記法(PAD)がおすすめです。

複雑な条件判断

プログラムで表現したい事象が非常に複雑で多くの変数が絡み合った条件判断をロジック化しなければならない場合があります。関与する変数が多ければ多いほど、またその組み合わせが多ければ多いほど、条件判断の抜け漏れが発生しがちです。それを避けるためには、関与する変数全ての値域を明確にし、決定表(Decision Table)を作成するとよいでしょう。
決定表は一定のルールに則ってコンパクト化が可能です。コンパクト化された決定表に基づいて条件ロジックを記述すると最小限の条件判断文が導出できます。

処理の実行順序

一般的に使われている大抵のプログラム言語は、上から下に記述された順番にステートメントが実行されていきます。この様に記述されたステートメントが順番に実行されていくようなプログラミング言語を、“手続き型言語”と呼びます。

※ ここでは、データとロジックをカプセル化できるオブジェクト指向言語も、しょせん、ロジックの部分は記述されたステートメントが上から下に流れていくので、“手続き型言語”に含めます。

概念モデリング教本」では「概念振舞モデル」の章で、データフローモデルを紹介しています。データフロー図で書いたアクションを手続き型言語のロジックに変換する場合、データフローの後段のデータ出力側のバブルからステートメントに変換し、下から上に積み上げていくと、比較的スムーズに手続き型のロジックに変換していくことができます。

一般的に手続き型言語に分類されるプログラミング言語でも、必ずしも実行が上から下に行われるとは限らないものも実は存在しています。
C#では、非同期プログラミングという仕組みがあり、書き方によっては、メソッドコールのステートメント実行において、そのメソッドのロジックが全て終わらないうちに制御が帰ってきて次のステートメントが実行される場合もあり得ます。また、C#はマルチコア対応の言語実行プラットフォームを持っているので、foreach による反復などで、通常の手続き型言語なら反復する対象一つ一つが逐次実行されるのに対し、反復のロジックが同時並行的に実行されることもあり得ます。
順番に実行されるという思い込みでプログラムを書くと想定外の動きをしてしまう場合もあるので注意が必要です。

参考までにですが、プログラミング言語には、手続き型言語以外にも、宣言型言語に分類されるものもあります。手続き型言語は実行したい処理を逐一書いていくのに対し、宣言型言語は、論理的な構成記述によって計算結果を得ることができます。例えば、リレーショナルデータベースからデータを検索するSQLは正に宣言型言語に分類されるクエリー言語と呼ばれるもので、IoTソリューションのサービス側では、クエリー言語が多用されます。
手続き型言語はとりあえず処理を順番に書いていけば、そこそこ動くプログラムが作れますが、宣言型言語は、その言語が前提としている論理や仕組み、数学的基礎を理解していないとなかなか使いこなせません。

ロジックと変数名の一般化

ロジックをプログラミング言語で記述する際は、なるべくシンプルに記述することを心がけましょう。HWリソースの制約など特段の制約がない限り、処理を簡潔にできるプログラミング言語仕様やライブラリがある場合は、それらを活用すべきです。また、条件判断や反復は、目先の処理を包含するような一般化したロジックにすることを心がけます。ロジック内で使用するデータ変数や条件判断、反復数においては絶対に即値(3とか0.17といった具体的な数字や固定文字列)を使ってはいけません。ロジック実行時に渡される引数や、定数値が別の場所で代入されたRead Onlyの変数を使うようにします。

概念モデリング教本」で紹介したアプリケーションドメインに相当するプログラムのロジックの場合、ロジック内で使用するデータ変数の名前は、アプリケーションドメインに対応するビジネスシナリオの用語が対応付けられるようなスペシフィックな名前を付けます。対照的にサービスドメインに相当するプログラムのロジックの場合は、様々なドメインから利用されることを想定して、なるべく一般的で無味乾燥な変数名にします。

※ スペシフィックな名前を付けるとそれに引きずられて、本来なら多くのドメインから再利用可能なポテンシャルを持っているロジックが、限定的にしか使えないロジックになってしまうから

コメント

コメントは必要十分な量を記述することを心がけましょう。たまに、ソースコードで書かれていることをそのまま文章にして書いているコメントを見かけますが、ソースコードを見れば誰でも理解できるので、書くだけ無駄です。加えて、統合開発環境のエディターで表示している場合、コメントが書かれた行だけ、表示領域を占有し、肝心のソースコードが表示できる行数が減ってしまい、デバッグ時やバグフィックス時の思考を妨げてしまいます。
コメントは、その直後のステートメント、あるいはステートメントの塊に関する、背景や理由、実行時の制約等を最低限だけ記載しましょう。
また、C#をはじめとする各種高級言語は、決まった形式のコメントを書くと、インターフェイスの仕様書を自動生成できるようになっています。ほかの言語でも doxygen を使えば同じようなことが可能です。昔々、コーディングは、関数やメソッドを説明する詳細仕様書を作成して(レビューして)から行うべきというような風習もありましたが、IT技術が発達し開発環境も進化した現在では、この様な自動生成機能を使ってインターフェイス仕様書を作ることが推奨されます。インターフェイスの不具合はコンパイルしたりデバッグしたりしているあいだに発見されるものであり、昔ながらのやり方では、一つの修正を行うのに2つのテキストを修正しなければなりません。また、人が見るためだけの自然言語で書かれたドキュメントはコンパイラでチェックするわけにもいかないので記載ミスも織り込まれてしまう恐れもあります。ソースコードの記述を修正してインターフェイスのドキュメントを自動生成するのが理にかなっています。

アルゴリズム

ある問題を論理的に解く手段をアルゴリズムと言います。プログラムを構成するロジックは現実世界の諸手続きを自動化するという問題の解決手段なので、ロジック自体がアルゴリズムと言えますが、エンティティ群のソートや検索といった、大抵のプログラムで頻繁に出てくる問題に対する一般的なアルゴリズムが昔から提案されています。ロジックの設計では、特段の理由がない限りは、各自のアプリケーションに適したアルゴリズムを選択してプログラムを作成します。選択したアルゴリズムによっては、プログラムのデータ構造の実装方法にも影響を及ぼすので、メモリ効率の観点も含めたアルゴリズム選定を行うようにしましょう。
特に検索のアルゴリズムについては、検索にかかる計算量の目安が、O(1)、O(N)、O(logN)といったオーダーと呼ばれる表現形式で明記されています。Oのカッコ内の数字が小さいほど高速な検索が可能ですが、検索アルゴリズム毎に、扱うエンティティ群に対するソートアルゴリズムが決まっていて、検索が早いアルゴリズムほど、データ構造が冗長になる傾向にあるので、必要な扱うエンティティの総数も考慮しながら適切なアルゴリズムを選択します。

マルチスレッド

一般的に、手続き型のプログラムで実装されたプログラムは、最初のステートメント行から順番に実行されていきます。この一連の処理の流れを“実行スレッド”、または、単に“スレッド”と呼びます。多くのプラットフォームでは、一つのロジック内で複数の実行スレッドを処理する方法が提供され、プログラミング言語も複数の実行スレッドの並行実行を可能にするような記述方法を提供しています。この様に、同時に複数の実行スレッドを実行するような状況を“マルチスレッド”と呼び、そのようなプログラムを記述することを“マルチスレッドプログラミング”と呼びます。
例えば、Linux系のOSでは、POSIXというライブラリが提供されていて、それを使えば、CやC++でマルチスレッドプログラミングが可能であり、C#の場合は、System.Threadingというパッケージを使う事でマルチスレッドプログラミングを記述できます。C#の場合は、非同期プログラミングも言語仕様として用意されているので、このパッケージを使わなくても暗黙的なマルチスレッドなプログラムの記述が可能です。

HW技術が進化した昨今、安くて極小なMPUでさえ、マルチコアな時代が到来していますが、昔々は、制御機器で使われるMPUやWindows PCも含め、MPUはシングルコアでした。その時代のシングルコアのプラットフォームでマルチスレッドを実現する場合は、ロジック内で起動した各実行スレッドは、下図に示すように、OSによって一定のルールに基づいて細切れに分断しコア上で処理する実行スレッドを切り替えながら処理を行い、見かけ上のマルチスレッド実行を実現していました。
マルチコアのプラットフォームの場合、複数の実行スレッドをそれぞれのコアに割り当てられるので、実際にそれぞれの実行スレッドが並行的に処理されていきます。ただし、利用可能なコアの数は有限なので、コア数を超えた実行スレッドが生成されるようなロジックでは、シングルコアの時と同じように一つのコア上での複数の実行スレッドが細切れ分断処理が行われます。
それぞれの実行スレッドは、その実行スレッドが生成される前に定義された参照可能な変数に対する読み書きができます。これは、ある変数に対して、複数の実行スレッドが同時に読み書きを行う可能性があるということを意味します。プログラミング言語で記述されたステートメントの構成単位と、実際にプラットフォーム上で実行される際の実行単位は異なります。プラットフォーム上で実行されるのは、そのステートメントがコンパイルされた結果生成されたマシン語の命令単位で実行されるからです。
そのため、スレッドAとスレッドBという二つのスレッドがあって、p->yという変数をどちらも参照可能な場合、スレッドAがxの値を読む処理とスレッドBがp->yの値を書き込み更新する処理が記述されている場合は、スレッドAのp->yの値を読み込む処理の途中でスレッドBの処理に切り替わりxの値を更新する処理の途中で更にスレッドAの処理に戻るような事態が発生し得ます。この様な状況ではスレッドAが参照したp->yの値は、思いもよらぬ不思議な値になって、その後の処理も思いもよらない結果になってしまう恐れがあります。

画像6

この様な事態を発生させないために、マルチスレッドプログラミングでは、“排他制御”と呼ばれる処理をプログラムの記述に織り込まなければなりません。
Linux系、Windows系どちらのプラットフォームでも、排他制御のための機構を提供しています。Semaphore、Critical Section、Mutex等、色々ありますが、変数の読み書きの調停であれば、MutexによるLock、Unlockで十分です。
また、C#の場合は、言語仕様で、lockというキーワードが用意されているので便利です。

設計が不十分なマルチスレッドなプログラムの場合、不適切に記述された排他制御のために、MutexのLockとUnlockが二つ以上の実行スレッドの処理で襷掛け状態が発生し、そのまま処理が止まってしまう事態になる可能性があります。この様な状態に陥っている事を、“デッドロックが発生した”と言います。実際にこの事態が起こった時、原因を究明することは非常に難しく、そんな状態にあるプログラムを正常に動かすように修正するのは至難の業です。また、目分量で範囲を決めて排他処理を記述すると、無駄な処理待ちが各所で発生しパフォーマンスを劣化させます。排他制御が必要なのは、ある変数に対する同時読み書きの排除の部分だけなので、データ構造をしっかり設計して、ロジックがその変数にアクセスする時にロックして、アクセス終了後にアンロックすればいいだけの話です。

画像7

概念モデリング教本」では、ビジネスシナリオの文脈上で発生しうる同一リソースへの複数同時要求の排除を、概念情報モデルと概念振舞モデルを使って解決するように解説しています。この観点を満たすように構築された概念振舞モデルのアクションを、プログラミング言語に変換する場合は、インスタンスの生成・削除、インスタンスの特徴値の読み書き、インスタンスのリンクの切り貼り、インスタンスセットの問合せ、インスタンスを起点にしたリンクを辿る、といった部分だけMutexで保護するコードを織り込めば十分であり、他には何も必要はありません。面倒くさいモデリングは適当にさっさと済ませて、適当に書いてコンパイルが通れば簡単に動くプログラムを早く書きたいと思う気持ちも痛いほど良くわかりますが(まぁ、認めませんけど私は:苦笑)、問題を引き起こしてから解決に脳細胞と時間をかけるか、問題を引き起こさないようにするために脳細胞と時間をかけるか、どちらか適切な方を選択してください。

概念モデリング教本」の「概念振舞モデル」の章で、状態機械を紹介しました。状態機械は、自分以外、あるいは、自分自身が生成する事象をキューイングして、ひとつづつ事象を元に次の遷移状態を決定し、その状態に紐づいたアクションを実行していきます。状態機械の一つの実装例として、状態機械ごとにスレッドを割り付けることができます。状態機械の実行スレッドは、事象キューを監視していて、事象がなければ、事象キューに新しい事象が格納されるまで待ちに入ります。この様な場合、事象キューは、状態機械のものではない別のスレッドからの書き込みと、状態機械のスレッド自身の参照、事象の取り出し(事象キューへの書き込みに相当)が同時に発生しえるので、先ずは、Mutexによる排他制御が必要です。加えて、状態機械のスレッドは、事象キューが空の場合は実行を止めていなければならず、他のスレッドは、事象を事象キューに格納した時に、状態機械のスレッドへの実行再開通知処理が必要です。
この機構向けに、CやC++では、POSIXライブラリの場合はCondition変数が、Win32 APIの場合はEventが、C#の場合は、WaitHandleが提供されています。

くどい様で申し訳ないですが、「概念モデリング教本」で紹介したソフトウェア開発スタイルを使うと、マルチスレッドプログラミングは、MutexとCondition変数(またはそれに相当する仕掛け)だけで、デッドロックも起こさず実現可能です。多数の事象や状態が複雑に絡み合う場合や関連するサブシステムとの連携における通信タイミングが複雑な場合には、設計時に構築した状態モデルが役立ちます。

第三者のライブラリに潜むデッドロック

マルチスレッドプログラミングにおける排他制御を紹介してきましたが、自分が書いたプログラムであれば、排他制御に関する記述を織り込むことはできますが、他の誰かが開発して提供されたライブラリを使う場合には、そうは問屋が卸しません。周辺機器やストレージ、HWアクセラレータ等のリソースに複数の実行スレッドがライブラリを介して同時にアクセスするような場合、利用しているライブラリがマルチスレッドによる利用を想定していない場合には、各自のプログラムの側で適切な排他制御を記述しなければなりません。一方で、利用しているライブラリがマルチスレッドからの利用を想定して開発されている場合は、気にせずにプログラムを記述できます。マルチスレッド対応されたライブラリのことを“スレッドセーフ“なライブラリと呼びます。ライブラリの仕様書には大抵、スレッドセーフか否かが記載されているので、それを確認して各自のロジックをプログラム化しましょう。

テスト

データ構造を定義し、ロジックを記述してプログラムを作成したら、必ずテストを行います。テストは、必ず全てのロジックが一回以上実行されること、分岐の条件判断が発生しうる変数の値域の組合せが全て一回以上実行されることを目安とします。以下に紹介する技法を使って、

- 同値分析
- 限界地分析
- 原因結果グラウ
- デシジョンツリー

条件判断の抜け漏れを防ぎます。
コーディングの振り返りもかねて、開発環境のデバッガーを使って可能な限り一度は、一行づつ実行して想定通りの動作であることを確認しましょう。特殊な制御機器やマイクロサービスの様な実行環境上での実機デバッグが困難な場合は、ロジックを検証するためのテストベッドを別に作って、動作を確認します。もし動作確認したにもかかわらず、目的の実行環境上での動作中に動作不具合が発生した場合は、あなたが書いたロジック以外の原因によるものと推定できます。経験上、バグは動作確認していないところに潜んでいます。あくまでもきちんと合理的に設計され系統だって実装されたプログラムに限りますが、対偶をとれば、「バグが潜まないのは動作確認済みの部分」なので、可能な限り動作確認を行いましょう。

プログラムを作成する開発環境とソフトウェアが実際に動く実行プラットフォームが異なるようなソフトウェア開発を、“クロス開発”と呼びます。ITシステムの場合、多くの部分がクラウドやデータセンター上で実行され、IoTソリューションでは組込み制御機器上でソフトウェアが動作するので、ほとんどがクロス開発といってよいでしょう。クロス開発において、テストのための実行環境を用意するのはなかなかに骨が折れる作業ですが、実運用実行環境で発生した障害対応にかかる労力とコストを大幅に低減できるので、テスト実行環境構築にかかるコストは十分ペイできます。実行プラットフォーム上で動いているソフトウェアをネットワーク越し、あるいは、USBケーブル接続越しに、開発用PCからデバッグすることを“リモートデバッグ”と言います。リモートデバッグは障害対応やバグフィックスを行うための強力なツールです。実行プラットフォームを選択する際の選定基準に、「リモートデバッグができること」を入れておくのも良いでしょう。

テスト実行は可能な限り自動化することをお勧めします。C#には標準のUnit Test Frameworkがあり、開発環境で用意されているテスト支援機能と連携してビルド時のテスト自動実行が実現できます。C++やJavaにも同様なテスティングフレームワークがあるので、それらを活用して、テスト実行をビルドに組み込みます。Apacheプロジェクトをはじめとする多くのオープンソースプロジェクトのビルド・インストールの仕組みを見ると、ほとんどの場合、テスティングフレームワークが織り込まれているので参考にしてみてください。

昔から「バグのないプログラムは存在しない」と言われています。テストにテストを重ねてもバグをなくすことはできません。そのため、原理主義的な流儀で徹底的にテストを行うような開発プロセスを採用したい誘惑にかられる人もいるかもしれません。プログラムの行数あたりに潜むバグ数をバグ密度と呼びます。経験的にバグの発見にかかるコストはバグ密度が低くなればなるほど指数関数的に増大してしまうものです。テストはほどほどにして、プログラムに実行ログを収集できる仕組みを織り込んで、いつ何をどんな順番で実行したかが運用時に分かる仕組みを入れて、バグ発見時の原因究明が迅速に行える体制を整えましょう。

特にC言語では、malloc関数によるメモリの確保や、システムライブラリが提供する各種ハンドルの取得時、メモリ不足による失敗がありえます。失敗した時には、関数は大抵の場合NULL値を返してきます。こうなってしまっては、正常な処理は続けられないので、この様なリソースを取得するような関数をコールした直後には必ずNULLチェックを行ってエラーが発生したことをログし、それぞれのケースに応じたエラー発生時にやるべき処理をコードとして記述しなければなりません。C言語だけでなく、Exceptionの発生など同様な事態の発生が想定される場合は、エラーのログを残す処理とエラー時の処理を必ずコード化しておきます。実製品や実サービスを制御するソフトウェアコードの多くの部分はエラーに関する処理で占められているものです

現実の世界に起因する実時間制約や、発生した事象が同じでも内部状態が異なる場合はそれぞれの状態に応じた処理を行わなければならないような、リアルタイム制御ソフトウェアのテストは、これまた、厄介なものです。通常、テスト用の実行環境と実運用環境はHWリソースの潤沢さやパフォーマンスが異なるので、十分なテストを行うには工夫が必要です。マルチスレッドプログラミングの節でも言及しましたが、この様なソフトウェアでは、状態モデルを使った設計が効力を発揮します。ソフトウェアの外で発生した事象に対する内部処理と外への応答は、状態モデルで定義できるので、その論理的な処理については、実行するHWに依存せずにテストが可能です。外部との複数のコラボレーションがあったとしても、時間スケールが変わるだけで、外部とのやり取りのシーケンス自体は論理的には同じで、それが状態モデルとして定義されているからです。あとは、要求される時間内に、トリガーとなる外部事象発生から外部への応答が完了する事の確認です。この部分は実運用環境のHWでしかテストはできません。「概念モデリング教本」で解説したソフトウェア開発方法においては、実時間制約という非機能要件を満たすように、インフラストラクチャドメインの選択と、振舞モデルからコードへの変換方法を設計しているので、万が一、実運用環境で非機能要件が満たせない場合は、インフラストラクチャドメインの選択とコードへの変換方法を見直すことになります。

シンタックスとセマンティクス

設計したデータ構造とロジックは、プログラミング言語を使って、コンピュータ上で実行可能な形式に変換可能なテキスト化できます。一般の開発で利用されているプログラミング言語は、その言語仕様が標準化されています。
- C - http://www.open-std.org/jtc1/sc22/wg14/
- C++ - http://www.open-std.org/jtc1/sc22/wg21/
- C# - https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/
- Java - https://docs.oracle.com/javase/specs/index.html
- Python - https://docs.python.org/ja/3/reference/index.html
- JavaScript - https://www.ecma-international.org/publications-and-standards/standards/ecma-262/
- Ruby - https://docs.ruby-lang.org/ja/latest/doc/index.html

プログラミング言語仕様の内容は、大きく分けて、セマンティクスとシンタックスから構成されています。セマンティクスは、これまで説明してきたデータ構造やロジックの構成要素、実行時の状態変化(実行セマンティクスと呼びます)の規定で、シンタックスは、それら概念をテキストとして記述するための文法です。一般的に高級なプログラミング言語であればあるほど、セマンティクスで扱う対象の抽象度が高く、少ないテキストでより多くの実行が成されるようなプログラムの作成が可能です。

※ 抽象度が高ければ高いほど人間の思考をそのままプログラム化することができる。逆に抽象度が低い場合はHWの仕様に近い仕様ってこと。HWよりであれば、メモリやHW Acceleratorの処理がし易い

概念モデリング教本」で解説した、概念モデルからのプログラムへの変換は、概念モデルの記述で使われるセマンティクスとプログラミング言語のセマンティクスを対応付け、プログラミング言語のシンタックスを元に、プログラム化の為のルールを定義し、そのルールに基づいてコードを、サービスドメインの要素を織り込みながら生成していくということになります。

画像8

ちなみに、“セマンティクス”は、日本語に訳すと、“意味論”です。ロジックを記述するのに必要な概念群の意味と、それらの関係を定義します。「概念モデリング教本」の読者は、ここで、「であれば、プログラミング言語もドメインと捉えて、概念情報モデルができるのではないか?」と思うに違いありません。実際その通りで、プログラミング言語の概念情報モデルを構築してプログラミング言語をドメインとして定義することができます。
図形式ではありませんが、より実践的に使いやすい.NETのライブラリとして提供されている、「セマンティック解析の概要」に記載の方法で実際のプログラムコードを解析して生成できる、は、正に、概念情報モデルそのものです。興味深いのは、このモデルは、C#だけでなく、Visual C++(Nativeを除く)、Visual Basic、F#という.NET Framework上で実行可能なプログラミング言語全てに共通するモデルであるということです。

言語によっては一部使えないセマンティクス要素はありますが、この事実は、ここに挙げた4つの言語は、セマンティクス的には全く同じで、単にシンタックス、つまり見かけが異なるだけだということを意味します。勿論、関数型のプログラムを記述しやすいF#のシンタックス、魔法の様に Native CodeとManaged Codeを行ったり来たりができたり、C++の言語標準に準拠したVisual C++のシンタックスなど、シンタックスはプログラミングの生産性に大きな影響を与えはしますが、セマンティクスについては、4つのマイクロソフト系のプログラミング言語以外のJava、Python、JavaScript、Ruby等もそれほど大きな違いはありません。プログラミングの基礎を学んだあとは、言語のセマンティクスの理解に力を入れると、より早くプログラミングスキルが上達します。

念のため、書いておきますが、コードも書かずにネットで検索したブログや書籍を読み込めと言っているわけではないので注意してください。例えばC言語の場合、言語標準によれば、実行セマンティクスの中には、実行プラットフォームが確定して初めて動作が決まるような要素が実際あったりします。また、頭の中だけでは想像しにくいマルチスレッドな複数の実行スレッドの処理の流れの様などもあるので、単に説明を読み込むのではなく、実際にプログラムを書いて、いろいろな実行プラットフォームで実際に動かして試してみながら学習すると効率よく、様々なプログラミング言語を習得できるので是非実践してみてください。

プログラミング言語の選択

実プロジェクトにおいては、プログラミング言語の選定は、開発効率や開発コスト、運用や保守にかかるコストに大きな影響を与えるので、そのプロジェクトにあったプログラミング言語を選択しなければなりません。IoTソリューションの様に、組込み制御機器、現場のサーバー、クラウドサービス、マイクロサービス実行環境、ユーザー向けアプリケーション等、実行環境が多岐にわたる場合には、それぞれの実行環境に合ったプログラミング言語をそれぞれ選択しなければなりません。
前述のプログラミング言語仕様で挙げた7つの言語以外にもObjective CやSwift、Go、Python、Perl、PHP、Visual Basic、…と多数の様々なプログラミング言語、が世の中には存在しています。それぞれのプログラミング言語はそれぞれ得意分野を持っていてそれぞれの歴史的背景の元、発展してきています。

プログラミング言語の選定にあたっては、プログラミング言語のセマンティクスとシンタックスだけでなく、そのプログラミング言語で使えるライブラリ、そのプログラミング言語が実行可能なプラットフォームも含めた検討が必要です。
例えば、非力なMCU、少ないメモリ量の小型組込み制御機器の場合は、プログラムを動かすのに必要なフットプリントがコンパクトな C や C++ を、Windows OS 環境であれば C#、Web アプリ実行環境なら JavaScript といった具合です。Linux であれば、HW の規模によって、C/C++ や、Java、C#(.NET Core)が利用可能です。また、AI で使われる学習フレームワークライブラリはPythonが充実しているので、Python にするということもあるでしょう。また、AI と同様、文字列変換が多くセキュリティの考慮もしなければならないネットワーク通信が大部分を占めるプログラムや、GUIを駆使したプログラムなどは、それぞれの機能の作りこみ量を大幅に削減してくれる便利なライブラリが提供されているプログラミング言語を選択するべきでしょう。

ただし、ライブラリの中には、特定のCPUアーキテクチャ(x86、x64、AMD64、arm32、arm64、RISCVなど)や、OSが限られていることが良くあるので、選定には注意が必要です。加えて、Dockerなどのマイクロサービスコンテナ環境での実行での向き不向きもあるので、それぞれのプロジェクトに対する非機能要件から適切な選定条件を導き出してください。そのプログラミング言語に対応する出来の良い統合開発環境も選定要件の一つです。

最悪な選択理由は、「現状の開発チームが使える言語」を元にした選択です。そもそもなじみのある言語が限られている場合、複数のプログラミング言語から適切な言語を選択するのは不可能です。日頃から様々なプログラミング言語(最低限、標準仕様を紹介したリストに挙げているもの)が使えるように、開発部門の技術者は腕を磨いておくべきでしょう。

※ これは筆者の個人的な意見ですが、自分の興味の赴くままのホビープログラミングや学習目的のプログラミングではなく、対価をもらってソフトウェアを開発する、プロフェッショナルのソフトウェア技術者であれば、今まで一度も使ったことがないプログラミング言語でも、2週間で、そこそこのプログラミングができるぐらいの技量を持っているべきでしょう
※ プログラミングを生業とするなら、常日頃から意識して複数の言語を使いこなすトレーニングをすることを推奨します。とはいっても、誰が作ったかわからない継ぎ接ぎだらけのプログラムの御守りで手一杯、80年代以前のソフトウェア開発環境をベースにしたコーディング規約や開発プロセスに従うのが手一杯でそんな時間はありません、という方も多数いることでしょう。仕事で開発する実プロジェクトの開発において、自分の好みだけでプログラミング言語を選択することはできないので、自分の仕事を自動化するツールを開発してみてはいかがでしょう。これなら、言語、実行プラットフォームの選択は自由です。自動化対象の問題もよく理解しているはずですし、最初は時間がかかってもツールが出来上がってくると、学習にかけた時間を回収できるほどの時間短縮ができるはずです。
※ コードレビューについて
筆者の経験によれば、きちんと設計がされずに書かれたプログラムコードに対するコードレビューはやるだけ無駄なので、やらないことをお勧めします。勿論正式なレビューを否定するわけではなく、複数のステークフォルダーが異なる観点から論理的な手順に従ったチェックは、必要なポイントポイントで実施すべきです。「概念モデリング教本」で解説した開発方法においては、概念モデル作成時、概念モデルの実装への変換方法の定義の際には、いくつかチェックポイントを設けて、正式なレビューを行う事を推奨します。コードレビューは、本気でやろうとすると、テキストファイルに記述された一行一行を隈なく読んでチェックしていくわけです。10行やるだけでも数十分かかってしまいます。また、コードレビューに参加する開発者は、コードレビュー中には各自の開発作業はできないので進捗は止まり、コードレビューで指摘を受けた項目を管理する工数、後日適切に修正されたか確認する工数、などなど、開発チームの生産性に著しい悪影響を及ぼします。更には、20世紀の未発達の開発環境での開発経験に裏打ちされた時代遅れのコーディングスタイルを信望するような宗教じみた参加者がいると最悪です。
そんな無駄な工程に時間を割くくらいなら、コードレビューがほぼいらないようなコーディング開始前の設計を行いましょう。または、gitによるソースコード共有環境と、Visual StudioのLive Shareの様な環境を用意して、いつでも開発チームのメンバーが書いているコードをネット越しに見られるようにしておいて、何か気づく点があれば都度コメントして、コメントされた担当者の修正結果も見られるようにしておくとよいでしょう。なお、どうしても設計時間が取れない場合は、二人以上が共同で行うペアプログラミングでコードを書いていくのがおすすめです。一つのモニターの前に二人が座って、コードを書く担当者が、次々と頭に浮かぶ検討事項や懸念を口にしながら、何故こういうコードにするのかを隣に座っている開発者に説明しつつ、隣で聞いている開発者が適度な突込みをいれながらコーディングしていくのも有効です。

大規模ソフトウェア開発では、プログラムを適度に分割しながら作業を進めます。次の章は、「モジュール分割」です。

この記事が気に入ったらサポートをしてみませんか?