Мені це питання було дуже цікавим, тим більше, що я користуюся async
скрізь з Ado.Net та EF 6. Я сподівався, що хтось дасть пояснення цьому питанню, але це не сталося. Тому я спробував відтворити цю проблему на моєму боці. Сподіваюсь, хтось із вас знайде це цікавим.
Перша гарна новина: я відтворив її :) І різниця величезна. З коефіцієнтом 8 ...
Спочатку я підозрював щось, що має справу CommandBehavior
, оскільки я прочитав цікаву статтю про async
Адо, кажучи про це:
"Оскільки в режимі непослідовного доступу потрібно зберігати дані для всього рядка, це може спричинити проблеми, якщо ви читаєте великий стовпець із сервера (наприклад, варбінарний (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") // Local instance
{
}
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)) // Fill 3500 lines
{
byte[] dummyData = new byte[1 << 18]; // with 256 Kbyte
new Random().NextBytes(dummyData);
db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
}
await db.SaveChangesAsync();
}
}
using (TestContext db = new TestContext()) // EF Warm Up
{
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
Створено 120 000 екземплярів
- 727 519
Interlocked
дзвінків
- 290 569
Monitor
дзвінків
- 98 283
ExecutionContext
екземпляри з 264 481 захопленнями
- 208 733
SpinLock
дзвінків
Я здогадуюсь, що буферизація проводиться асинхронним способом (і не дуже добре), паралельні завдання намагаються прочитати дані з TDS. Занадто багато завдань створено просто для аналізу бінарних даних.
Як попередній висновок, ми можемо сказати, що Async чудовий, EF6 - чудовий, але використання асинхронного режиму EF6 в його поточній реалізації додає значних витрат на стороні продуктивності, нарізання потоку та процесора (12% використання процесора в ToList()
випадку і 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-аналізаторі, я не знаю ...