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

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

ただいまの
回答率

88.13%

C# task Mutexでの排他制御 逆に実行結果が遅くなってしまう

解決済

回答 3

投稿

  • 評価
  • クリップ 1
  • VIEW 5,770

score 26

【行いたいこと】
既存の処理(文字列の配列から1つデータを取り出して○○する*1処理6秒位です)
を並列で処理して時間の短縮を図りたいと考えています。
配列から1つデータを取り出す際には排他処理を行って取り出すデータにダブりが無いようにしたいです。
*因みにこの配列自体が20万近くデータがありますのでそこそこ長い時間処理を続けることになります

【行ったこと】
・並列処理はtaskを使用すれば良いということでTask.Factory.StartNewで取り敢えず10個タスクを作って実行
・排他処理はMutexを使用して多重起動を禁止すれば良いということで配列のカウントアップする処理をMutexのWaitOneで排他処理にした

【疑問】
Testで配列から取り出した文字列をConsole.Writeで書き出すテストを「taskを使用しない方法」と「10個taskを作って並列で処理する方法」で試した結果、何故か後者の方が遅い結果となりました。
何故並列処理を行って遅い結果となるのか原因が知りたいです。
*ThreadIDを出して確認しましたが、複数のtaskは走っているようです。そうなるとMutexの使い方が間違っているのかなと考えたのですが、行き詰まってしまったので質問させて頂きました。宜しくお願いいたします。

【コード】
//文字列の配列クラス
class StringArray
{
private string[] arystring;
private string strTemporary;
private int intI;
private Mutex objMutex = new Mutex();

public StringArray(string[] aarystring)
{
this.arystring = aarystring;
intI = 0;
}

//呼び出されると文字列データを返す()
public string GetString()
{
//配列の限界に達したら空文字を返す
if (intI >= this.arystring.Length)
{
return "空文字__" + intI;
}

//配列のカウントアップの重複を避けたいのでここから多重起動を禁止する
objMutex.WaitOne();

strTemporary = this.arystring[intI];

intI = intI + 1;

objMutex.ReleaseMutex();

return strTemporary + "__" + intI.ToString();
}

}

private void button3_Click(object sender, EventArgs e)
{
int intI;
int intJ;
int intK;

//何らかの文字列配列を用意*今回は0~2000までの数字を文字列として渡すことにします
string[] arystring;
ArrayList aryTemporary = new ArrayList();
for (intI = 0; intI < 2000; intI++)
{
aryTemporary.Add(intI.ToString());
}
arystring = (string[])aryTemporary.ToArray(typeof(string));

//文字列配列のクラスを定義する
StringArray classStringArray = new StringArray(arystring);

//【比較テスト】
//○並列処理ではない場合
/*
var watch0 = Stopwatch.StartNew();
for (intJ = 0; intJ < arystring.Length; intJ++)
{
Console.Write("0結果:{0}経過時間:{1}" + Environment.NewLine, classStringArray.GetString(), watch0.ElapsedMilliseconds);
}
*/

//○並列処理の場合
var watch = Stopwatch.StartNew();
for (intK = 0; intK < 10; intK++)
{
Task.Factory.StartNew(() =>
{
for (intJ = 0; intJ < arystring.Length; intJ++)
{
Console.Write("1実行数:{0}経過時間:{1}" + Environment.NewLine, classStringArray.GetString(), watch.ElapsedMilliseconds);
}
});
}
return;
}

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 3

checkベストアンサー

+1

同時に10個のタスクが走っていても、配列にアクセスできるのはそのうちの一つだけで、他はアクセス待ちになります。待っている間はタスクは停止しますから、結果的には遅くなりますよ。

この場合10個のタスクでそれぞれ配列の一部分のみを担当するようにして(つまり20万要素ある配列なら、2万要素×10と考える)やる方が要望に沿うでしょう。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/02/25 16:49

    あー、そういうことでしたか。同時にアクセス出来るのが1つだけでは全く意味がないですね・・・
    配列を分割する方法を検討してみたいと思います。
    助かりました。回答ありがとうございました。

    キャンセル

+1

以下の記事の図「シングルCPUとマルチCPUにおけるマルチスレッドの比較」を見てください。

