質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.04%

C#のマルチスレッド処理で処理時間が落ちる問題

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 475

TOMO6181

score 27

C#で、UIスレッド内で別スレッドを2つ以上起動するとUIスレッド上の処理時間が延びてしまう問題について質問です。

マルチスレッドを使用すると、UI上では別スレッドで処理が行われるため、並列処理を2つ以上呼び出してもUIスレッドのイベントの処理時間は
延びない想定でしたが、下記のコードを実行すると、イベント内の処理時間が20msecとか30msec以上延びることが時々あります。(0msecの時もある。)
この処理では、開始ボタン押下イベント(btnStart_Click)を実行すると、停止ボタン押下イベント(btn_Stop_Click)が実行されるまで
タイマーにセットした処理(ArrivedTimer)を2秒間隔で呼び出します。
ArrivedTimer中で呼ばれている関数(CalcCls.JikankakaruProc)は、処理時間が2.1秒程かかります。
ちなみに、ArrivedTimer関数内のt2のタスクの処理をコメントアウトすると、処理時間は0msecか1msecぐらいしか延びないです。

UIスレッド側の処理

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Timers;

namespace Jikankakaru
{
    public partial class Form1 : Form
    {
        /// <summary>
        /// タイマークラス
        /// </summary>
        private System.Timers.Timer timerProc;

        public Form1()
        {
            InitializeComponent();

            // 初期処理
            InitData();
        }

        /// <summary>
        /// 初期処理
        /// </summary>
        private void InitData()
        {
            // 1秒に1回
            this.timerProc = new System.Timers.Timer();
            // 間隔は1秒
            this.timerProc.Interval = 2000;
            // イベントセット
            this.timerProc.Elapsed += ArrivedTimer;
        }

        #region ■デリゲート

        /// <summary>
        /// デリゲート
        /// </summary>
        /// <param name="str"></param>
        private delegate void SetLabelDel(Label label, string str);

        #endregion

        #region ■デリゲート用関数

        /// <summary>
        /// デリゲート
        /// </summary>
        /// <param name="str"></param>
        private void SetLabel(Label label, string str)
        {
            label.Text = str;
        }
        #endregion

        /// <summary>
        /// 処理開始
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnStart_Click(object sender, EventArgs e)
        {
            this.timerProc.Start();
        }

        /// <summary>
        /// タイマー停止
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btn_Stop_Click(object sender, EventArgs e)
        {
            this.timerProc.Stop();
        }

        /// <summary>
        /// タイマーが来たときのイベント
        /// </summary>
        private void ArrivedTimer(object sender, ElapsedEventArgs e)
        {
            Stopwatch sw = new Stopwatch();

            BeginInvoke(new SetLabelDel(SetLabel), this.lblState, "未完了");

            // 冒頭の処理時間
            sw.Start();

            // 時間かかる処理
            OutDll ans1 = new OutDll();
            OutDll ans2 = new OutDll();

            Task t1 = Task.Run(() => CalcCls.JikankakaruProc(ans1));
            Task t2 = Task.Run(() => CalcCls.JikankakaruProc(ans2));

            int a1 = CalcCls.GetData();
            int a2 = CalcCls.GetData();

            // 答え出力
            BeginInvoke(new SetLabelDel(SetLabel), this.lblAns1, a1.ToString());
            BeginInvoke(new SetLabelDel(SetLabel), this.lblAns2, a2.ToString());
            BeginInvoke(new SetLabelDel(SetLabel), this.lblState, "完了");

            sw.Stop();
            long data = sw.ElapsedMilliseconds;
            sw.Reset();

            // 処理時間出力
            BeginInvoke(new SetLabelDel(SetLabel), this.lblProctime, data.ToString());

            // 処理時間ファイル出力
            WriteFile(data.ToString());
        }

        /// <summary>
        /// ファイル出力
        /// </summary>
        private void WriteFile(string str)
        {
            // 改行追加
            str += Environment.NewLine;

            // 追加形式
            string filePath = this.txtFolder.Text + this.txtFile.Text + ".csv";

            Encoding enc = System.Text.Encoding.GetEncoding("shift_jis");

            // ファイル書き込み
            File.AppendAllText(filePath, str, enc);
        }
    }
}

タイマーイベント内で呼び出されている関数のクラス

