4

CPU が実際に使用されていないときに、イベントが 1 つのスレッドでキューに入っているように見える場合、BackgroundWorkers とそれらが起動するイベントの奇妙な動作に気付きました。

システムの基本的な設計は、ユーザーの操作に基づいてスレッドが作成され、Web 要求を送信してデータをフェッチするというものです。結果に基づいて、それぞれに対して BackgroundWorkers を使用して、他の多くの非同期リクエストを開始する場合があります。私がこれを行っているのは、リクエストを管理するコードがロックを使用して、一度に 1 つのリクエストのみが送信されるようにするためです (複数の同時リクエストでサーバーにスパムを送信し、サーバーがそれらを無視/ブロックする可能性があるため)。これにはもっと良い設計があるかもしれません.それを聞いてみたいです. ただし、デザインの変更に関係なく、私が見ている動作の原因を知りたいと思っています。

この問題を実証するために、比較的単純なテスト アプリを作成しました。これは基本的に、結果を表示するためのボタンとテキスト ボックスを備えた単なるフォームです (おそらく、フォームを使用せずにコンソールに結果を表示することもできますが、実際のアプリの動作を再現するためにこのようにしました)。コードは次のとおりです。

delegate void AddToLogCallback(string str);

private void AddToLog(string str)
{
    if(textBox1.InvokeRequired)
    {
        AddToLogCallback callback = new AddToLogCallback(AddToLog);
        Invoke(callback, new object[] { str });
    }
    else
    {
        textBox1.Text += DateTime.Now.ToString() + "   " + str + System.Environment.NewLine;
        textBox1.Select(textBox1.Text.Length, 0);
        textBox1.ScrollToCaret();
    }
}

private void Progress(object sender, ProgressChangedEventArgs args)
{
    AddToLog(args.UserState.ToString());
}

private void Completed(object sender, RunWorkerCompletedEventArgs args)
{
    AddToLog(args.Result.ToString());
}

private void DoWork(object sender, DoWorkEventArgs args)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    lock (typeof(Form1)) // Ensure only a single request at a time
    {
        worker.ReportProgress(0, "Start");
        Thread.Sleep(2000); // Simulate waiting on the request
        worker.ReportProgress(50, "Middle");
        Thread.Sleep(2000); // Simulate handling the response from the request
        worker.ReportProgress(100, "End");
        args.Result = args.Argument;
    }
}

private void button1_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(RunMe);
    thread.Start();
}

private void RunMe()
{
    for(int i=0; i < 20; i++)
    {
        AddToLog("Starting " + i.ToString());
        BackgroundWorker worker = new BackgroundWorker();
        worker.WorkerReportsProgress = true;
        worker.DoWork += DoWork;
        worker.RunWorkerCompleted += Completed;
        worker.ProgressChanged += Progress;
        worker.RunWorkerAsync(i);
    }
}

戻ってきた結果は次のとおりです。

30/07/2009 2:43:22 PM   Starting 0
30/07/2009 2:43:22 PM   Starting 1
<snip>
30/07/2009 2:43:22 PM   Starting 18
30/07/2009 2:43:22 PM   Starting 19
30/07/2009 2:43:23 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:36 PM   End
30/07/2009 2:43:36 PM   0
30/07/2009 2:43:36 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:36 PM   End
30/07/2009 2:43:36 PM   1
30/07/2009 2:43:36 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:36 PM   End
30/07/2009 2:43:36 PM   8
30/07/2009 2:43:36 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:38 PM   13
30/07/2009 2:43:38 PM   End
30/07/2009 2:43:38 PM   Start
30/07/2009 2:43:40 PM   Middle
30/07/2009 2:43:42 PM   18
30/07/2009 2:43:42 PM   Start
30/07/2009 2:43:42 PM   End
30/07/2009 2:43:44 PM   Middle
30/07/2009 2:43:46 PM   End
30/07/2009 2:43:46 PM   2
30/07/2009 2:43:46 PM   Start
30/07/2009 2:43:48 PM   Middle

