以并行方式将项目添加到 ListBox
Adding items to a ListBox in a parallel way
我正在编写一个简单的应用程序(用于测试目的),它将 10 M 元素添加到 ListBox。我正在使用 BackgroundWorker 来完成工作,并使用 ProgressBar 控件来显示进度。
每个元素只是一个“Hello World!”字符串和我在此过程中添加的索引。我的程序需要大约 7-8 秒来填充 ListBox,我想是否可以通过使用我 PC 上的所有可用内核 (8) 来加快速度。
为了实现这一点,我尝试使用 TPL 库,更准确地说是 Parallel.For 循环,但结果不可预测或不起作用如我所愿。
这是我的应用程序代码:
private BackgroundWorker worker = new BackgroundWorker();
private Stopwatch sw = new Stopwatch();
private List<String> numbersList = new List<String>();
public MainWindow()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
}
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
worker.RunWorkerAsync();
}
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
sw.Start();
int max = 10000000;
int oldProgress = 0;
for (int i = 1; i <= max; i++)
{
numbersList.Add("Hello World! [" + i + "]");
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
worker.ReportProgress(progressPercentage);
oldProgress = progressPercentage;
}
}
}
private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pb.Value = e.ProgressPercentage;
}
private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
lstLoremIpsum.ItemsSource = numbersList;
lblCompleted.Content = "OK";
lblCompleted.Content += " (" + numbersList.Count + " elements added" + ")";
lblElementiLista.Content += " (" +sw.Elapsed.TotalSeconds + ")";
worker.Dispose();
}
}
以及我尝试编写的并行实现(在 DoWork 中):
Parallel.For(1, max, i =>
{
lock (lockObject)
{
numbersList.Add("Hello World! [" + i + "]");
}
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
worker.ReportProgress(progressPercentage);
oldProgress = progressPercentage;
}
});
结果是应用程序冻结,大约需要 15 秒 来填满我的列表框。 (元素也是无序的)
在这种情况下可以做什么,并行性是否会加快 "filling" 进程?
您在每次添加时都锁定了列表,而所有的进程负载就是向列表中添加一个元素,因此您没有加快速度而是减慢了它们的速度,因为实际上没有并行工作。
如果您的项目列表的大小已知(看起来),则创建一个具有适当大小的数组而不是列表,然后在并行 for 循环中将适当的项目设置为它的值,以这种方式不执行锁定,应该会更快。
此外,在您的代码中,您不会显示何时填充列表视图,只显示列表,所以我想您正在使用此列表作为数据源,在设置它之前执行 listView.BeginUpdate() 和设置listView.EndUpdate()后,可能会加快速度,m添加元素时listview有点慢。
如果您使用 Parallel.For,则不需要 BackgroundWorker。并且 Worker 不再像预期的那样工作,因为您正试图从另一个线程访问它。
去掉BackgroundWorker,直接做Parallel.For,使用Interlocked方法更新进度条:
private int ProgressPercentage { get; set; }
private void DoWork()
{
Parallel.For(1, max, i =>
{
lock (lockObject)
{
numbersList.Add("Hello World! [" + i + "]");
}
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
Interlocked.Exchange(ProgressPercentage, progressPercentage);
ShowProgress();
}
});
}
private void ShowProgress()
{
pb.Value = ProgressPercentage;
}
线程中的 lock 语句基本上将并行处理减少为顺序处理,但会产生获取锁的开销(使其实际上变慢)。
此外,这里可以使用的线程池线程数量有限,因此您不会同时添加全部 10m。
我认为更好的方法是使用非 UI 线程来填充列表然后绑定它 - 这将确保 UI 不是 frozen/unusable 而1000 万次迭代循环是 运行:
public MainWindow()
{
InitializeComponent();
Task.Factory.StartNew(PopList);
}
然后你可以在需要的时候调用UI线程:
private void PopList()
{
sw.Start();
int max = 10000000;
int oldProgress = 0;
for (int i = 1; i <= max; i++)
{
numbersList.Add("Hello World! [" + i + "]");
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
Dispatcher.BeginInvoke(new Action(() => { pb.Value = progressPercentage; }));
oldProgress = progressPercentage;
}
}
Dispatcher.BeginInvoke(new Action(() => { lstLoremIpsum.ItemsSource = numbersList; }));
}
在 MVVM 世界中,您可以设置绑定的 IEnumerable 而不是上面示例中所示的 ItemsSource。
我正在编写一个简单的应用程序(用于测试目的),它将 10 M 元素添加到 ListBox。我正在使用 BackgroundWorker 来完成工作,并使用 ProgressBar 控件来显示进度。
每个元素只是一个“Hello World!”字符串和我在此过程中添加的索引。我的程序需要大约 7-8 秒来填充 ListBox,我想是否可以通过使用我 PC 上的所有可用内核 (8) 来加快速度。
为了实现这一点,我尝试使用 TPL 库,更准确地说是 Parallel.For 循环,但结果不可预测或不起作用如我所愿。
这是我的应用程序代码:
private BackgroundWorker worker = new BackgroundWorker();
private Stopwatch sw = new Stopwatch();
private List<String> numbersList = new List<String>();
public MainWindow()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
}
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
worker.RunWorkerAsync();
}
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
sw.Start();
int max = 10000000;
int oldProgress = 0;
for (int i = 1; i <= max; i++)
{
numbersList.Add("Hello World! [" + i + "]");
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
worker.ReportProgress(progressPercentage);
oldProgress = progressPercentage;
}
}
}
private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pb.Value = e.ProgressPercentage;
}
private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
lstLoremIpsum.ItemsSource = numbersList;
lblCompleted.Content = "OK";
lblCompleted.Content += " (" + numbersList.Count + " elements added" + ")";
lblElementiLista.Content += " (" +sw.Elapsed.TotalSeconds + ")";
worker.Dispose();
}
}
以及我尝试编写的并行实现(在 DoWork 中):
Parallel.For(1, max, i =>
{
lock (lockObject)
{
numbersList.Add("Hello World! [" + i + "]");
}
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
worker.ReportProgress(progressPercentage);
oldProgress = progressPercentage;
}
});
结果是应用程序冻结,大约需要 15 秒 来填满我的列表框。 (元素也是无序的)
在这种情况下可以做什么,并行性是否会加快 "filling" 进程?
您在每次添加时都锁定了列表,而所有的进程负载就是向列表中添加一个元素,因此您没有加快速度而是减慢了它们的速度,因为实际上没有并行工作。
如果您的项目列表的大小已知(看起来),则创建一个具有适当大小的数组而不是列表,然后在并行 for 循环中将适当的项目设置为它的值,以这种方式不执行锁定,应该会更快。
此外,在您的代码中,您不会显示何时填充列表视图,只显示列表,所以我想您正在使用此列表作为数据源,在设置它之前执行 listView.BeginUpdate() 和设置listView.EndUpdate()后,可能会加快速度,m添加元素时listview有点慢。
如果您使用 Parallel.For,则不需要 BackgroundWorker。并且 Worker 不再像预期的那样工作,因为您正试图从另一个线程访问它。
去掉BackgroundWorker,直接做Parallel.For,使用Interlocked方法更新进度条:
private int ProgressPercentage { get; set; }
private void DoWork()
{
Parallel.For(1, max, i =>
{
lock (lockObject)
{
numbersList.Add("Hello World! [" + i + "]");
}
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
Interlocked.Exchange(ProgressPercentage, progressPercentage);
ShowProgress();
}
});
}
private void ShowProgress()
{
pb.Value = ProgressPercentage;
}
线程中的 lock 语句基本上将并行处理减少为顺序处理,但会产生获取锁的开销(使其实际上变慢)。
此外,这里可以使用的线程池线程数量有限,因此您不会同时添加全部 10m。
我认为更好的方法是使用非 UI 线程来填充列表然后绑定它 - 这将确保 UI 不是 frozen/unusable 而1000 万次迭代循环是 运行:
public MainWindow()
{
InitializeComponent();
Task.Factory.StartNew(PopList);
}
然后你可以在需要的时候调用UI线程:
private void PopList()
{
sw.Start();
int max = 10000000;
int oldProgress = 0;
for (int i = 1; i <= max; i++)
{
numbersList.Add("Hello World! [" + i + "]");
int progressPercentage = Convert.ToInt32((double)i / max * 100);
// Only report progress when it changes
if (progressPercentage != oldProgress)
{
Dispatcher.BeginInvoke(new Action(() => { pb.Value = progressPercentage; }));
oldProgress = progressPercentage;
}
}
Dispatcher.BeginInvoke(new Action(() => { lstLoremIpsum.ItemsSource = numbersList; }));
}
在 MVVM 世界中,您可以设置绑定的 IEnumerable 而不是上面示例中所示的 ItemsSource。