Нюансы в распараллеливании задачи по заполнению массива данными
В целях инспекции кода и познании большего в распараллеливании, перенёс ответ из () в этот вопрос.
Изначальный вопрос: Есть обычный Windows Form's и огромные объемы данных,которые надо на него вывести,желательно с выделением некоторых слов другим текстом + функция автоскролла. Вопрос в том,как лучше и выгоднее это сделать.Пробовал через RichTextBox,чуть не заснул ,пока он это делал. Может кто сталкивался?Писать класс что-то не хочется...
Дальше приведён код моего ответа на данный вопрос.
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Test
{
public partial class Form1 : Form
{
double[] a = new double[10000];
public Form1()
{
InitializeComponent();
listView1.RetrieveVirtualItem += (sender, item) =>
item.Item = new ListViewItem(a[item.ItemIndex].ToString("F3"), 0);
}
async private void button2_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
Parallel.For(0, a.Length, i =>
{
a[i] = RandomGen.Next();
});
});
}
private void button1_Click(object sender, EventArgs e)
{
listView1.VirtualListSize = a.Length;
}
}
public static class RandomGen
{
private static Random global = new Random();
[ThreadStatic]
private static Random local;
public static int Next()
{
Random instance = local;
if (instance == null)
{
int seed;
lock (global) seed = global.Next();
local = instance = new Random(seed);
}
return instance.Next(1,4);
}
}
}
Ответы (1 шт):
Как положительный момент я хочу отметить понимание того факта, что класс Random не является потокобезопасным. И поэтому создаётся свой экземпляр дла каждого потока.
Также радует понимание работы атрибута ThreadStatic. Хотя я ни разу не встречал его в реальном коде.
Десять тысяч элементов - это совсем немного. И заполнение массива простым циклом:
var random = new Random();
for (int i = 0; i < a.Length; i++)
{
a[i] = random.NextDouble();
}
будет быстрее без распараллеливания. Потому что запуск потоков, пусть даже взятых из пула, - тоже требует времени.
Что ещё плохо в этом коде:
Parallel.For(0, a.Length, i =>
{
a[i] = RandomGen.Next();
});
Тут каждая ячейка массива заполняется в отдельном потоке. Гораздо лучше будет разбить массив на диапазоны и заполнять каждый диапазон полностью в одном потоке.
Для этого удобно использовать метод Partitioner.Create. Пример:
var partitioner = Partitioner.Create(0, a.Length);
Parallel.ForEach(partitioner, range =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
a[i] = random.NextDouble();
}
});
Больше информации по ссылке: How to: Speed Up Small Loop Bodies
При каких размерах массива следует переходить на параллельность?
Для начала качаем небольшую (сто страниц) книгу Patterns of parallel programming by Stephen Toub. Это официально бесплатно.
В ней в разделе False sharing (стр. 44) есть пример многопоточного заполнения массивов. Размер там двадцать миллионов элементов. Вот на таких величинах распараллеливание окупается.
И там объяснен ещё один момент, который может сильно ухудшить производительность: False sharing.
Собственно я и затеял этот ответ, чтобы ещё раз порекламировать эту книгу и посоветовать всем дотнетчикам с ней ознакомиться (моя настольная книга).
Конкретно в вашем примере кода каждый экземпляр Random, вероятно, будет попадать в отдельную линию кэша, потому что они создаются не одновременно. Поэтому проблема False sharing не стоит. Но о ней следует знать.