ご覧のとおり、最初の「開始」メッセージが表示されてから 13 秒の遅延があり、その後約 15 個のメッセージを処理します (ほとんどのメッセージが起動されるまでに 2 秒の遅延があるにもかかわらず)。

何が起こっているか知っている人はいますか?

4

3 に答える 3

3

編集:さて、私はゼロから始めています。これは、問題を示す短いが完全なコンソール アプリです。メッセージの時刻とそれがオンになっているスレッドをログに記録します。

using System;
using System.Threading;
using System.ComponentModel;

class Test
{
    static void Main()
    {
        for(int i=0; i < 20; i++)
        {
            Log("Starting " + i);
            BackgroundWorker worker = new BackgroundWorker();
            worker.WorkerReportsProgress = true;
            worker.DoWork += DoWork;
            worker.RunWorkerCompleted += Completed;
            worker.ProgressChanged += Progress;
            worker.RunWorkerAsync(i);
        }
        Console.ReadLine();
    }

    static void Log(object o)
    {
        Console.WriteLine("{0:HH:mm:ss.fff} : {1} : {2}",
            DateTime.Now, Thread.CurrentThread.ManagedThreadId, o);
    }

    private static void Progress(object sender,
                                 ProgressChangedEventArgs args)
    {
        Log(args.UserState);
    }

    private static void Completed(object sender,
                                  RunWorkerCompletedEventArgs args)
    {
        Log(args.Result);
    }

    private static void DoWork(object sender, DoWorkEventArgs args)
    {
        BackgroundWorker worker = (BackgroundWorker) sender;
        Log("Worker " + args.Argument + " started");
        lock (typeof(Test)) // Ensure only a single request at a time
        {
            worker.ReportProgress(0, "Start");
            Thread.Sleep(2000); // Simulate waiting on the request
            worker.ReportProgress(50, "Middle");
            Thread.Sleep(2000); // Simulate handling the response
            worker.ReportProgress(100, "End");
            args.Result = args.Argument;
        }
    }
}

出力例:

14:51:35.323 : 1 : Starting 0
14:51:35.328 : 1 : Starting 1
14:51:35.330 : 1 : Starting 2
14:51:35.330 : 3 : Worker 0 started
14:51:35.334 : 4 : Worker 1 started
14:51:35.332 : 1 : Starting 3
14:51:35.337 : 1 : Starting 4
14:51:35.339 : 1 : Starting 5
14:51:35.340 : 1 : Starting 6
14:51:35.342 : 1 : Starting 7
14:51:35.343 : 1 : Starting 8
14:51:35.345 : 1 : Starting 9
14:51:35.346 : 1 : Starting 10
14:51:35.350 : 1 : Starting 11
14:51:35.351 : 1 : Starting 12
14:51:35.353 : 1 : Starting 13
14:51:35.355 : 1 : Starting 14
14:51:35.356 : 1 : Starting 15
14:51:35.358 : 1 : Starting 16
14:51:35.359 : 1 : Starting 17
14:51:35.361 : 1 : Starting 18
14:51:35.363 : 1 : Starting 19
14:51:36.334 : 5 : Worker 2 started
14:51:36.834 : 6 : Start
14:51:36.835 : 6 : Worker 3 started
14:51:37.334 : 7 : Worker 4 started
14:51:37.834 : 8 : Worker 5 started
14:51:38.334 : 9 : Worker 6 started
14:51:38.836 : 10 : Worker 7 started
14:51:39.334 : 3 : Worker 8 started
14:51:39.335 : 11 : Worker 9 started
14:51:40.335 : 12 : Worker 10 started
14:51:41.335 : 13 : Worker 11 started
14:51:42.335 : 14 : Worker 12 started
14:51:43.334 : 4 : Worker 13 started
14:51:44.335 : 15 : Worker 14 started
14:51:45.336 : 16 : Worker 15 started
14:51:46.335 : 17 : Worker 16 started
14:51:47.334 : 5 : Worker 17 started
14:51:48.335 : 18 : Worker 18 started
14:51:49.335 : 19 : Worker 19 started
14:51:50.335 : 20 : Middle
14:51:50.336 : 20 : End
14:51:50.337 : 20 : Start
14:51:50.339 : 20 : 0
14:51:50.341 : 20 : Middle
14:51:50.343 : 20 : End
14:51:50.344 : 20 : 1
14:51:50.346 : 20 : Start
14:51:50.348 : 20 : Middle
14:51:50.349 : 20 : End
14:51:50.351 : 20 : 2
14:51:50.352 : 20 : Start
14:51:50.354 : 20 : Middle
14:51:51.334 : 6 : End
14:51:51.335 : 6 : Start
14:51:51.334 : 20 : 3
14:51:53.334 : 20 : Middle

