Tôi thấy câu hỏi này rất thú vị, đặc biệt là khi tôi đang sử dụng async
ở mọi nơi với Ado.Net và EF 6. Tôi đã hy vọng ai đó đưa ra lời giải thích cho câu hỏi này, nhưng nó đã không xảy ra. Vì vậy, tôi đã cố gắng tái tạo vấn đề này về phía tôi. Tôi hy vọng một số bạn sẽ tìm thấy điều này thú vị.
Tin tốt đầu tiên: Tôi đã sao chép nó :) Và sự khác biệt là rất lớn. Với hệ số 8 ...
Đầu tiên tôi đã nghi ngờ điều gì đó CommandBehavior
, vì tôi đã đọc một bài viết thú vị vềasync
Ado, nói điều này:
"Do chế độ truy cập không tuần tự phải lưu trữ dữ liệu cho toàn bộ hàng, nên nó có thể gây ra sự cố nếu bạn đang đọc một cột lớn từ máy chủ (chẳng hạn như varbinary (MAX), varchar (MAX), nvarchar (MAX) hoặc XML ). "
Tôi đã nghi ngờ ToList()
các cuộc gọi đến CommandBehavior.SequentialAccess
và không đồng bộCommandBehavior.Default
(không liên tục, có thể gây ra sự cố). Vì vậy, tôi đã tải xuống các nguồn của EF6 và đặt các điểm dừng ở mọi nơi ( CommandBehavior
tất nhiên là nơi được sử dụng).
Kết quả: không có gì . Tất cả các cuộc gọi được thực hiện vớiCommandBehavior.Default
.... Vì vậy, tôi đã cố gắng bước vào mã EF để hiểu điều gì xảy ra ... và .. ooouch ... Tôi chưa bao giờ thấy một mã ủy nhiệm như vậy, mọi thứ dường như lười biếng được thực thi ...
Vì vậy, tôi đã cố gắng thực hiện một số hồ sơ để hiểu những gì xảy ra ...
Và tôi nghĩ rằng tôi có một cái gì đó ...
Đây là mô hình để tạo bảng tôi đã được điểm chuẩn, với 3500 dòng bên trong và 256 Kb dữ liệu ngẫu nhiên trong mỗi bảng 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; }
}
Và đây là mã tôi đã sử dụng để tạo dữ liệu thử nghiệm và điểm chuẩn 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);
}
}
Đối với cuộc gọi EF thông thường ( .ToList()
), cấu hình có vẻ "bình thường" và dễ đọc:
Ở đây chúng tôi tìm thấy 8.4 giây chúng tôi có với Đồng hồ bấm giờ (định hình làm chậm sự hoàn hảo). Chúng tôi cũng tìm thấy HitCount = 3500 dọc theo đường dẫn cuộc gọi, phù hợp với 3500 dòng trong thử nghiệm. Về phía trình phân tích cú pháp TDS, mọi thứ bắt đầu trở nên tồi tệ hơn khi chúng ta đọc 118 353 cuộc gọi trên TryReadByteArray()
phương thức, đó là vòng lặp đệm xảy ra. (trung bình 33,8 cuộc gọi cho mỗi byte[]
256kb)
Đối với async
trường hợp, nó thực sự rất khác biệt .... Đầu tiên, .ToListAsync()
cuộc gọi được lên lịch trên ThreadPool, và sau đó được chờ đợi. Không có gì tuyệt vời ở đây. Nhưng, bây giờ, đây là async
địa ngục trên ThreadPool:
Đầu tiên, trong trường hợp đầu tiên, chúng tôi chỉ có 3500 lượt truy cập dọc theo đường dẫn cuộc gọi đầy đủ, ở đây chúng tôi có 118 371. Hơn nữa, bạn phải tưởng tượng tất cả các cuộc gọi đồng bộ hóa mà tôi đã không đặt trên màn hình ...
Thứ hai, trong trường hợp đầu tiên, chúng tôi đã có các cuộc gọi "chỉ 118 353" cho TryReadByteArray()
phương thức, ở đây chúng tôi có 2 cuộc gọi 050 210! Nó gấp 17 lần ... (trong một thử nghiệm với mảng 1Mb lớn, gấp 160 lần)
Hơn nữa, có:
- 120 000
Task
trường hợp được tạo
- 727 519
Interlocked
cuộc gọi
- 290 569
Monitor
cuộc gọi
- 98 283
ExecutionContext
trường hợp, với 264 481 Chụp
- 208 733
SpinLock
cuộc gọi
Tôi đoán là bộ đệm được thực hiện theo cách không đồng bộ (và không phải là tốt), với các Nhiệm vụ song song cố gắng đọc dữ liệu từ TDS. Quá nhiều tác vụ được tạo ra chỉ để phân tích dữ liệu nhị phân.
Như một kết luận sơ bộ, chúng ta có thể nói Async là tuyệt vời, EF6 là tuyệt vời, nhưng cách sử dụng không đồng bộ của EF6 trong việc triển khai hiện tại của nó thêm một chi phí lớn, về phía hiệu suất, phía Threading và phía CPU (sử dụng CPU 12% trong ToList()
trường hợp và 20% trongToListAsync
trường hợp cho một công việc dài hơn 8 đến 10 lần ... Tôi chạy nó trên một chiếc i7 920 cũ).
Trong khi thực hiện một số bài kiểm tra, tôi đã suy nghĩ về bài viết này một lần nữa và tôi nhận thấy một điều tôi bỏ lỡ:
"Đối với các phương thức không đồng bộ mới trong .Net 4.5, hành vi của chúng hoàn toàn giống với các phương thức đồng bộ, ngoại trừ một ngoại lệ đáng chú ý: ReadAsync ở chế độ không tuần tự."
Gì ?!!!
Vì vậy, tôi mở rộng điểm chuẩn của mình để đưa Ado.Net vào cuộc gọi thường xuyên / không đồng bộ và với CommandBehavior.SequentialAccess
/ CommandBehavior.Default
, và đây là một bất ngờ lớn! :
Chúng tôi có hành vi tương tự chính xác với Ado.Net !!! Khuôn mặt ...
Kết luận dứt khoát của tôi là : có lỗi trong triển khai EF 6. Nó sẽ chuyển CommandBehavior
sang SequentialAccess
khi một cuộc gọi không đồng bộ được thực hiện trên một bảng có chứa một binary(max)
cột. Vấn đề tạo quá nhiều Nhiệm vụ, làm chậm quá trình, nằm ở phía Ado.Net. Vấn đề của EF là nó không sử dụng Ado.Net.
Bây giờ bạn đã biết thay vì sử dụng các phương thức không đồng bộ của EF6, tốt hơn bạn nên gọi cho EF theo cách không đồng bộ thông thường, sau đó sử dụng một TaskCompletionSource<T>
để trả về kết quả theo cách không đồng bộ.
Lưu ý 1: Tôi đã chỉnh sửa bài đăng của mình vì một lỗi đáng xấu hổ .... Tôi đã thực hiện thử nghiệm đầu tiên của mình qua mạng chứ không phải cục bộ và băng thông hạn chế đã làm sai lệch kết quả. Dưới đây là kết quả cập nhật.
Lưu ý 2: Tôi đã không mở rộng thử nghiệm của mình sang các trường hợp sử dụng khác (ví dụ: nvarchar(max)
có nhiều dữ liệu), nhưng có nhiều khả năng hành vi tương tự xảy ra.
Lưu ý 3: Một cái gì đó thông thường cho ToList()
trường hợp, là CPU 12% (1/8 CPU của tôi = 1 lõi logic). Một cái gì đó bất thường là tối đa 20% cho ToListAsync()
trường hợp, như thể Trình lập lịch biểu không thể sử dụng tất cả các Tread. Có lẽ do có quá nhiều Tác vụ được tạo hoặc có thể là nút cổ chai trong trình phân tích cú pháp TDS, tôi không biết ...