「ソフテックだより」では、ソフトウェア開発に関する情報や開発現場における社員の取り組みなどを定期的にお知らせしています。
さまざまなテーマを取り上げていますので、他のソフテックだよりも、ぜひご覧下さい。
ソフテックだより(発行日順)のページへ
ソフテックだより 技術レポート(技術分野別)のページへ
ソフテックだより 現場の声(シーン別)のページへ
先日、20数年前に製作したWindowsNTで動いていたアプリケーションをWindows7で動かすためのソフト変更を行いました。
プログラムの変更を終えて動作確認をしていたところ、操作基板のスイッチを押したときに点灯するLEDがいつもより時間が掛かって点いたように感じました。
機能的には問題ないのですが、違和感があったことと変更による影響があるのかもしれないと思い原因を調査しました。
この調査により、操作基板と通信するスレッドが他のスレッドよりも後回しにされ続け、処理されない時間が長くなっていることがわかりました。
このようになる原因をマイクロソフトのWebサイトで調べたところ
『OSがWindowsNT、2000、XPの時代は、スレッドがLockで待機した順に再開した。
しかし、それよりあとのOS(具体的にはWindows Server 2003 SP1以降)はLockで待機した順には再開せずパフォーマンスが高くなるスレッドが優先して再開する』
がわかりました。つまり先入れ先出し(FIFO)順では動作しないということです。
私が経験したようにWindowsNT、2000、XPの時代のアプリケーションをそのままWindows Vista以降で動かすと、同様のことが起こりえます。
そこで今回は私が使ったWindowsの同期オブジェクトの1つ「クリティカルセクション」のFIFO動作について説明します。そしてクリティカルセクションを使ったソフトがFIFO動作する対策を説明したいと思います。
私がソフト変更したアプリケーションは通信ボードを使っております。
ボードへのアクセスは1つのスレッドのみ可能です。
このように1つの「資源」(今回は通信ボード)へ同時アクセスしないようにする(=排他的にアクセスする)ためにOSが用意している仕組みが同期オブジェクトです。
例えば「お医者さんが一人の病院にAさん、Bさん、Cさんの順番に来院した」とします。この場合「資源」がお医者さんで「スレッド」がAさんなどの患者さんです。
お医者さんは一人なのでAさんを診察中にBさん、Cさんが来たらAさんの診察が終わるまで待合室で待ってもらいます。そしてAさんの診察が終わったらBさんかCさんを診察室に案内します。
このように「待合室で待ってもらう」「終わったら次の人を診察する」で一人ずつ診察する仕組みが「同期オブジェクト」に相当します。
また「次の人」はFIFO動作であればAさん→Bさん→Cさんと来院順になります。しかしFIFO動作でなければお医者さんのパフォーマンスが高くなる順番で呼び出すということになります。言い換えると長く待たされる患者さんが出てくるということです。
なお、同期オブジェクトには幾つか種類がありますが本レポートは「クリティカルセクション」を対象にします。
マイクロソフト社のホームページに注意点の記載があります。その一部を抜粋しGoogle Chromeで機械翻訳した内容を記します。
『Windows Server 2003 Service Pack 1(SP1)以降、クリティカルセクションで待機しているスレッドは、先着順でクリティカルセクションを取得しません。この変更により、ほとんどのコードのパフォーマンスが大幅に向上します。ただし、一部のアプリケーションは先入れ先出し(FIFO)の順序に依存しており、現在のバージョンのWindowsではパフォーマンスが低下するかまったく機能しない場合があります』(2021/08/01更新) |
注意点は2つです。
1つ目は「先着順でクリティカルセクションを取得しません」です。
これは先にクリティカルセクションのロックで待機したスレッドが先に待機を抜けるわけではない、つまりFIFO動作しないということです。
2つ目は「パフォーマンスが低下するかまったく機能しない場合があります」です。
「パフォーマンスが低下する」はロックの待機時間が長くなるスレッドがあり得るということです。そして待機が永遠に続くとまったく機能しないことになるわけです。
下図に動作例を示します。
なお、図中の@〜Eはスレッドが「資源」を使う処理を呼ぶ順番を、(1)〜(6)は実際に処理された順番を示しています。
図1はFIFO動作する例です。これは見ての通り順番が一致しています。
図2はFIFO動作しない例です。スレッドCは2番目に処理を呼んだにもかかわらずLock期間が続いて最後に処理されています。私が変更したアプリの「操作基板と通信するスレッド」がこの状態だったと思われます。
これが注意点2つ目の「パフォーマンスが低下する」です。
ただしこのようにすることで、スレッドAとスレッドBは処理時間が短縮されています。3つのスレッドのうち2つでパフォーマンスが上がっているため全体ではパフォーマンスが向上していると言えます。「ほとんどのコードのパフォーマンスが大幅に向上します」はこのことだと考えています。
図1. FIFO動作する例(Lockした順に「資源」を使う処理が実行される)
図2. FIFO動作しない例(スレッドCは2番目に処理を呼ぶも完了は一番最後)
2つの注意点を解決すべく対策を検討します。
まず問題が発生する要因を考えます。要因は「複数のスレッドがクリティカルセクションのロックで待機している状態が継続している」です。
「資源」を使う処理の処理時間が長いことや「資源」を使う頻度が高いことでこのようになると思われます。
そこで「資源」を使う処理にクリティカルセクションを使わず対策する方法を検討しました。概要は次の通りです。
実際のコード(C++)を下図へ示します。
私が変更したアプリではこの対策で処理時間が長くなる現象は発生しなくなりました。
しかしこれも完全な対策ではありません。理由は番号の更新処理を排他的に行うためにクリティカルセクションを使っているからです(9〜12行目)。
2行でクリティカルセクションを抜けるため、複数スレッドがここでロックする状況は起きにくいと考えますが、スレッドが同時アクセスすると順番を保証出来なくなります。
引き続き良い対策を検討したいと思います。
ソフテックが得意とするFA(Factory Automation)やPA(Process Automation)のソフトは様々な機器とネットワークでつながっていることが多いです。それらの機器の制御、「順番通りに処理する」「最悪でも○〇秒以下で処理する」というように順番とワーストタイムの保証が必要なシステムの開発が多いです。
私が感じたように「スイッチ押下への反応がいつもより遅いときがある」「極まれに処理順番が入れ替わる」ときは、本レポートを参考にして頂ければ幸いに存じます。
最後までご覧いただきありがとうございました。
(N.K.)
関連ページへのリンク
関連するソフテックだより