(等)

何が起こっているのかを解明しようとしていますが、ワーカー スレッド1 秒間隔で開始されていることに注意することが重要です。

編集: さらなる調査:ThreadPool.SetMinThreads(500, 500)私の Vista ボックスでさえ電話をかけると、基本的にすべてのワーカーが一緒に開始することが示されます。

への呼び出しの有無にかかわらず、上記のプログラムを試すと、ボックスはどうなりますSetMinThreadsか? この場合に役立つが、実際のプログラムではない場合、SetMinThreads呼び出しでも問題があることを示す、同様に短いが完全なプログラムを作成できますか?


私はそれを理解していると信じています。ReportProgressメッセージを処理するための新しいタスクを追加していると思いThreadPoolます... 同時に、スレッド プールに 20 のタスクを追加するのに忙しくしています。ここで、スレッド プールに関する問題は、要求が到着するとすぐに要求を処理するのに十分なスレッドがない場合、プールは新しいスレッドを作成する前に 0.5 秒間待機することです。これは、既存のタスクが終了するのを待つだけであれば、1 つのスレッドで簡単に処理できる一連の要求に対して大量のスレッドを作成することを避けるためです。

つまり、10 秒間、長いキューにタスクを追加し、0.5 秒ごとに新しいスレッドを作成するだけです。20 の「メイン」タスクはすべて比較的長いタスクですが、ReportProgressタスクは非常に短いため、長時間実行されるすべてのリクエストと 1 つの短いリクエストを処理するのに十分なスレッドが得られるとすぐに、あなたは離れてすべてのタスクを実行できます。メッセージはすぐに届きます。

に呼び出しを追加する場合

ThreadPool.SetMaxThreads(50, 50);

すべてが始まる前に、期待どおりに動作することがわかります。実際のアプリケーションで必ずこれを行う必要があることを示唆しているわけではありませんが、違いを示すためです。これにより、最初はプール内に一連のスレッドが作成され、リクエストを待つだけになります。

設計に関する 1 つのコメント: 異なるスレッドに 20 の異なるタスクがありますが、実際に一度に実行できるのはそのうちの 1 つだけです (ロックのため)。とにかくリクエストを効果的にシリアライズしているのに、なぜ複数のスレッドを使用するのでしょうか? あなたの実際のアプリケーションにこの問題がないことを願っています。

于 2009-07-30T06:09:39.383 に答える
2

BackgroundWorker クラスは、作成中のスレッドでコールバックを発行します。これは、InvokeRequired に続く Invoke() または BeginInvoke() で追加のチェックを行う必要がないため、UI タスクにとって非常に便利です。

欠点は、作成中のコードがブロックされている場合、またはタイトなループにある場合、コールバックがキューに入れられることです。

解決策は、スレッドを自分で管理することです。スレッドを手動で作成する方法を知っていることは既に示されていますが、その方法の詳細については ThreadPool トピックを参照してください。