第1回 マルチスレッドはこんなときに使う (1/2)
http://www.atmarkit.co.jp/ait/articles/0503/12/news025.html

たぶん、その図の ② のようになっていて、その図の下に書いてあるように "実際にはすべての処理が終了するまでの時間は変わらず、むしろ処理切り替えのために悪化する可能性もある" ということになっているのであろうと思われます。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/02/25 16:49

    まだ知識が浅いのでとても参考になります。
    恐らく私の行いたい処理自体はマルチスレッドに適していると思うのでこのまま進めたいと思います。
    しかしこれを見るとマルチCPUの優秀さがよく分かりますね。処理を実行するPCはもっと贅沢させた方が良いかもしれない・・・
    回答ありがとうございました。

    キャンセル

+1

すでに解決済みでしたか・・・。
せっかく書いたので投稿しておきます。参考まで。

  • まず、「何故並列処理を行って遅い結果となるのか」という問いに対しては、他の方も回答されているとおり「タスクを10個に分けても、結局1か所で待たされているから」というのが答えになります。

  • ご提示の検証用コードですが、「文字列の配列から1つデータを取り出して○○する」の部分が6秒かかるほど重いのであれば、そこを再現するためにThread.Sleepを入れておくとかしないと、このような性能検証は的外れな結論になる危険があります。
    (排他処理をすること自体にそれなりの負荷があるので、GetStringの処理がシンプルすぎると「並列にしないほうが速い」という結論になってしまう)

  • なお、カウントアップの排他にMutexを使われていますが、Mutexはプロセス間での排他が必要な場合に使うのが一般的かなと思います。
    今回のような例ではlockを使うか、カウントアップ変数にInterlockedを使います。

以上をふまえ、Interlockedを使って、必要なところ以外を排他しないように書き直してみた例が以下になります。
(ついでに気になる点にコメントを入れさせていただきました)

//このクラスではカウントを管理しない
//(聞かれた位置の値を処理して返すだけ)
class StringArray
{
    private string[] arystring;

    public StringArray(string[] aarystring)
    {
        this.arystring = aarystring;
    }

    //引数でインデクスを指定してもらう
    public string GetString(int i)
    {
        //時間がかかる処理なのでSleepしておく
        Thread.Sleep(10);

        if (i >= this.arystring.Length)
        {
            return "空文字__" + i;
        }

        return this.arystring[i] + "__" + i;
    }
}
private void button3_Click(object sender, EventArgs e)
{
    //※↓こういうふうに一時変数まで全部宣言しちゃうようなことは
    // C#では普通やらないです。意味がないので。
    // 変数は使う時に宣言します。
    //int intI;
    //int intJ;
    //int intK;

    //※この用途なら普通はList<T>を使います
    //(ArrayListは過去の遺産なので基本使わない)
    var list = new List<string>();
    for (int i = 0; i < 2000; i++)
    {
        list.Add(i.ToString());
    }
    var aryString = list.ToArray();

    //文字列配列のクラスを定義する 
    StringArray classStringArray = new StringArray(aryString);

    //【比較テスト】 
    //○並列処理ではない場合 
    var watch0 = Stopwatch.StartNew();
    for (int i = 0; i < aryString.Length; i++)
    {
        Console.WriteLine(classStringArray.GetString(i));
    }
    //手元の環境では20秒くらい
    Console.WriteLine("0結果:{0}ms", watch0.ElapsedMilliseconds);

    //○並列処理の場合 
    var watch1 = Stopwatch.StartNew();
    int idx = -1;
    var tasks = new Task[10];
    for (int iTask = 0; iTask < tasks.Length; iTask++)
    {
        tasks[iTask] = Task.Factory.StartNew(() =>
        {
            while (true)
            {
                //Interlocked.Incrementでカウントアップしつつ値を取る
                var next = Interlocked.Increment(ref idx);
                if (aryString.Length <= next)
                {
                    break;
                }
                Console.WriteLine(classStringArray.GetString(next));
            }
        });
    }
    Task.WaitAll(tasks);
    //手元の環境では4秒くらい
    Console.WriteLine("1結果:{0}ms", watch1.ElapsedMilliseconds);
}

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

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

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

関連した質問

同じタグがついた質問を見る