Software Transactional Memory (STM), veritabanlarında bulunan transaction yapısının avantajlarını kullanarak kodun herhangi bir parçasında transaction bütünlüğünü sağlayabileceğimiz bir mekanizma. STM’yi direkt olarak kullanabildiğimiz diller (Clojure) olabileceği gibi, Java, C# gibi dillerde de farklı kütüphaneler ile STM desteğini sağlayabiliyoruz. Örnek kodlarda, bulabildiğim en güncel C# STM kütüphanesi olan Shielded‘ı kullandım.
Mesela elimizde aşağıdaki gibi bir kod olduğunu düşünelim;
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 |
namespace STM { class Program { private static int counter = 0; static void Main(string[] args) { var thread1 = new Thread(run); var thread2 = new Thread(run); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine(counter); } private static void run() { for (int i = 0; i < (int)1e8; i++) { counter++; } } } } |
Kod aslında counter ‘ı 2 thread kullanarak
yapmaya çalışıyor ama thread-safe olmadığı için her çalıştırmada rastgele ve beklenen değerden küçük sonuçlar verecek. Bunu düzeltmek için C#’ın bize sağladığı lock yapısını kullanıyoruz.
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 |
namespace STM { class Program { private static int counter = 0; private static object mutex = new object(); static void Main(string[] args) { var thread1 = new Thread(run); var thread2 = new Thread(run); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine(counter); } private static void run() { for (int i = 0; i < (int)1e8; i++) { lock (mutex) { counter++; } } } } } |
Bu kod bize her seferinde doğru sonucu verecek. Basit bir durum olduğu için lock bloğunu rahatça kullandık. Peki eğer elimizde 2 counter varsa ne yapacağız? Yeni durumda counter1 eskisi gibi çalışsın, buna ek olarak da counter1‘in 100’ün katını aldığı her değerde counter2‘yi 1 arttıralım. 3. bir thread’le de counter2‘yi teker teker arttıralım. Bu durumda kodu nasıl thread-safe yapacağız? Bir örneği aşağıdaki gibi;
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 |
namespace STM { class Program { private static int counter1 = 0; private static int counter2 = 0; private static object mutex1 = new object(); private static object mutex2 = new object(); static void Main(string[] args) { var thread1 = new Thread(run1); var thread2 = new Thread(run1); var thread3 = new Thread(run2); thread1.Start(); thread2.Start(); thread3.Start(); thread1.Join(); thread2.Join(); thread3.Join(); Console.WriteLine(counter1); Console.WriteLine(counter2); } private static void run1() { for (int i = 0; i < (int)1e8; i++) { lock (mutex1) { counter1++; if (counter1 % 100 == 0) { lock (mutex2) { counter2++; } } } } } private static void run2() { for (int i = 0; i < (int)1e6; i++) { lock (mutex2) { counter2++; } } } } } |
Kodu daha basit yapmak için tüm işlemlerde mutex1 ‘i kullanabilirim ama bu doğru değil, counter2 ‘yi de bağımsız olarak arttırabilmem lazım. Bir counter daha ekleyince kod gereksiz yere karmaşıklaştı.
Burada alternatif olarak STM’yi deneyebilirim artık.
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 |
namespace STM { using Sh = Shielded; class Program { private static Sh.Shielded<int> counter1 = new Sh.Shielded<int>(0); private static Sh.Shielded<int> counter2 = new Sh.Shielded<int>(0); static void Main(string[] args) { var thread1 = new Thread(run1); var thread2 = new Thread(run1); var thread3 = new Thread(run2); thread1.Start(); thread2.Start(); thread3.Start(); thread1.Join(); thread2.Join(); thread3.Join(); Sh.Shield.InTransaction( () => { Console.WriteLine(counter1.Value); Console.WriteLine(counter2.Value); } ); } private static void run1() { for (int i = 0; i < (int)1e6; i++) { Sh.Shield.InTransaction( () => { counter1.Value = counter1 + 1; if (counter1 % 100 == 0) { counter2.Value = counter2 + 1; } } ); } } private static void run2() { for (int i = 0; i < (int)1e4; i++) { Sh.Shield.InTransaction( () => { counter2.Value = counter2 + 1; } ); } } } } |
Son kod ile iç içe bir sürü lock kullanmadan işi hallettim.
Shielded bu kontrolleri dilin verdiği lock yapısı ile değil, nesnelere birer zaman etiketi atayarak ve her erişimde bunu kontrol ederek yapıyor. Nesnenin en son haline erişemediğimiz halde de verdiğimiz kod bloğunu baştan çalıştırıyor. Bu tekrar durumunu göz önünde bulundurarak, blok içindeki Shielded<> olmayan nesnelere ve I/O gibi transaction dışı işlemlere güvenmemek gerekiyor. Yani, run2 ‘deki bloğun başına bir Console.WriteLine yazarsak, her çalıştırmada konsola yazılan satır sayısı aynı olmayabilir.
Son olarak, Shielded kütüphanesi içinde Tree, HashSet, Dictionary gibi yapıların transaction ile çalışan hallerini bulmak da mümkün.