问题
I started off trying to add a progress bar to the windows form that updates the progress of code running within a Parallel.Foreach loop. In order to do this the UI thread has to be available to update the progress bar. I used a Task to run the Parallel.Foreach loop to allow the UI thread to update the progress bar.
The work done within the Parallel.Foreach loop is rather intensive. After running the executables of the program(not debugging within visual studio) with the Task, the program became unresponsive. This is not the case if I run my program without Task. The key difference I noticed between the two instances is that the program takes ~80% of the cpu when ran without Task, and ~5% when ran with Task.
private void btnGenerate_Click(object sender, EventArgs e)
{
var list = GenerateList();
int value = 0;
var progressLock = new object ();
progressBar1.Maximum = list.Count();
Task t = new Task(() => Parallel.ForEach (list, item =>
{
DoWork ();
lock (progressLock)
{
value += 1;
}
}));
t.Start();
while (!t.IsCompleted)
{
progressBar1.Value = value;
Thread.Sleep (100);
}
}
Side Note: I know that
Interlocked.Increment(ref int___);
works in place of the lock. Is it considered more efficient?
My Question is three fold:
1.) Why would the program with the Task become unresponsive when the load is much less?
2.) Does using Task to run Parallel.Foreach limit the thread pool of the Parallel.Foreach to only the thread running the task?
3.) Is there a way to make the UI thread responsive instead of sleeping for the .1 second duration without using cancellation token?
I'm grateful for any help or ideas, I've spent quite a lot of time researching this. I also apologize if I've violated any posting format or rules. I tried to adhere to them, but may have missed something.
回答1:
You can greatly simplify your code there by using the built in Invoke
method that invokes a delegate on the owning Windows synchronization context.
From MSDN:
Executes the specified delegate on the thread that owns the control's underlying window handle.
The Invoke method searches up the control's parent chain until it finds a control or form that has a window handle if the current control's underlying window handle does not exist yet.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
string[] GenerateList() => new string[500];
void DoWork()
{
Thread.Sleep(50);
}
private void button1_Click(object sender, EventArgs e)
{
var list = GenerateList();
progressBar1.Maximum = list.Length;
Task.Run(() => Parallel.ForEach(list, item =>
{
DoWork();
// Update the progress bar on the Synchronization Context that owns this Form.
this.Invoke(new Action(() => this.progressBar1.Value++));
}));
}
}
This will invoke the Action
delegate on the same UI thread that the Form belongs to, from within the Task.
Now to try and answer your questions
1.) Why would the program with the Task become unresponsive when the load is much less?
I'm not 100% sure, but this could be related to you locking a member on the UI thread. If the load is less, then the lock will happen more frequently, potentially causing the UI thread to "hang" while the progressbar is incremented.
You are also running a while loop that is sleeping the UI thread every 100 milliseconds. You'll see UI hanging due to that while loop.
2.) Does using Task to run Parallel.Foreach limit the thread pool of the Parallel.Foreach to only the thread running the task?
It does not. Several tasks will get created within the Parallel.ForEach
call. The underlying ForEach
uses a partitioner to spread the work out, and not create more tasks than what is necessary. It creates tasks in batches, and processes the batches.
3.) Is there a way to make the UI thread responsive instead of sleeping for the .1 second duration without using cancellation token?
I was able to handle that by removing the while
loop and using the Invoke
method to just go ahead and execute a lambda on the UI thread directly.
来源:https://stackoverflow.com/questions/30519430/using-task-with-parallel-foreach-in-net-4-0