मुझे यह सवाल बहुत दिलचस्प लगा, खासकर जब से मैं async
Ado.Net और EF 6 के साथ हर जगह उपयोग कर रहा हूं। मैं उम्मीद कर रहा था कि कोई इस प्रश्न के लिए स्पष्टीकरण देगा, लेकिन ऐसा नहीं हुआ। इसलिए मैंने अपनी तरफ से इस समस्या को दोहराने की कोशिश की। मुझे उम्मीद है कि आप में से कुछ को यह दिलचस्प लगेगा।
पहली अच्छी खबर: मैंने इसे पुन: पेश किया :) और अंतर बहुत बड़ा है। एक कारक 8 के साथ ...
सबसे पहले मैं के साथ काम कर कुछ शक था CommandBehavior
के बाद से, मैं एक दिलचस्प लेख को पढ़ने के बारे में async
हलचल के साथ इस कहा:
"चूंकि गैर-अनुक्रमिक पहुंच मोड में संपूर्ण पंक्ति के लिए डेटा संग्रहीत करना पड़ता है, इसलिए यह समस्या पैदा कर सकता है यदि आप सर्वर से एक बड़ा कॉलम पढ़ रहे हैं (जैसे कि varbinary (MAX), varchar (MAX), nvarchar (MAX) या XML )। "
मुझे संदेह था कि ToList()
कॉल होने के लिए CommandBehavior.SequentialAccess
और async वाले होने के लिए CommandBehavior.Default
(गैर-अनुक्रमिक है, जो मुद्दों का कारण बन सकता है)। इसलिए मैंने EF6 के स्रोतों को डाउनलोड किया, और हर जगह ब्रेकपॉइंट्स लगाए (जहां CommandBehavior
जहां इस्तेमाल किया, निश्चित रूप से)।
परिणाम: कुछ नहीं । सभी कॉल किए जाते हैं CommandBehavior.Default
.... इसलिए मैंने यह समझने के लिए ईएफ कोड में कदम रखने की कोशिश की कि क्या होता है ... और .. ऊऊच ... मैंने कभी ऐसा प्रतिनिधि कोड नहीं देखा, सब कुछ आलसी लगता है ...
इसलिए मैंने यह समझने की कोशिश की कि क्या होता है?
और मुझे लगता है कि मेरे पास कुछ है ...
यहां तालिका I बेंचमार्क बनाने के लिए मॉडल है, इसके अंदर 3500 लाइनें और प्रत्येक में 256 Kb यादृच्छिक डेटा है varbinary(MAX)
। (एफई 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);
}
}
नियमित ईएफ कॉल ( .ToList()
) के लिए, प्रोफाइलिंग "सामान्य" लगती है और पढ़ने में आसान है:
यहां हमें स्टॉपवॉच के साथ हमारे पास 8.4 सेकंड मिलते हैं (प्रोफाइल धीमा हो जाता है। हमें कॉल पथ के साथ HitCount = 3500 भी मिलता है, जो परीक्षण में 3500 लाइनों के अनुरूप है। टीडीएस पार्सर की ओर से, चीजें बदतर होने लगती हैं क्योंकि हम TryReadByteArray()
विधि पर 118 353 कॉल पढ़ते हैं , जो बफरिंग लूप थे। (प्रत्येक byte[]
256kb के लिए औसत 33.8 कॉल )
के लिए async
मामला है, यह वास्तव में वास्तव में अलग है .... सबसे पहले, .ToListAsync()
कॉल ThreadPool पर निर्धारित है, और उसके बाद प्रतीक्षा कर रहे थे। यहां कुछ भी अद्भुत नहीं है। लेकिन, अब, async
थ्रेडपोल पर यहाँ नरक है:
सबसे पहले, पहले मामले में हम पूर्ण कॉल पथ के साथ सिर्फ 3500 हिट काउंट कर रहे थे, यहां हमारे पास 118 371 हैं। इसके अलावा, आपको उन सभी सिंक्रोनाइज़ेशन कॉल की कल्पना करनी होगी, जिन्हें मैंने स्क्रीनशूट पर नहीं डाला था ...
दूसरा, पहले मामले में, हम TryReadByteArray()
विधि के लिए "सिर्फ 118 353" कॉल कर रहे थे , यहाँ हमारे पास 2 050 210 कॉल हैं! यह 17 गुना अधिक है ... (बड़े 1Mb सरणी के साथ एक परीक्षण पर, यह 160 गुना अधिक है)
इसके अलावा हैं:
- 120 000
Task
उदाहरण बनाए
- 727 519 पर
Interlocked
कॉल करें
- 290 569
Monitor
कॉल
- 98 283
ExecutionContext
उदाहरण, 264 481 कैप्चर के साथ
- 208 733 पर
SpinLock
कॉल करता है
मेरा अनुमान है कि बफ़रिंग एक एस्सेन्ट तरीके (और एक अच्छा नहीं) से बना है, जिसमें समानांतर टास्क टीडीएस से डेटा पढ़ने की कोशिश कर रहे हैं। बाइनरी डेटा को पार्स करने के लिए बहुत सारे टास्क बनाए जाते हैं।
प्रारंभिक निष्कर्ष के रूप में, हम कह सकते हैं कि Async महान है, EF6 महान है, लेकिन वर्तमान कार्यान्वयन में async का EF6 का उपयोग एक प्रमुख ओवरहेड जोड़ता है, प्रदर्शन पक्ष, थ्रेडिंग पक्ष, और CPU पक्ष (12% CPU उपयोग में) ToList()
मामला और ToListAsync
8 से 10 गुना लंबे समय तक काम करने के मामले में 20% ... मैं इसे एक पुराने i7 920 पर चलाता हूं)।
कुछ परीक्षण करते समय, मैं इस लेख के बारे में फिर से सोच रहा था और मुझे कुछ याद आ गया है:
".Net 4.5 में नए अतुल्यकालिक तरीकों के लिए, उनका व्यवहार बिल्कुल तुल्यकालिक विधियों के समान है, सिवाय एक उल्लेखनीय अपवाद के: गैर-अनुक्रमिक मोड में रीडअंश।"
क्या ?!!!
इसलिए मैं Ado.Net को नियमित / async कॉल और CommandBehavior.SequentialAccess
/ / के साथ शामिल करने के लिए अपने बेंचमार्क का विस्तार करता हूं CommandBehavior.Default
, और यहां एक बड़ा आश्चर्य है! :
हमारे पास Ado.Net के साथ ठीक वैसा ही व्यवहार है !!! Facepalm ...
मेरा निश्चित निष्कर्ष है : EF 6 कार्यान्वयन में एक बग है। यह टॉगल चाहिए CommandBehavior
करने के लिए SequentialAccess
एक async कॉल एक से युक्त एक मेज पर किया जाता है जब binary(max)
स्तंभ। प्रक्रिया को धीमा करते हुए बहुत सारे टास्क बनाने की समस्या Ado.Net की तरफ है। EF समस्या यह है कि यह Ado.Net का उपयोग नहीं करता है जैसा कि इसे करना चाहिए।
अब आप EF6 async विधियों का उपयोग करने के बजाय जानते हैं, तो आपको EF को नियमित रूप से गैर- async तरीके से कॉल करना होगा, और फिर एक का उपयोग करना होगा TaskCompletionSource<T>
परिणाम को async तरीके से वापस करने के लिए होगा।
नोट 1: मैंने एक शर्मनाक त्रुटि के कारण अपनी पोस्ट को संपादित किया .... मैंने अपना पहला परीक्षण नेटवर्क पर किया है, स्थानीय रूप से नहीं, और सीमित बैंडविड्थ ने परिणामों को विकृत कर दिया है। यहाँ अद्यतन परिणाम हैं।
नोट 2: मैंने अन्य उपयोग मामलों के लिए अपने परीक्षण का विस्तार नहीं किया (उदा: nvarchar(max)
बहुत अधिक डेटा के साथ) में नहीं किया है, लेकिन संभावना है कि समान व्यवहार होता है।
नोट 3: ToList()
मामले के लिए कुछ सामान्य है, 12% सीपीयू (1/8 ऑफ माय सीपीयू = 1 लॉजिकल कोर) है। कुछ असामान्य ToListAsync()
मामला के लिए अधिकतम 20% है , जैसे कि शेड्यूलर सभी Treads का उपयोग नहीं कर सकता है। यह शायद बहुत सारे टास्क के कारण, या शायद टीडीएस पार्सर में अड़चन है, मुझे नहीं पता ...