更新: これは、キューとカスタム SingletonWorker スレッドを使用するフィードバックに基づく実用的なサンプルです。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        SingletonWorker.ProgressHandler = Progress;
        SingletonWorker.CompleteHandler = Completed;
    }
    private void button1_Click( object sender, EventArgs e )
    {
        // this is based on an app requirement, seems odd but I'm sure there's a reason :)
        Thread thread = new Thread( AddTasks );
        thread.Start();
    }
    private void AddTasks()
    {
        for ( int i = 0; i < 5; i++ )
        {
            AddToLog( "Creating Task " + i );
            SingletonWorker.AddTask( new Task { NumberToWorkOn = i } );
        }
    }
    private void AddToLog( string message )
    {
        if( textBox1.InvokeRequired )
        {
            textBox1.Invoke( new Action<string>( AddToLog ), message );
            return;
        }
        textBox1.Text += DateTime.Now + "   " + message + System.Environment.NewLine;
        textBox1.Select( textBox1.Text.Length, 0 );
        textBox1.ScrollToCaret();
    }
    private void Progress( string message, int percentComplete )
    {
        AddToLog( String.Format( "{0}%, {1}", percentComplete, message ) );
    }
    private void Completed( string message )
    {
        AddToLog( message );
    }
}
public class Task
{
    public int NumberToWorkOn { get; set; }
}
public static class SingletonWorker
{
    private static readonly Thread Worker;
    private static readonly Queue<Task> Tasks;
    // assume params are 'message' and 'percent complete'
    // also assume only one listener, otherwise use events
    public static Action<string, int> ProgressHandler;
    public static Action<string> CompleteHandler;
    static SingletonWorker()
    {
        Worker = new Thread( Start );
        Tasks = new Queue<Task>();
        Worker.Start();
    }
    private static Task GetNextTask()
    {
        lock( Tasks )
        {
            if ( Tasks.Count > 0 )
                return Tasks.Dequeue();

            return null;
        }
    }
    public static void AddTask( Task task )
    {
        lock( Tasks )
        {
            Tasks.Enqueue( task );
        }
    }
    private static void Start()
    {
        while( true )
        {
            Task task = GetNextTask();
            if( task == null )
            {
                // sleep for 500ms waiting for another item to be enqueued
                Thread.Sleep( 500 );
            }
            else
            {
                // work on it
                ProgressHandler( "Starting on " + task.NumberToWorkOn, 0 );
                Thread.Sleep( 1000 );
                ProgressHandler( "Almost done with " + task.NumberToWorkOn, 50 );
                Thread.Sleep( 1000 );
                CompleteHandler( "Finished with " + task.NumberToWorkOn );
            }
        }
    }
}
于 2009-07-30T07:03:54.250 に答える
0

同じ問題が発生しました。BackgroundWorkerスレッドがシリアル方式で実行されていました。解決策は、コードに次の行を追加することでした。

ThreadPool.SetMinThreads(100, 100);

デフォルトのMinThreadsは1であるため、(おそらく主にシングルコアCPUで)スレッドスケジューラは、BackgroundWorkerまたはThreadPoolを使用してスレッドを作成している場合、同時スレッドの数として1が許容可能であると想定します。したがって、スレッドはで動作します。シリアルファッションすなわち。後で開始されるスレッドに、前のスレッドが終了するのを待機させます。より高い最小値を許可するように強制することで、複数のスレッドを並行して実行するように強制します。つまり、コアよりも多くのスレッドを実行する場合はタイムスライスします。

この動作は、SetMinThreadsの値を増やさなくても、同時に正しく機能しているように見えるThreadクラス(つまり、thread.start())では現れません。

また、Webサービスへの呼び出しが一度に最大2つまでしか機能しないことがわかった場合、これは2がWebサービス呼び出しのデフォルトの最大値であるためです。これを増やすには、app.configファイルに次のコードを追加する必要があります。

<system.net>
  <connectionManagement>
    <add address="*" maxconnection="100" />
  </connectionManagement>
</system.net>
于 2011-01-31T21:28:40.923 に答える