/// <summary>
    /// 計算用クラス
    /// </summary>
    public static class CalcCls
    {
        /// <summary>
        /// 共通のデータ
        /// </summary>
        static private int commonData;

        /// <summary>
        /// 時間かかる処理
        /// </summary>
        public static void JikankakaruProc(OutDll ansData)
        {
            for (int i = 0; i < 1000000000; i++)
            {
                double data = 0;
                data /= 100000;
            }

            // 共通データに乱数設定
            System.Random r = new System.Random(1000);
            commonData = r.Next(100);
            ansData.FAns = commonData;
        }

        /// <summary>
        /// データ返す
        /// </summary>
        /// <returns></returns>
        public static int GetData()
        {
            return commonData;
        }
    }

実際の現場で使用するコードでは、100msec周期でカメラ画像取得イベントが呼び出されるため、20~30msecの遅れでもかなり致命的になります。
呼び出すタスクを2つにすると処理時間が延びてしまう原因が分かる方がおられましたら、宜しくお願い致します。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

こんにちは。

手元でやってみましたが、そのような現象は起きませんでした。
プロジェクトを作って、Form1.csをコピペし、その中にOutDllクラスを定義、CalcClsクラスをコピペ、2つのボタンと4つのラベルを作り、ボタンのclickイベントを設定しました。WriteFile()関連処理はコメントアウトしてます。
Debug.WriteLine()をArrivedTimerの最初と最後にいれて確認しました。

startを押すと2秒毎にArrivedTimerが呼び出され、この関数は最初の1回目を除き、0mSecで終了しています。正常に動作しているようです。

ところで、JikankakaruProc処理終了を待たずにcommonDataを表示しているので、最初の1回目を除き、結果は全て同じ値ですし、一瞬でBeginInvoke(new SetLabelDel(SetLabel), this.lblState, "完了");が実行されるので、常に「完了」と表示されています。

ざっと見たたところ、3つの問題点があります。(結果が出る前に結果を表示、Randomを毎回初期化、commonDataを複数のスレッドから排他制御無しにアクセス)

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/27 00:45

    C# で幾つか(把握してない^_;)スレッドを動かしてますが、秒オーダーの問題は発生して無いですね。一応、カメラ画像の取得を可能な限り高速で行ってます。まあ、最高速が出てないとの指摘はありますが、一瞬のもたつき程度。なんか別の問題無いでしょうか?

    キャンセル

  • 2019/05/27 01:22

    この問題は2つあります。

    1)WindowsはリアルタイムOSではない(=応答性能は保証されない)
    なので、1~2秒の応答遅れが致命的(機械が壊れるのはもちろん、データ・ロストが許容範囲を越えるなど)にはならないように設計するべきと思います。例えばプチフリーズは結構有名な話と思います。
    なお、保証がないということは理論的には数分とか数時間、数日、数年もありえるでしょうが、それは流石に非現実的でしょう。(その結果、高価な機械が壊れたり人が死んだりするようならアウトですが。)

    2)ガベージコレクションによる停止が不意に発生する
    上記のコメント内の最後のリンク先にはGCによる停止パターンがいくつか記載されてています。数mSec~数100mSecの停止が発生する可能性があるようです。

    そうそう、スレッド・プールから次々とスレッドを起動すると、ある一定数を越えるとTask起動に大きな遅延が発生します。https://oita.oika.me/2016/02/18/task-and-threadpool/
    これは私も経験したことがあります。このような仕様があることに驚いたものです。
    数10mSecではなく、数100mSec以上の遅延なので今回のケースには当たらないと思いますが。

    キャンセル

  • 2019/05/29 00:26

    御回答下さりありがとうございます。
    御指摘の中にありますように、GCが影響していたようです。
    現場で実際に使用しているコードでは、何百万個の要素をもった配列を定義していて、メモリエラーをよく起こしていたため、GC.Collect()を入れたりしていたのですが、ここで処理時間がかかったりかからなかったりするところがありました。
    また、それとは別に、タイマーイベント内でラベルやピクチャーボックス等の操作や参照が多く、GUIスレッド上で待ちが発生していたのも原因でした。これも処理時間がかかったりかからなかったりするので見落としていました。
    そういった個所をコメントアウトすると、イベントの処理時間が遅れることがなくなりました。メモリの節約等コードはまだ整理しないといけないですが、解決できそうです。
    Chironianさん、詳しい説明をありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.04%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる