در EF 6.x به ازای هر عبارت insert/update/delete یکبار رفت و برگشت به بانک اطلاعاتی صورت میگیرد. به همین جهت کارآیی تعداد بالای ثبت، به روز رسانی و حذف رکوردها توسط آن پایین است. برای رفع این مشکل ویژگی Batching به EF Core اضافه شدهاست که توسط آن اینبار دستهای از عبارات را به صورت یکجا و در طی یک رفت و برگشت، به سمت بانک اطلاعاتی ارسال میکند. به این ترتیب کارآیی و سرعت insert/update/delete آن به شدت افزایش خواهد یافت.
نحوهی فعالسازی Batching در EF Core
Batching به صورت پیش فرض در EF Core بدون نیاز به هیچگونه تنظیم اضافهتری فعال است. اما اگر خواستید برای مثال، حالت پیش فرض EF 6.x را توسط آن شبیه سازی کنید، میتوانید مقدار MaxBatchSize را به عدد 1 تنظیم نمائید (تا غیرفعال شود):
مقدار پیش فرض MaxBatchSize را در کلاس SqlServerModificationCommandBatch میتوانید مشاهده کنید:
در اینجا MaxRowCount همان MaxBatchSize پیش فرض است که به عدد 1000 تنظیم شدهاست. بنابراین اگر تنظیم options => options.MaxBatchSize(1) را ذکر نکنید، به معنای ارسال 1000 تایی دستورات insert/update/delete در طی یک درخواست به سمت سرور است.
آیا محدودیتی هم در مورد عملیات Batching وجود دارد؟
SQL Server به ازای هر batch تنها 2100 پارامتر را پشتیبانی میکند. در این حالت EF Core به صورت خودکار یک چنین کوئریهای حجیمی را به چند Batch جهت تنظیم این محدودیت تقسیم خواهد کرد و در نهایت برنامه به مشکلی بر نمیخورد.
یک آزمایش: Batching پیش فرض به چه صورتی کار میکند و چه اثری را دارد؟
کدهای کامل این آزمایش را از اینجا میتوانید دریافت کنید: Batching.zip
در اینجا کلاس Blog را به همراه Context متناظر با آن مشاهده میکنید:
در ابتدا MaxBatchSize را تنظیم نخواهیم کرد. یعنی از همان عدد 1000 پیش فرض استفاده میشود. تنظیم EnableSensitiveDataLogging نیز سبب میشود تا لاگ نهایی تهیه شده جهت نمایش، پرمحتواتر شود.
در این حالت اگر به روز رسانیها (2 مورد) و ثبتهای ذیل (6 مورد) را انجام دهیم:
یک چنین خروجی SQL ایی تولید میشود:
در این دستورات موارد ذیل قابل توجه هستند:
- فقط یکبار Executed DbCommand مشاهده میشود.
- کل دستورات update و insert در طی یک درخواست و یک تراکنش به سمت بانک اطلاعاتی ارسال شدهاند.
- ثبت دستهای توسط merge using انجام شدهاست.
- در آخر نیز طبق معمول کار EF، شماره Idهای رکوردهای ثبت شده به سمت کلاینت بازگشت داده میشود.
در ادامه MaxBatchSize را به عدد 2 تنظیم میکنیم:
در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:
در این دستورات موارد ذیل قابل توجه هستند:
- اینبار تعداد 4 دستور Executed DbCommand مشاهده میشود ( برای انجام 2 به روز رسانی و 6 ثبت).
- هر batch بر اساس تنظیم MaxBatchSize به 2 دستور T-SQL محدود شدهاست که البته در انتها در حالتهای insert، یک select هم برای بازگشت Idها به سمت کلاینت وجود دارد.
بنابراین اینبار بجای یکبار رفت و برگشت حالت قبل (استفاده از مقدار پیش فرض 1000 برای MaxBatchSize)، 4 بار رفت و برگشت به سمت بانک اطلاعاتی صورت گرفتهاست.
زمان کل انجام عملیات در حالت اول 41 میلی ثانیه و در حالت دوم 84 میلی ثانیه است که سرعت آن 51 درصد نسبت به حالت اول کاهش یافتهاست.
نحوهی فعالسازی Batching در EF Core
Batching به صورت پیش فرض در EF Core بدون نیاز به هیچگونه تنظیم اضافهتری فعال است. اما اگر خواستید برای مثال، حالت پیش فرض EF 6.x را توسط آن شبیه سازی کنید، میتوانید مقدار MaxBatchSize را به عدد 1 تنظیم نمائید (تا غیرفعال شود):
optionsBuilder.UseSqlServer( @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;", options => options.MaxBatchSize(1) );
مقدار پیش فرض MaxBatchSize را در کلاس SqlServerModificationCommandBatch میتوانید مشاهده کنید:
public class SqlServerModificationCommandBatch : AffectedCountModificationCommandBatch { private const int DefaultNetworkPacketSizeBytes = 4096; private const int MaxScriptLength = 65536 * DefaultNetworkPacketSizeBytes / 2; private const int MaxParameterCount = 2100; private const int MaxRowCount = 1000;
آیا محدودیتی هم در مورد عملیات Batching وجود دارد؟
SQL Server به ازای هر batch تنها 2100 پارامتر را پشتیبانی میکند. در این حالت EF Core به صورت خودکار یک چنین کوئریهای حجیمی را به چند Batch جهت تنظیم این محدودیت تقسیم خواهد کرد و در نهایت برنامه به مشکلی بر نمیخورد.
یک آزمایش: Batching پیش فرض به چه صورتی کار میکند و چه اثری را دارد؟
کدهای کامل این آزمایش را از اینجا میتوانید دریافت کنید: Batching.zip
در اینجا کلاس Blog را به همراه Context متناظر با آن مشاهده میکنید:
public class Blog { public int BlogId { get; set; } public string Name { get; set; } public string Url { get; set; } } public class BloggingContext : DbContext { public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;"/*, options => options.MaxBatchSize(2)*/ ); optionsBuilder.EnableSensitiveDataLogging(); } }
در این حالت اگر به روز رسانیها (2 مورد) و ثبتهای ذیل (6 مورد) را انجام دهیم:
using (var db = new BloggingContext()) { db.GetService<ILoggerFactory>().AddProvider(new MyLoggerProvider()); // Modify some existing blogs var existing = db.Blogs.ToArray(); existing[0].Url = "http://sample.com/blogs/dogs"; existing[1].Url = "http://sample.com/blogs/cats"; // Insert some new blogs db.Blogs.Add(new Blog { Name = "The Horse Blog", Url = "http://sample.com/blogs/horses" }); db.Blogs.Add(new Blog { Name = "The Snake Blog", Url = "http://sample.com/blogs/snakes" }); db.Blogs.Add(new Blog { Name = "The Fish Blog", Url = "http://sample.com/blogs/fish" }); db.Blogs.Add(new Blog { Name = "The Koala Blog", Url = "http://sample.com/blogs/koalas" }); db.Blogs.Add(new Blog { Name = "The Parrot Blog", Url = "http://sample.com/blogs/parrots" }); db.Blogs.Add(new Blog { Name = "The Kangaroo Blog", Url = "http://sample.com/blogs/kangaroos" }); db.SaveChanges(); }
Executed DbCommand (41ms) [Parameters=[@p1='57', @p0='http://sample.com/blogs/dogs' (Size = 4000), @p3='58', @p2='http://sample.com/blogs/cats' (Size = 4000), @p4='The Horse Blog' (Size = 4000), @p5='http://sample.com/blogs/horses' (Size = 4000), @p6='The Snake Blog' (Size = 4000), @p7='http://sample.com/blogs/snakes' (Size = 4000), @p8='The Fish Blog' (Size = 4000), @p9='http://sample.com/blogs/fish' (Size = 4000), @p10='The Koala Blog' (Size = 4000), @p11='http://sample.com/blogs/koalas' (Size = 4000), @p12='The Parrot Blog' (Size = 4000), @p13='http://sample.com/blogs/parrots' (Size = 4000), @p14='The Kangaroo Blog' (Size = 4000), @p15='http://sample.com/blogs/kangaroos' (Size = 4000)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Blogs] SET [Url] = @p0 WHERE [BlogId] = @p1; SELECT @@ROWCOUNT; UPDATE [Blogs] SET [Url] = @p2 WHERE [BlogId] = @p3; SELECT @@ROWCOUNT; DECLARE @inserted2 TABLE ([BlogId] int, [_Position] [int]); MERGE [Blogs] USING ( VALUES (@p4, @p5, 0), (@p6, @p7, 1), (@p8, @p9, 2), (@p10, @p11, 3), (@p12, @p13, 4), (@p14, @p15, 5)) AS i ([Name], [Url], _Position) ON 1=0 WHEN NOT MATCHED THEN INSERT ([Name], [Url]) VALUES (i.[Name], i.[Url]) OUTPUT INSERTED.[BlogId], i._Position INTO @inserted2; SELECT [t].[BlogId] FROM [Blogs] t INNER JOIN @inserted2 i ON ([t].[BlogId] = [i].[BlogId]) ORDER BY [i].[_Position];
- فقط یکبار Executed DbCommand مشاهده میشود.
- کل دستورات update و insert در طی یک درخواست و یک تراکنش به سمت بانک اطلاعاتی ارسال شدهاند.
- ثبت دستهای توسط merge using انجام شدهاست.
- در آخر نیز طبق معمول کار EF، شماره Idهای رکوردهای ثبت شده به سمت کلاینت بازگشت داده میشود.
در ادامه MaxBatchSize را به عدد 2 تنظیم میکنیم:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;", options => options.MaxBatchSize(2) ); optionsBuilder.EnableSensitiveDataLogging(); }
Executed DbCommand (17ms) [Parameters=[@p1='65', @p0='http://sample.com/blogs/dogs' (Size = 4000), @p3='66', @p2='http://sample.com/blogs/cats' (Size = 4000)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Blogs] SET [Url] = @p0 WHERE [BlogId] = @p1; SELECT @@ROWCOUNT; UPDATE [Blogs] SET [Url] = @p2 WHERE [BlogId] = @p3; SELECT @@ROWCOUNT; Executed DbCommand (18ms) [Parameters=[@p0='The Horse Blog' (Size = 4000), @p1='http://sample.com/blogs/horses' (Size = 4000), @p2='The Snake Blog' (Size = 4000), @p3='http://sample.com/blogs/snakes' (Size = 4000)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]); MERGE [Blogs] USING ( VALUES (@p0, @p1, 0), (@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0 WHEN NOT MATCHED THEN INSERT ([Name], [Url]) VALUES (i.[Name], i.[Url]) OUTPUT INSERTED.[BlogId], i._Position INTO @inserted0; SELECT [t].[BlogId] FROM [Blogs] t INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId]) ORDER BY [i].[_Position]; Executed DbCommand (34ms) [Parameters=[@p0='The Fish Blog' (Size = 4000), @p1='http://sample.com/blogs/fish' (Size = 4000), @p2='The Koala Blog' (Size = 4000), @p3='http://sample.com/blogs/koalas' (Size = 4000)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]); MERGE [Blogs] USING ( VALUES (@p0, @p1, 0), (@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0 WHEN NOT MATCHED THEN INSERT ([Name], [Url]) VALUES (i.[Name], i.[Url]) OUTPUT INSERTED.[BlogId], i._Position INTO @inserted0; SELECT [t].[BlogId] FROM [Blogs] t INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId]) ORDER BY [i].[_Position]; Executed DbCommand (15ms) [Parameters=[@p0='The Parrot Blog' (Size = 4000), @p1='http://sample.com/blogs/parrots' (Size = 4000), @p2='The Kangaroo Blog' (Size = 4000), @p3='http://sample.com/blogs/kangaroos' (Size = 4000)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]); MERGE [Blogs] USING ( VALUES (@p0, @p1, 0), (@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0 WHEN NOT MATCHED THEN INSERT ([Name], [Url]) VALUES (i.[Name], i.[Url]) OUTPUT INSERTED.[BlogId], i._Position INTO @inserted0; SELECT [t].[BlogId] FROM [Blogs] t INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId]) ORDER BY [i].[_Position];
- اینبار تعداد 4 دستور Executed DbCommand مشاهده میشود ( برای انجام 2 به روز رسانی و 6 ثبت).
- هر batch بر اساس تنظیم MaxBatchSize به 2 دستور T-SQL محدود شدهاست که البته در انتها در حالتهای insert، یک select هم برای بازگشت Idها به سمت کلاینت وجود دارد.
بنابراین اینبار بجای یکبار رفت و برگشت حالت قبل (استفاده از مقدار پیش فرض 1000 برای MaxBatchSize)، 4 بار رفت و برگشت به سمت بانک اطلاعاتی صورت گرفتهاست.
زمان کل انجام عملیات در حالت اول 41 میلی ثانیه و در حالت دوم 84 میلی ثانیه است که سرعت آن 51 درصد نسبت به حالت اول کاهش یافتهاست.
اشتراکها
سری توسعه بازی Blazor Wolfenstein
اشتراکها
پیاده سازی CQRS در .NET
اشتراکها
افراد نابینا چگونه کدنویسی میکنند؟
اشتراکها
بررسی Performance در ASP.NET 5
زمانیکه یک متد async، یک Task یا Task of T (نسخهی جنریک Task) را باز میگرداند، کامپایلر سیشارپ به صورت خودکار تمام استثناءهای رخ داده درون متد را دریافت کرده و از آن برای تغییر حالت Task به اصطلاحا faulted state استفاده میکند. همچنین زمانیکه از واژهی کلیدی await استفاده میشود، کدهایی که توسط کامپایلر تولید میشوند، عملا مباحث Continue موجود در TPL یا Task parallel library معرفی شده در دات نت 4 را پیاده سازی میکنند و نهایتا نتیجهی Task را در صورت وجود، دریافت میکند. زمانیکه نتیجهی یک Task مورد استفاده قرار میگیرد، اگر استثنایی وجود داشته باشد، مجددا صادر خواهد شد. برای مثال اگر خروجی یک متد async از نوع Task of T باشد، امکان استفاده از خاصیتی به نام Result نیز برای دسترسی به نتیجهی آن وجود دارد:
در این مثال یکی از روشهای استفاده از متدهای async را در یک برنامهی کنسول مشاهده میکنید. هر چند خروجی متد doSomethingAsync از نوع Task of int است، اما مستقیما یک int بازگشت داده شده است. تبدیلات نهایی در اینجا توسط کامپایلر انجام میشود. همچنین نحوهی استفاده از خاصیت Result را نیز در متد Main مشاهده میکنید.
البته باید دقت داشت، زمانیکه از خاصیت Result استفاده میشود، این متد همزمان عمل خواهد کرد و نه غیرهمزمان (ترد جاری را بلاک میکند؛ یکی از موارد مجاز استفاده از آن در متد Main برنامههای کنسول است). همچنین اگر در متد doSomethingAsync استثنایی رخ داده باشد، این استثناء زمان استفاده از Result، به صورت یک AggregateException مجددا صادر خواهد شد. وجود کلمهی Aggregate در اینجا به علت امکان استفادهی تجمعی و ترکیب چندین Task باهم و داشتن چندین شکست و استثنای ممکن است.
همچنین اگر از کلمهی کلیدی await بر روی یک faulted task استفاده کنیم، AggregateException صادر نمیشود. در این حالت کامپایلر AggregateException را بررسی کرده و آنرا تبدیل به یک Exception متداول و معمول کدهای دات نت میکند. به عبارتی سعی شدهاست در این حالت، رفتار کدهای async را شبیه به رفتار کدهای متداول همزمان شبیه سازی کنند.
یک مثال
در اینجا توسط متد getTitleAsync، اطلاعات یک صفحهی وب به صورت async دریافت شده و سپس عنوان آن استخراج میشود. در متد showTitlesAsync نیز از آن استفاده شده و در طی یک حلقه، چندین وب سایت مورد بررسی قرار خواهند گرفت. چون متد getTitleAsync از نوع async تعریف شدهاست، فراخوان آن نیز باید async تعریف شود تا بتوان از واژهی کلیدی await برای کار با آن استفاده کرد.
نهایتا در متد Main برنامه، وظیفهی غیرهمزمان showTitlesAsync اجرا شده و تا پایان عملیات آن صبر میشود. چون خروجی آن از نوع Task است و نه Task of T، در اینجا دیگر خاصیت Result قابل دسترسی نیست. متد Wait نیز ترد جاری را همانند خاصیت Result بلاک میکند.
کلیه عملیات مبتنی برشبکه، همیشه مستعد به بروز خطا هستند. قطعی ارتباط یا حتی کندی آن میتوانند سبب بروز استثناء شوند.
برنامه را در حالت عدم اتصال به اینترنت اجرا کنید. استثنای صادر شده، در متد task.Wait ظاهر میشود (چون متدهای async ترد جاری را خالی کردهاند):
و اگر در اینجا بر روی لینک View details کلیک کنیم، در inner exception حاصل، خطای واقعی قابل مشاهده است:
همانطور که ملاحظه میکنید، استثنای صادر شده از نوع System.AggregateException است. به این معنا که میتواند حاوی چندین استثناء باشد که در اینجا تعداد آنها با عدد یک مشخص شدهاست. بنابراین در این حالات، بررسی inner exception را فراموش نکنید.
در ادامه داخل حلقهی foreach متد showTitlesAsync، یک try/catch قرار میدهیم:
اینبار اگر برنامه را اجرا کنیم، خروجی ذیل را در صفحه میتوان مشاهده کرد:
در اینجا دیگر خبری از AggregateException نبوده و استثنای واقعی رخ داده در متد await شده بازگشت داده شدهاست. کار واژهی کلیدی await در اینجا، بررسی استثنای رخ داده در متد async فراخوانی شده و بازگشت آن به جریان متداول متد جاری است؛ تا نتیجهی عملیات همانند یک کد کامل همزمان به نظر برسد. به این ترتیب کامپایلر توانسته است رفتار بروز استثناءها را در کدهای همزمان و غیرهمزمان یک دست کند. دقیقا مانند حالتی که یک متد معمولی در این بین فراخوانی شده و استثنایی در آن رخ دادهاست.
مدیریت تمام inner exceptionهای رخ داده در پردازشهای موازی
همانطور که عنوان شد، await تنها یک استثنای حاصل از Task در حال اجرا را به کد فراخوان بازگشت میدهد. در این حالت اگر این Task، چندین شکست را گزارش دهد، چطور باید برای دریافت تمام آنها اقدام کرد؟ برای مثال استفاده از Task.WhenAll میتواند شامل چندین استثنای حاصل از چندین Task باشد، ولی await تنها اولین استثنای دریافتی را بازگشت میدهد. اما اگر از خاصیتی مانند Result یا متد Wait استفاده شود، یک AggregateException حاصل تمام استثناءها را دریافت خواهیم کرد. بنابراین هرچند await تنها اولین استثنای دریافتی را بازگشت میدهد، اما میتوان به Taskهای مرتبط مراجعه کرد و سپس بررسی نمود که آیا استثناهای دیگری نیز وجود دارند یا خیر؟
برای نمونه در مثال فوق، حلقهی foreach تشکیل شده آنچنان بهینه نیست. از این جهت که هر بار تنها یک سایت را بررسی میکند، بجای اینکه مانند مرورگرها چندین ترد را به یک یا چند سایت باز کرده و نتایج را دریافت کند.
البته انجام کارها به صورت موازی همیشه ایدهی خوبی نیست ولی حداقل در این حالت خاص که با یک یا چند سرور راه دور کار میکنیم، درخواستهای همزمان دریافت اطلاعات، سبب کارآیی بهتر برنامه و بالا رفتن سرعت اجرای آن میشوند. اما مثلا در حالتیکه با سخت دیسک سیستم کار میکنیم، اجرای موازی کارها نه تنها کمکی نخواهد کرد، بلکه سبب خواهد شد تا مدام drive head در مکانهای مختلفی مشغول به حرکت شده و در نتیجه کارآیی آن کاهش یابد.
برای ترکیب چندین Task، ویژگی خاصی به زبان سیشارپ اضافه نشده، زیرا نیازی نبوده است. برای این حالت تنها کافی است از متد Task.WhenAll، برای ساخت یک Task مرکب استفاده کرد. سپس میتوان واژهی کلیدی await را بر روی این Task مرکب فراخوانی کرد.
همچنین میتوان از متد ContinueWith یک Task مرکب نیز برای جلوگیری از بازگشت صرفا اولین استثنای رخ داده توسط کامپایلر، استفاده کرد. در این حالت امکان دسترسی به خاصیت Result آن به سادگی میسر میشود که حاوی AggregateException کاملی است.
اعتبارسنجی آرگومانهای ارسالی به یک متد async
زمان اعتبارسنجی آرگومانهای ارسالی به متدهای async مهم است. بعضی از مقادیر را نمیتوان بلافاصله اعتبارسنجی کرد؛ مانند مقادیری که نباید نال باشند. تعدادی دیگر نیز پس از انجام یک Task زمانبر مشخص میشوند که معتبر بودهاند یا خیر. همچنین فراخوانهای این متدها انتظار دارند که متدهای async بلافاصله بازگشت داده شده و ترد جاری را خالی کنند. بنابراین اعتبارسنجیهای آنها باید با تاخیر انجام شود. در این حالات، دو نوع استثنای آنی و به تاخیر افتاده را شاهد خواهیم بود. استثنای آنی زمان شروع به کار متد صادر میشود و استثنای به تاخیر افتاده در حین دریافت نتایج از آن دریافت میگردد. باید دقت داشت کلیه استثناهای صادر شده در بدنهی یک متد async، توسط کامپایلر به عنوان یک استثنای به تاخیر افتاده گزارش داده میشود. بنابراین اعتبارسنجیهای آرگومانها را بهتر است در یک متد سطح بالای غیر async انجام داد تا بلافاصله بتوان استثناءهای حاصل را دریافت نمود.
از دست دادن استثناءها
فرض کنید مانند مثال قسمت قبل، دو وظیفهی async آغاز شده و نتیجهی آنها پس از await هر یک، با هم جمع زده میشوند. در این حالت اگر کل عملیات را داخل یک قطعه کد try/catch قرار دهیم، اولین await ایی که یک استثناء را صادر کند، صرفنظر از وضعیت await دوم، سبب اجرای بدنهی catch میشود. همچنین انجام این عملیات بدین شکل بهینه نیست. زیرا ابتدا باید صبر کرد تا اولین Task تمام شود و سپس دومین Task شروع گردد و به این ترتیب پردازش موازی Taskها را از دست خواهیم داد. در یک چنین حالتی بهتر است از متد await Task.WhenAll استفاده شود. در اینجا دو Task مورد نیاز، تبدیل به یک Task مرکب میشوند. این Task مرکب تنها زمانی خاتمه مییابد که هر دوی Task اضافه شده به آن، خاتمه یافته باشند. به این ترتیب علاوه بر اجرای موازی Taskها، امکان دریافت استثناءهای هر کدام را نیز به صورت تجمعی خواهیم داشت.
مشکل! همانطور که پیشتر نیز عنوان شد، استفاده از await در اینجا سبب میشود تا کامپایلر تنها اولین استثنای دریافتی را بازگشت دهد و نه یک AggregateException نهایی را. روش حل آنرا نیز عنوان کردیم. در این حالت بهتر است از متد ContinueWith و سپس استفاده از خاصیت Result آن برای دریافت کلیه استثناءها کمک گرفت.
حالت دوم از دست دادن استثناءها زمانیاست که یک متد async void را ایجاد میکنید. در این حالات بهتر است از یک Task بجای بازگشت void استفاده شود. تنها علت وجودی async voidها، استفاده از آنها در روالهای رویدادگردان UI است (در سایر حالات code smell درنظر گرفته میشود).
در مثال فوق، نحوهی ترکیب دو Task را توسط Task.WhenAll جهت اجرای موازی و سپس اعمال نکتهی یک ContinueWith خالی و در ادامه استفاده از Result نهایی را جهت دریافت تمامی استثناءهای حاصل، مشاهده میکنید.
در این مثال دیگر مانند مثال قسمت قبل
هر بار صبر نشدهاست تا یک Task تمام شود و سپس Task بعدی شروع گردد.
با کمک متد Task.WhenAll ترکیب آنها ایجاد و سپس با فراخوانی await، سبب اجرای موازی چندین Task با هم شدهایم.
مدیریت خطاهای مدیریت نشده
ابتدا مثال زیر را در نظر بگیرید:
در این مثال دو متد که یکی async Task و دیگری async void است، تعریف شدهاند.
اگر برنامه را کامپایل کنید، کامپایلر بر روی سطر فراخوانی متد Test اخطار زیر را صادر میکند. البته برنامه بدون مشکل کامپایل خواهد شد.
اما چنین اخطاری در مورد async void صادر نمیشود. بنابراین ممکن است جایی در کدها، فراخوانی await فراموش شود. اگر خروجی متد شما ازنوع Task و مشتقات آن باشد، کامپایلر حتما اخطاری را جهت رفع آن گوشزد خواهد کرد؛ اما نه در مورد متدهای void که صرفا جهت کاربردهای UI و روالهای رخدادگردان آن طراحی شدهاند.
همچنین اگر برنامه را اجرا کنید استثنای صادر شده در متد async void سبب کرش برنامه میشود؛ اما نه استثنای صادر شده در متد async Task. متدهای async void چون دارای Synchronization Context نیستند، استثنای صادره را به Thread pool برنامه صادر میکنند. به همین جهت در همان لحظه نیز سبب کرش برنامه خواهند شد. اما در حالت async Task به این نوع استثناءها اصطلاحا Unobserved Task Exception گفته شده و سبب بروز faulted state در Task تعریف شده میگردند.
برای مدیریت آنها در سطح برنامه باید در ابتدای کار و در متد Main، توسط TaskScheduler.UnobservedTaskException روال رخدادگردانی را برای مدیریت اینگونه استثناءها تدارک دید. زمانیکه GC شروع به آزاد سازی منابع میکند، این استثناءها نیز درنظر گرفته شده و سبب کرش برنامه خواهند شد. با استفاده از متد SetObserved همانند قطعه کد زیر، میتوان از کرش برنامه جلوگیری کرد:
البته لازم به ذکر است که این رفتار در دات نت 4.5 به این شکل تغییر کرده است تا کار با متدهای async سادهتر شود. در دات نت 4، یک چنین استثناءهای مدیریت نشدهای،بلافاصله سبب بروز استثناء و کرش برنامه میشدند.
به عبارتی رفتار قطعه کد زیر در دات نت 4 و 4.5 متفاوت است:
در دات نت 4 اگر این برنامه را خارج از VS.NET اجرا کنیم، برنامه کرش میکند؛ اما در دات نت 4.5 خیر و آنها به UnobservedTaskException یاد شده هدایت خواهند شد. اگر میخواهید این رفتار را به همان حالت دات نت 4 تغییر دهید، تنظیم زیر را به فایل config برنامه اضافه کنید:
یک نکتهی تکمیلی: ممکن است عبارات lambda مورد استفاده، از نوع async void باشد.
همانطور که عنوان شد باید از async void منهای مواردی که کار مدیریت رویدادهای عناصر UI را انجام میدهند (مانند برنامههای ویندوز 8)، اجتناب کرد. چون پایان کار آنها را نمیتوان تشخیص داد و همچنین کامپایلر نیز اخطاری را در مورد استفاده ناصحیح از آنها بدون await تولید نمیکند (چون نوع void اصطلاحا awaitable نیست). به علاوه بروز استثناء در آنها، بلافاصله سبب خاتمه برنامه میشود. بنابراین اگر جایی در برنامه متد async void وجود دارد، قرار دادن try/catch داخل بدنهی آن ضروری است.
در این مثال خاص ویندوز 8، شاید به نظر برسد که try/catch تعریف شده سبب مهار استثنای صادر شده میشود؛ اما خیر!
امضای متد TappedEventHandler از نوع delegate void است. بنابراین try/catch را باید داخل بدنهی روال رویدادگردان تعریف شده قرار داد و نه خارج از آن.
using System.Threading.Tasks; namespace Async05 { class Program { static void Main(string[] args) { var res = doSomethingAsync().Result; } static async Task<int> doSomethingAsync() { await Task.Delay(1); return 1; } } }
البته باید دقت داشت، زمانیکه از خاصیت Result استفاده میشود، این متد همزمان عمل خواهد کرد و نه غیرهمزمان (ترد جاری را بلاک میکند؛ یکی از موارد مجاز استفاده از آن در متد Main برنامههای کنسول است). همچنین اگر در متد doSomethingAsync استثنایی رخ داده باشد، این استثناء زمان استفاده از Result، به صورت یک AggregateException مجددا صادر خواهد شد. وجود کلمهی Aggregate در اینجا به علت امکان استفادهی تجمعی و ترکیب چندین Task باهم و داشتن چندین شکست و استثنای ممکن است.
همچنین اگر از کلمهی کلیدی await بر روی یک faulted task استفاده کنیم، AggregateException صادر نمیشود. در این حالت کامپایلر AggregateException را بررسی کرده و آنرا تبدیل به یک Exception متداول و معمول کدهای دات نت میکند. به عبارتی سعی شدهاست در این حالت، رفتار کدهای async را شبیه به رفتار کدهای متداول همزمان شبیه سازی کنند.
یک مثال
در اینجا توسط متد getTitleAsync، اطلاعات یک صفحهی وب به صورت async دریافت شده و سپس عنوان آن استخراج میشود. در متد showTitlesAsync نیز از آن استفاده شده و در طی یک حلقه، چندین وب سایت مورد بررسی قرار خواهند گرفت. چون متد getTitleAsync از نوع async تعریف شدهاست، فراخوان آن نیز باید async تعریف شود تا بتوان از واژهی کلیدی await برای کار با آن استفاده کرد.
نهایتا در متد Main برنامه، وظیفهی غیرهمزمان showTitlesAsync اجرا شده و تا پایان عملیات آن صبر میشود. چون خروجی آن از نوع Task است و نه Task of T، در اینجا دیگر خاصیت Result قابل دسترسی نیست. متد Wait نیز ترد جاری را همانند خاصیت Result بلاک میکند.
using System; using System.Collections.Generic; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Async05 { class Program { static void Main(string[] args) { var task = showTitlesAsync(new[] { "http://www.google.com", "https://www.dntips.ir" }); task.Wait(); Console.WriteLine(); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } static async Task showTitlesAsync(IEnumerable<string> urls) { foreach (var url in urls) { var title = await getTitleAsync(url); Console.WriteLine(title); } } static async Task<string> getTitleAsync(string url) { var data = await new WebClient().DownloadStringTaskAsync(url); return getTitle(data); } private static string getTitle(string data) { const string patternTitle = @"(?s)<title>(.+?)</title>"; var regex = new Regex(patternTitle); var mc = regex.Match(data); return mc.Groups.Count == 2 ? mc.Groups[1].Value.Trim() : string.Empty; } } }
برنامه را در حالت عدم اتصال به اینترنت اجرا کنید. استثنای صادر شده، در متد task.Wait ظاهر میشود (چون متدهای async ترد جاری را خالی کردهاند):
و اگر در اینجا بر روی لینک View details کلیک کنیم، در inner exception حاصل، خطای واقعی قابل مشاهده است:
همانطور که ملاحظه میکنید، استثنای صادر شده از نوع System.AggregateException است. به این معنا که میتواند حاوی چندین استثناء باشد که در اینجا تعداد آنها با عدد یک مشخص شدهاست. بنابراین در این حالات، بررسی inner exception را فراموش نکنید.
در ادامه داخل حلقهی foreach متد showTitlesAsync، یک try/catch قرار میدهیم:
static async Task showTitlesAsync(IEnumerable<string> urls) { foreach (var url in urls) { try { var title = await getTitleAsync(url); Console.WriteLine(title); } catch (Exception ex) { Console.WriteLine(ex); } } }
System.Net.WebException: The remote server returned an error: (502) Bad Gateway. System.Net.WebException: The remote server returned an error: (502) Bad Gateway. Press any key to exit...
مدیریت تمام inner exceptionهای رخ داده در پردازشهای موازی
همانطور که عنوان شد، await تنها یک استثنای حاصل از Task در حال اجرا را به کد فراخوان بازگشت میدهد. در این حالت اگر این Task، چندین شکست را گزارش دهد، چطور باید برای دریافت تمام آنها اقدام کرد؟ برای مثال استفاده از Task.WhenAll میتواند شامل چندین استثنای حاصل از چندین Task باشد، ولی await تنها اولین استثنای دریافتی را بازگشت میدهد. اما اگر از خاصیتی مانند Result یا متد Wait استفاده شود، یک AggregateException حاصل تمام استثناءها را دریافت خواهیم کرد. بنابراین هرچند await تنها اولین استثنای دریافتی را بازگشت میدهد، اما میتوان به Taskهای مرتبط مراجعه کرد و سپس بررسی نمود که آیا استثناهای دیگری نیز وجود دارند یا خیر؟
برای نمونه در مثال فوق، حلقهی foreach تشکیل شده آنچنان بهینه نیست. از این جهت که هر بار تنها یک سایت را بررسی میکند، بجای اینکه مانند مرورگرها چندین ترد را به یک یا چند سایت باز کرده و نتایج را دریافت کند.
البته انجام کارها به صورت موازی همیشه ایدهی خوبی نیست ولی حداقل در این حالت خاص که با یک یا چند سرور راه دور کار میکنیم، درخواستهای همزمان دریافت اطلاعات، سبب کارآیی بهتر برنامه و بالا رفتن سرعت اجرای آن میشوند. اما مثلا در حالتیکه با سخت دیسک سیستم کار میکنیم، اجرای موازی کارها نه تنها کمکی نخواهد کرد، بلکه سبب خواهد شد تا مدام drive head در مکانهای مختلفی مشغول به حرکت شده و در نتیجه کارآیی آن کاهش یابد.
برای ترکیب چندین Task، ویژگی خاصی به زبان سیشارپ اضافه نشده، زیرا نیازی نبوده است. برای این حالت تنها کافی است از متد Task.WhenAll، برای ساخت یک Task مرکب استفاده کرد. سپس میتوان واژهی کلیدی await را بر روی این Task مرکب فراخوانی کرد.
همچنین میتوان از متد ContinueWith یک Task مرکب نیز برای جلوگیری از بازگشت صرفا اولین استثنای رخ داده توسط کامپایلر، استفاده کرد. در این حالت امکان دسترسی به خاصیت Result آن به سادگی میسر میشود که حاوی AggregateException کاملی است.
اعتبارسنجی آرگومانهای ارسالی به یک متد async
زمان اعتبارسنجی آرگومانهای ارسالی به متدهای async مهم است. بعضی از مقادیر را نمیتوان بلافاصله اعتبارسنجی کرد؛ مانند مقادیری که نباید نال باشند. تعدادی دیگر نیز پس از انجام یک Task زمانبر مشخص میشوند که معتبر بودهاند یا خیر. همچنین فراخوانهای این متدها انتظار دارند که متدهای async بلافاصله بازگشت داده شده و ترد جاری را خالی کنند. بنابراین اعتبارسنجیهای آنها باید با تاخیر انجام شود. در این حالات، دو نوع استثنای آنی و به تاخیر افتاده را شاهد خواهیم بود. استثنای آنی زمان شروع به کار متد صادر میشود و استثنای به تاخیر افتاده در حین دریافت نتایج از آن دریافت میگردد. باید دقت داشت کلیه استثناهای صادر شده در بدنهی یک متد async، توسط کامپایلر به عنوان یک استثنای به تاخیر افتاده گزارش داده میشود. بنابراین اعتبارسنجیهای آرگومانها را بهتر است در یک متد سطح بالای غیر async انجام داد تا بلافاصله بتوان استثناءهای حاصل را دریافت نمود.
از دست دادن استثناءها
فرض کنید مانند مثال قسمت قبل، دو وظیفهی async آغاز شده و نتیجهی آنها پس از await هر یک، با هم جمع زده میشوند. در این حالت اگر کل عملیات را داخل یک قطعه کد try/catch قرار دهیم، اولین await ایی که یک استثناء را صادر کند، صرفنظر از وضعیت await دوم، سبب اجرای بدنهی catch میشود. همچنین انجام این عملیات بدین شکل بهینه نیست. زیرا ابتدا باید صبر کرد تا اولین Task تمام شود و سپس دومین Task شروع گردد و به این ترتیب پردازش موازی Taskها را از دست خواهیم داد. در یک چنین حالتی بهتر است از متد await Task.WhenAll استفاده شود. در اینجا دو Task مورد نیاز، تبدیل به یک Task مرکب میشوند. این Task مرکب تنها زمانی خاتمه مییابد که هر دوی Task اضافه شده به آن، خاتمه یافته باشند. به این ترتیب علاوه بر اجرای موازی Taskها، امکان دریافت استثناءهای هر کدام را نیز به صورت تجمعی خواهیم داشت.
مشکل! همانطور که پیشتر نیز عنوان شد، استفاده از await در اینجا سبب میشود تا کامپایلر تنها اولین استثنای دریافتی را بازگشت دهد و نه یک AggregateException نهایی را. روش حل آنرا نیز عنوان کردیم. در این حالت بهتر است از متد ContinueWith و سپس استفاده از خاصیت Result آن برای دریافت کلیه استثناءها کمک گرفت.
حالت دوم از دست دادن استثناءها زمانیاست که یک متد async void را ایجاد میکنید. در این حالات بهتر است از یک Task بجای بازگشت void استفاده شود. تنها علت وجودی async voidها، استفاده از آنها در روالهای رویدادگردان UI است (در سایر حالات code smell درنظر گرفته میشود).
public async Task<double> GetSum2Async() { try { var task1 = GetNumberAsync(); var task2 = GetNumberAsync(); var compositeTask = Task.WhenAll(task1, task2); await compositeTask.ContinueWith(x => { }); return compositeTask.Result[0] + compositeTask.Result[1]; } catch (Exception ex) { //todo: log ex throw; } }
در این مثال دیگر مانند مثال قسمت قبل
public async Task<double> GetSumAsync() { var leftOperand = await GetNumberAsync(); var rightOperand = await GetNumberAsync(); return leftOperand + rightOperand; }
با کمک متد Task.WhenAll ترکیب آنها ایجاد و سپس با فراخوانی await، سبب اجرای موازی چندین Task با هم شدهایم.
مدیریت خطاهای مدیریت نشده
ابتدا مثال زیر را در نظر بگیرید:
using System; using System.Threading.Tasks; namespace Async01 { class Program { static void Main(string[] args) { Test2(); Test(); Console.ReadLine(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.ReadLine(); } public static async Task Test() { throw new Exception(); } public static async void Test2() { throw new Exception(); } } }
اگر برنامه را کامپایل کنید، کامپایلر بر روی سطر فراخوانی متد Test اخطار زیر را صادر میکند. البته برنامه بدون مشکل کامپایل خواهد شد.
Warning 1 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
همچنین اگر برنامه را اجرا کنید استثنای صادر شده در متد async void سبب کرش برنامه میشود؛ اما نه استثنای صادر شده در متد async Task. متدهای async void چون دارای Synchronization Context نیستند، استثنای صادره را به Thread pool برنامه صادر میکنند. به همین جهت در همان لحظه نیز سبب کرش برنامه خواهند شد. اما در حالت async Task به این نوع استثناءها اصطلاحا Unobserved Task Exception گفته شده و سبب بروز faulted state در Task تعریف شده میگردند.
برای مدیریت آنها در سطح برنامه باید در ابتدای کار و در متد Main، توسط TaskScheduler.UnobservedTaskException روال رخدادگردانی را برای مدیریت اینگونه استثناءها تدارک دید. زمانیکه GC شروع به آزاد سازی منابع میکند، این استثناءها نیز درنظر گرفته شده و سبب کرش برنامه خواهند شد. با استفاده از متد SetObserved همانند قطعه کد زیر، میتوان از کرش برنامه جلوگیری کرد:
using System; using System.Threading.Tasks; namespace Async01 { class Program { static void Main(string[] args) { TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; //Test2(); Test(); Console.ReadLine(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.ReadLine(); } private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { e.SetObserved(); Console.WriteLine(e.Exception); } public static async Task Test() { throw new Exception(); } public static async void Test2() { throw new Exception(); } } }
به عبارتی رفتار قطعه کد زیر در دات نت 4 و 4.5 متفاوت است:
Task.Factory.StartNew(() => { throw new Exception(); }); Thread.Sleep(100); GC.Collect(); GC.WaitForPendingFinalizers();
<configuration> <runtime> <ThrowUnobservedTaskExceptions enabled="true"/> </runtime> </configuration>
یک نکتهی تکمیلی: ممکن است عبارات lambda مورد استفاده، از نوع async void باشد.
همانطور که عنوان شد باید از async void منهای مواردی که کار مدیریت رویدادهای عناصر UI را انجام میدهند (مانند برنامههای ویندوز 8)، اجتناب کرد. چون پایان کار آنها را نمیتوان تشخیص داد و همچنین کامپایلر نیز اخطاری را در مورد استفاده ناصحیح از آنها بدون await تولید نمیکند (چون نوع void اصطلاحا awaitable نیست). به علاوه بروز استثناء در آنها، بلافاصله سبب خاتمه برنامه میشود. بنابراین اگر جایی در برنامه متد async void وجود دارد، قرار دادن try/catch داخل بدنهی آن ضروری است.
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState) { try { ClickMeButton.Tapped += async (sender, args) => { throw new Exception(); }; } catch (Exception ex) { // This won’t catch exceptions! TextBlock1.Text = ex.Message; } }
public delegate void TappedEventHandler(object sender, TappedRoutedEventArgs e);
اشتراکها