Asenkron Ve Paralel Programlamada Lock Mekanizması
Selamlar Arkadaşlar,
Bu makalede Mvc bir sayfaya ait, farklı tipte modelleri asenkron ve paralel çekerken (Lock) mekanizmasının nasıl yapılacağını ve yanlış kullanımda ne gibi sonuçlar doğrucağını hep beraber inceleyeceğiz. Aynı zamanda Try{} Catch{} mekanizmasının MultiTasking bir yapıda nasıl yönetilceğini, yine örnekler ile inceleyeceğiz.
Öncelikle “Lock” işlemi örneğin bir cache düştüğü zaman birden fazla kişinin, cache’in düşmesi anında DB’ye, ya da ne işlem yapılmak isteniyor ise ona sadece 1 kişi tarafından ulaşılmasını sağlıyan bir güvenlik mekanizmasıdır. Aslında static object’den başka birşey değildir. Ama işlevi açısından hayati bir önem taşır. Yüksek trafikli bir sitede “lock“‘ın düzgün kullanılmaması durumunda, belirlenen süredeki cache düştüğü zaman, aynı anda DB’ye 1 milyon kişi gidebilir. Bu da tüm sistemin down olmasına sebebiyet verir.
Yukarıda görülen yönteme Double Check Lock denir. İlgili nesnenin “null” olup olmadı kontrol edilip, locklanır. Ve tekrardan kontrol edilip ilgili işlemler yapılır. Yani ilgili nesne 2 kere kontrol edilmiş olunur.
Durum 1:
Şimdi de biraz da Task Async yapısından bahsedelim. Bunla ilgili detaylı bilgili önceki makalemden erişebilirsiniz.Aşağıdaki örnekde 3 farklı durum “DoMultipleAsync()” methodu içerisinde çalıştırılmıştır. Amaç 3 farklı işi asenkron ve paralel olarak çalıştırmak ve olabilecek en kısa sürede bitirmektir. Aşağıdaki örnek, bir console uygulamasıdır. Aslında birtane “ExcAsync()” adında asenkron Task bir method vardır. İlgili method içinde 1 den 10’a kadar asenkron olarak console’a gönderilen string değer, yanında sıra numarası ile yazdırılmaktadır. İlgili method 3 kere çağrılmıştır. Yani 3 farklı Task’e atanmıştır. Bu 3 farklı işlemin asenkron olarak çalıştığının anlaşılması için “Task.Delay(100)” methodu ile “0.1 sn” bekletilmiştir. Böylece bekleme anında farklı taskların nasıl çağrıldığı incelene bilecektir.
Try {} Catch{} işleminde tüm Tasklar göz önüne alınmıştır. Yani hatalar tüm Task işlemleri “await allTasks“‘ın ardından toplu olarak bakılmaktadır.Tüm işlemler bittikten sonra “WhenAll” ile atandıkları “allTasks”‘ın içinde gezilerek tüm tasklara ait hatalar ekrana yazdırılır. Aşağıdaki örnekde “theTask1″ yani gönderilen info içinde,”First” kelimesi geçen task için özellikle Exeption fırlatılmıştır. Amaç tüm işlem bittikten sonra ilgili hata mesajını görebilmektir. Yani 3 Taskden 1’inde oluşan hata diğerlerini etkilememekte ve diğer 2 Task işlemlerini bitirebilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace TaskAsync { class Program { private static readonly AsyncLock m_lock = new AsyncLock(); private static readonly AsyncLock m_lock2 = new AsyncLock(); private static readonly AsyncLock m_lock3 = new AsyncLock(); static void Main(string[] args) { DoMultipleAsync().Wait(); Console.ReadLine(); } public static async Task DoMultipleAsync() { Task theTask1 = ExcAsync(lockObject:m_lock, info: "First Task"); Task theTask2 = ExcAsync(lockObject:m_lock2, info: "Second Task"); Task theTask3 = ExcAsync(lockObject: m_lock3,info: "Third Task"); Task allTasks = Task.WhenAll(theTask1, theTask2, theTask3); try { await allTasks; } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); Console.WriteLine("Task IsFaulted: " + allTasks.IsFaulted); foreach (var inEx in allTasks.Exception.InnerExceptions) { Console.WriteLine("Task Inner Exception: " + inEx.Message); } } } private static async Task ExcAsync(AsyncLock lockObject, string info) { using (await lockObject.LockAsync()) { for (var i = 0; i < 10; i++) { if (info.Contains("First")) //if (info.Contains("Second")) throw new Exception("Error-" + info); await Task.Delay(100); Console.WriteLine(info + " " + i); } } } } } |
Yukarıdaki çıktıya dikkat ederseniz, her bir “0.1” sn bekleme anında farklı bir task çağrılmış ve paralel işleyiş’e güzel bir örnek verilmiştir.
Async Lock Object : Yukarıda görülen asenkron methodları normal static object bir Lock nesnesi ile Locklanamaz. Bunun için aşağıda görülen “SemaphoreSlim” sınıfının kullanıldığı, AsyncLock objesi üreten bir sınıf kullanılır. İçinde tanımlanan “LockAsync()” methodu ile her bir Task’a özel yeni bir Lock nesnesi önceden oluşturulur ve “ExcAsync()” methodunda her bir task’a özel olarak gönderilir. Dikkat ederseniz her bir task’a ait farklı lock objeleri kullanılmıştır. Eğer hepsi için tek bir “Lock” objesi kullanılır ise, paralel bir işleyişden söz etmek mümkün olmaz. Ve tüm taskler birbirlerini beklemek zorunda kalırlar.
Yukarıdaki örnek çıktıda da görüldüğü gibi, her bir for döngüsü içinde “delay(100)” konulduğu için her bekleme anında farklı bir task çalıştırılmıştır. Bu nedenle farklı taskler random bir sıra içinde sıralanarak ekrana yazdırılmaktadır. En sonda da hata fırlatılan “First Task” console yazdırılmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public sealed class AsyncLock { private readonly SemaphoreSlim m_semaphore = new SemaphoreSlim(1, 1); private readonly Task<IDisposable> m_releaser; public AsyncLock() { m_releaser = Task.FromResult((IDisposable)new Releaser(this)); } public Task<IDisposable> LockAsync() { var wait = m_semaphore.WaitAsync(); return wait.IsCompleted ? m_releaser : wait.ContinueWith((_, state) => (IDisposable)state, m_releaser.Result, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } private sealed class Releaser : IDisposable { private readonly AsyncLock m_toRelease; internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; } public void Dispose() { m_toRelease.m_semaphore.Release(); } } } |
Durum 2: Aşağıdaki örnekde yeni bir Task(Task22) daha eklenmiştir. Ama burada farklı olarak Task2’de kullanılan ==> “Second” Lock object’i [m_lock2] “Task22″‘de ==> “Second2” kullanılan Lock object [m_lock2]’in aynısıdır. Bu demek oluyor ki “Task2”, “Task22″‘nin bitmesini beklemektedir. Bu durumda paralel ve asenkron bir çalışma “Task2” için söylenememektedir. Aşağıdaki çıktıdan da anlaşılacağı gibi “Second2” taskinin bitmesi beklenmiş ve “Second” Taskinin en sonunda senkron olarak çalıştırılması sağlanmıştır..
Bu durumda Lock objelerine dikkat edilmeli ve her bir task için ayrı bir lock object’i üretilmelidir. Aksi takdirde asenkron ve paralel bir çalışma yapılması sözkonusu değildir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class Program { private static readonly AsyncLock m_lock = new AsyncLock(); private static readonly AsyncLock m_lock2 = new AsyncLock(); private static readonly AsyncLock m_lock4 = new AsyncLock(); static void Main2(string[] args) { DoMultipleAsync().Wait(); Console.ReadLine(); } public static async Task DoMultipleAsync() { Task theTask1 = ExcAsync(lockObject: m_lock, info: "First Task"); Task theTask22 = ExcAsync(lockObject: m_lock2, info: "Second2 Task"); Task theTask2 = ExcAsync(lockObject: m_lock2, info: "Second Task"); Task theTask3 = ExcAsync(lockObject: m_lock4, info: "Third Task"); var sw = Stopwatch.StartNew(); Task allTasks = Task.WhenAll(theTask1, theTask2, theTask22, theTask3); try { await allTasks; } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); Console.WriteLine("Task IsFaulted: " + allTasks.IsFaulted); foreach (var inEx in allTasks.Exception.InnerExceptions) { Console.WriteLine("Task Inner Exception: " + inEx.Message); } } Console.WriteLine($"Total Tİme:{sw.Elapsed.TotalSeconds}"); } private static async Task ExcAsync(AsyncLock lockObject, string info) { using (await lockObject.LockAsync()) { for (var i = 0; i < 10; i++) { //if (info.Contains("First")) if (info.Contains("Third")) throw new Exception("Error-" + info); await Task.Delay(100); Console.WriteLine(info + " " + i); } } } } |
Durum 3: Peki diyelim ki yapılcak işlem sürekli değişen dinamik bir yapı olsun. Örneğin bir haberin detay sayfasına gidilecek. Bu durumda haber detay sayfası tek de olsa, okunacak haber dinamiktir. Yani her zaman değişebilir. Eğer hepsi için aynı “Lock” object kullanılır ise, bir haber cache den düştüğü zaman, ilgili data DB’den çekilmek istendiğinde, diğer tüm haberler onu beklemek zorunda kalacaktır. Bu da inanılmaz bir performance kaybına neden olacaktır. İşte tam bu sorunda ilk akla gelen her bir detay için ayrı Lock object’i üretmektir. Aşağıdaki son revizyonda yeni bir “Dictionary” nesnesi üretilmiş, ve oluşturulan her yeni Lock object’i bu dictionar içine konmuştur. Bir daha yeni bir habere gelindiğinde öncelikle, ilgili lock object bu listede aranmış ve yok ise yeni yaratılıp bu listeye konmuştur. Her bir lock object’e karşılık gelen haber detayın key’i tabii ki ilgili haberID’si olan “contentParentID”‘dir.
Ayrıca ilgili dictionary nesnesinin zamanla şişmemesi için, çarşamba günleri gece 1’den sonra temizlenmektedir. Böylece her bir haber detay için yani, her bir dinamic durum için yeni bir lock object yaratılmış ve ilgili dictionary’e atılmıştır. Böylece haber detaylardan herhangi biri, cacheden düştüğü zaman sadece ilgili haber kitlenmekte ve diğer haberlerin DB’den çekilmesi engellenmemektedir.
Aşağıdaki örneğe dikkat edilir ise şeytan ayrıntılarda gizlidir:) Aynı zamanda ilgili Dictionary nesnesine de olmayan bir Lock nesnesi yine “lockDic” adında yeni bir static lock object’i ile kitlenip atılmaktadır.
Program.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace TaskAsync { class Program { //List Lock Object And Method private static string lastClearDay; private static readonly Dictionary<String, AsyncLock> LockList = new Dictionary<string, AsyncLock>(); private static readonly object lockDic = new AsyncLock(); private static AsyncLock LockOnValue(String contentParentID) { AsyncLock lockObj = null; lock (lockDic) { if (!LockList.TryGetValue(contentParentID, out lockObj)) { //Çarşambaları Gece 1'de Tüm Keyler Temizlenir. if(lastClearDay!=DateTime.Today.ToShortDateString() && (int)DateTime.Now.DayOfWeek==2 && DateTime.Now.Hour==1) { lastClearDay = DateTime.Today.ToShortDateString(); LockList.Clear(); } lockObj = new AsyncLock(); LockList.Add(contentParentID, lockObj); } //return LockList[contentParentID]; return lockObj; } } //----------------Finish--------------------------------------- static void Main(string[] args) { DoMultipleAsync().Wait(); Console.ReadLine(); } public static async Task DoMultipleAsync() { Task theTask1 = ExcAsync(lockObject: LockOnValue("m_lock"), info: "First Task"); Task theTask22 = ExcAsync(lockObject: LockOnValue("m_lock2"), info: "Second2 Task"); Task theTask2 = ExcAsync(lockObject: LockOnValue("m_lock2"), info: "Second Task"); Task theTask3 = ExcAsync(lockObject: LockOnValue("m_lock3"), info: "Third Task"); Task allTasks = Task.WhenAll(theTask1, theTask2, theTask22, theTask3); try { await allTasks; } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); Console.WriteLine("Task IsFaulted: " + allTasks.IsFaulted); foreach (var inEx in allTasks.Exception.InnerExceptions) { Console.WriteLine("Task Inner Exception: " + inEx.Message); } } finally { int i = 0; } } private static async Task ExcAsync(AsyncLock lockObject, string info) { using (await lockObject.LockAsync()) { for (var i = 0; i < 10; i++) { if (info.Contains("First")) //if (info.Contains("Second")) throw new Exception("Error-" + info); await Task.Delay(100); Console.WriteLine(info + " " + i); } } } } public sealed class AsyncLock { private readonly SemaphoreSlim m_semaphore = new SemaphoreSlim(1, 1); private readonly Task<IDisposable> m_releaser; public AsyncLock() { m_releaser = Task.FromResult((IDisposable)new Releaser(this)); } public Task<IDisposable> LockAsync() { var wait = m_semaphore.WaitAsync(); return wait.IsCompleted ? m_releaser : wait.ContinueWith((_, state) => (IDisposable)state, m_releaser.Result, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } private sealed class Releaser : IDisposable { private readonly AsyncLock m_toRelease; internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; } public void Dispose() { m_toRelease.m_semaphore.Release(); } } } } |
Böylece geldik bir makalenin daha sonuna. Bu makalede asenkron yapılarda Lock nesnesinin nasıl kullanılacağına ve yanlış kullanımında ne gibi sakıncaların olabileceğine hepberaber inceledik. Unutulmamalıdır ki dinamic içerikli yapılarda lock object’in her bir detay için farklılaştırılması bize, asenkron paralel yapının korunmasını sağlamaktadır.
Yeni bir makalede görüşmek üzere hoşçakalın.
Source:
ThreadSafe bir işlemde double check yerine Lazy class’ ı kullanıyoruz hocam. Fakat bazı yerlerde thread yönetiminde de kullanıldığına şahit oldum. Multithread uygulamalarda hala try catch yönetimi çok zor iç içe olan threadlerde örneğin.
Selamlar,
Aslında thread ile task gerçekten karıştırıl ise de aslında birbirinden farklı konular. Lazy load ile lock işlemini, pek degil hiç önermiyorum.
Bir de bu makalede zaten iç içe threadlerden uzak durulmuş, paralel multitask çalışan işlemler üzerinden örnekler verilmiştir.
İyi çalışmalar.