Мне этот вопрос async
показался очень интересным, особенно потому, что я везде использую Ado.Net и EF 6. Я надеялся, что кто-то даст объяснение по этому вопросу, но этого не произошло. Поэтому я попытался воспроизвести эту проблему на своей стороне. Я надеюсь, что некоторым из вас это будет интересно.
Первая хорошая новость: воспроизвел :) А разница колоссальная. С коэффициентом 8 ...
Сначала я подозревал, что с чем-то связан CommandBehavior
, так как прочитал интересную статью о async
Адо, в которой говорилось следующее:
"Поскольку режим непоследовательного доступа должен хранить данные для всей строки, это может вызвать проблемы, если вы читаете большой столбец с сервера (например, varbinary (MAX), varchar (MAX), nvarchar (MAX) или XML). ). "
Я подозревал, что ToList()
вызовы CommandBehavior.SequentialAccess
и асинхронные вызовы CommandBehavior.Default
(непоследовательные, что может вызвать проблемы). Поэтому я скачал исходники EF6 и повсюду ставил точки останова ( CommandBehavior
конечно, там, где они использовались).
Результат: ничего . Все вызовы выполняются с помощью CommandBehavior.Default
... Итак, я попытался войти в код EF, чтобы понять, что происходит ... и ... ooouch ... Я никогда не видел такого кода делегирования, все кажется ленивым ...
Итак, я попытался провести профилирование, чтобы понять, что происходит ...
И я думаю, что у меня что-то есть ...
Вот модель для создания таблицы, которую я тестировал, с 3500 строками внутри и 256 КБ случайных данных в каждой varbinary(MAX)
. (EF 6.1 - CodeFirst - CodePlex ):
public class TestContext : DbContext
{
public TestContext()
: base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH")
{
}
public DbSet<TestItem> Items { get; set; }
}
public class TestItem
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] BinaryData { get; set; }
}
А вот код, который я использовал для создания тестовых данных и тестирования EF.
using (TestContext db = new TestContext())
{
if (!db.Items.Any())
{
foreach (int i in Enumerable.Range(0, 3500))
{
byte[] dummyData = new byte[1 << 18];
new Random().NextBytes(dummyData);
db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
}
await db.SaveChangesAsync();
}
}
using (TestContext db = new TestContext())
{
var warmItUp = db.Items.FirstOrDefault();
warmItUp = await db.Items.FirstOrDefaultAsync();
}
Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
watch.Start();
var testRegular = db.Items.ToList();
watch.Stop();
Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}
using (TestContext db = new TestContext())
{
watch.Restart();
var testAsync = await db.Items.ToListAsync();
watch.Stop();
Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.Default);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
}
}
Для обычного вызова EF ( .ToList()
) профилирование кажется "нормальным" и его легко читать:
Здесь мы находим 8,4 секунды, которые у нас есть с секундомером (профилирование замедляет производительность). Мы также находим HitCount = 3500 вдоль пути вызова, что соответствует 3500 строкам в тесте. На стороне парсера TDS все стало хуже, так как мы прочитали 118 353 вызовов TryReadByteArray()
метода, которые были в цикле буферизации. (в среднем 33,8 звонка на каждый byte[]
из 256кб)
Для async
случая это действительно совсем другое .... Сначала .ToListAsync()
вызов запланирован на ThreadPool, а затем ожидается. Здесь ничего удивительного. Но вот, async
черт возьми, ThreadPool:
Во-первых, в первом случае у нас было всего 3500 счетчиков совпадений по всему пути вызова, а здесь 118 371. Более того, вы должны представить себе все вызовы синхронизации, которые я не использовал на снимке экрана ...
Во-вторых, в первом случае у нас было «всего 118 353» вызовов TryReadByteArray()
метода, здесь у нас 2 050 210 вызовов! Это в 17 раз больше ... (на тесте с большим массивом в 1Мб больше в 160 раз)
Кроме того, есть:
Task
Создано 120000 экземпляров
- 727 519
Interlocked
звонков
- 290 569
Monitor
звонков
- 98 283
ExecutionContext
экземпляра, 264 481 захват
- 208733
SpinLock
звонка
Я предполагаю, что буферизация выполняется асинхронно (и не очень хорошо), когда параллельные задачи пытаются читать данные из TDS. Слишком много задач создано только для анализа двоичных данных.
В качестве предварительного вывода мы можем сказать, что Async великолепен, EF6 великолепен, но использование асинхронного режима EF6 в его текущей реализации приводит к значительным накладным расходам со стороны производительности, со стороны потоков и со стороны ЦП (12% использования ЦП в ToList()
case и 20% в ToListAsync
случае для работы в 8-10 раз дольше ... Я запускал его на старом i7 920).
Выполняя некоторые тесты, я снова думал об этой статье и замечаю то, что мне не хватает:
«Для новых асинхронных методов в .Net 4.5 их поведение точно такое же, как и для синхронных методов, за исключением одного примечательного исключения: ReadAsync в непоследовательном режиме».
Какие ?!!!
Поэтому я расширяю свои тесты, чтобы включить Ado.Net в обычный / асинхронный вызов и с CommandBehavior.SequentialAccess
/ CommandBehavior.Default
, и вот большой сюрприз! :
То же самое и с Ado.Net !!! Facepalm ...
Мой окончательный вывод : в реализации EF 6 есть ошибка. Он должен переключить , CommandBehavior
чтобы , SequentialAccess
когда асинхронный вызов выполняется над столом , содержащей binary(max)
колонку. Проблема создания слишком большого количества Задач, замедляющего процесс, находится на стороне Ado.Net. Проблема EF в том, что он не использует Ado.Net должным образом.
Теперь вы знаете, что вместо использования асинхронных методов EF6 вам лучше вызвать EF обычным неасинхронным способом, а затем использовать a TaskCompletionSource<T>
для возврата результата асинхронным способом.
Примечание 1: я отредактировал свой пост из-за постыдной ошибки .... Я провел свой первый тест по сети, а не локально, и ограниченная пропускная способность исказила результаты. Вот обновленные результаты.
Примечание 2: я не распространял свой тест на другие варианты использования (например, nvarchar(max)
с большим количеством данных), но есть вероятность, что произойдет то же самое.
Примечание 3: что-то обычное для этого ToList()
случая - это 12% ЦП (1/8 моего ЦП = 1 логическое ядро). Что-то необычное - это максимум 20% для ToListAsync()
случая, как будто планировщик не может использовать все ступени. Вероятно, это из-за слишком большого количества созданных задач или, может быть, из-за узкого места в парсере TDS, я не знаю ...