Mutex vs. Semaphore

Ezgi Gökdemir
SabancıDx
Published in
4 min readJun 30, 2023
Mutex & Semaphore

Herkese merhabalar. Bu yazımda mutex ve semaphore kavramlarının neler olduğundan ve birbirleri arasındaki farklardan bahsetmeye çalışacağım. Go ile bir takım örnekler uygulayarak konuyu pekiştirebilecek kod örnekleri göstereceğim.

Critical Section

Bir program eşzamanlı olarak çalıştığında, paylaşılan kaynakları yani shared resource dediğimiz alanı değiştiren kod bölümlerine aynı anda birden fazla thread tarafından erişilmemesi gerekir. Bu kısımlar, veri bütünlüğünü sağlamak ve race condition’lardan kaçınmak için her seferinde yalnızca bir işlem veya iş parçacığı tarafından yürütülmelidir. Paylaşılan kaynakları değiştiren bu kod bölümüne Critical Section denir. Critical Section, paylaşılan kaynaklara erişen ve bunları değiştiren bir program kodu parçasıdır aslında.

Mutex nedir?

Mutex (mutual exclusion), aynı anda paylaşılan bir kaynağa veya Critical Section’a yalnızca bir iş parçacığının ya da işlemin erişebilmesini sağlamak için eşzamanlı programlamada kullanılan bir yaklaşımdır.

Aynı anda shared resource bir alana birden fazla kez erişilmeye ve bir değişiklik yapılmaya çalışıldığında ortaya race condition dediğimiz bir problem çıkar. Bu gibi hataları önlemek için herhangi bir zamanda kodun Critical Section dediğimiz kısmını yalnızca bir thread veya goroutine’in çalıştırdığından emin olmak adına bir kilitleme mekanizması yani mutex kullanırız.

Bir iş parçacığı veya işlem, Critical Section’a girmek veya paylaşılan bir kaynağa erişmek istediğinde, mutex’i almaya çalışır. Mutex’in kilidi açılırsa, iş parçacığı veya işlem mutex’i başarılı bir şekilde alır ve kilitli hale getirerek işin yürütülmesine devam eder. Mutex zaten başka bir iş parçacığı veya işlem tarafından kilitlenmişse, istekte bulunan iş parçacığı veya işlem engellenir veya mutex’in kullanılabilir hale gelmesini bekleyen bir döngüde kalabilir.

Bir thread veya goroutine, Critical Section’daki işini bitirdiğinde, mutex’i serbest bırakarak kilidini kaldırır ve diğer goroutine’lerin onu alıp Critical Section’a girmesine izin verir.

Golang özelinde mutex, sync paketi içerisinde mevcuttur ve built-in bir şekilde bu yapıyı kullanabiliriz. Mutex’te Lock (Kilitleme) ve Unlock (Kilit Açma) olmak üzere iki yöntem tanımlanmıştır.

Şimdi bir örnek üzerinden konuyu detaylandırmaya çalışalım. İlk olarak bir race condition örneği inceleyelim.

Burada integer tipinde Price ve Name adlı iki alana sahip bir Product struct’ı tanımladık. Bu struct için bir setPrice metodu yazdık. Bu metot bir sync.WaitGroup pointer’ını parametre olarak alıyor ve içerisinde price değerini 1 artırarak goroutine’in execution işlemini tamamladığını bildirmek için WaitGroup’un Done() metodunu kullanıyor.

Main metodu içerisinde ise bir döngü başlattık. wg.Add(1) ile tek bir goroutine beklenmesi gerektiğini söyledik. Ardından, modelin setPrice metodunu eşzamanlı olarak yürüten go model.setName(&wg) yazarak bir goroutine oluşturduk. Döngü ile tüm goroutine’leri oluşturduktan sonra, WaitGroup’un Wait() metodunu çağırdık. Bu, tüm goroutine’ler WaitGroup’ta Done metodunu çağırana kadar main goroutine’in execute edilmesini engellemek için yapılır. Son olarak da modelin price değerini ekrana yazdırdık.

Ancak buradaki kod örneğinde olası bir problem var. Birden fazla goroutine aynı model üzerinde aynı anda setPrice metodunu çağırıyor. Dolayısıyla biz bu kodu çalıştırmak istediğimizde her seferinde farklı bir Price çıktısı alacağız. Yani bir race condition problemi yaşayacağız.

İşte bu tarz bir problemi gidermek için mutex kullanabiliriz. Dolayısıyla kodu aşağıdaki şekilde güncelleyelim.

Burada Product struct’ına bir sync.Mutex alanı ekledik. setPrice metodu, Price alanını değiştirmeden önce bu mutex’i lock’lyıp, daha sonra unlock yapıyor. Bu sayede, aynı anda yalnızca bir goroutine’nin shared resource bir alana erişmesini ve bunları değiştirmesini sağlayarak race condition’a engel oluyoruz. Dolayısıyla kodun çıktısında herhangi bir değişiklik olmayacak ve bizim için Price değeri her zaman 10000 olacak.

Semaphore nedir?

Semaphore, sınırlı sayıda kaynağa erişimi kontrol etmemize veya eşzamanlı işlem sayısını sınırlamamıza olanak sağlar. Semaphore’lar, birden fazla thread veya goroutine tarafından bir veya birden fazla sınırlı kaynağa erişimi kontrol etmek için kullanılabilir. Maksimum sınırın aşılmamasını sağlarken, belirli sayıda işlemin kaynaklara aynı anda erişmesine izin verirler. Bu sayede shared resource alanların verimli kullanılmasını sağlar ve birden fazla goroutine’nin aynı kaynağa aynı anda erişmeye çalıştığı durumlarda ortaya çıkabilecek race condition gibi problemlerin önüne geçer.

Bir kod örneği üzerinden gidelim.

Burada ilk olarak bir Semaphore struct’ı tanımladık. Bu struct içerisine concurrency kontrolü yapmak için concurrencyChan isimli bir channel ekledik.

Yazdığımız CreateSemaphore metodu parametre olarak bir integer değeri alıyor. Semaphore içerisinde eşzamanlı olarak çalışacak maksimum goroutine sayısını bu değerle belirtiyoruz.

Acquire metodu, semaphore’dan bir belirteç almaktan sorumludur. Yani bir goroutine’in shared resource bir alana erişmek için ilerleyebileceğini gösterir bize ve o alanı mevcut işlemler için kilitler. struct{}{} ifadesi Go dilinde genelde channel’larda veya bir takım senkronizasyon işlemlerinde kullanılır. Burada da amacımız herhangi bir özel veri taşımadan bir kaynağın kullanılabilirliğini ifade etmek olduğu için s.concurrencyChan <- struct{}{} şeklinde bir atama işlemi yapıyoruz.

Release metodu ile boş struct’ı channel’dan çıkarıyoruz. Bu sayede channel bir sonraki değer için müsait hale gelecek. Aslında burada bir unblocking işlemi yapıyoruz.

Main içerisinde CreateSemaphore metodunu, maksimum eşzamanlılık değeri 10 olacak şekilde yeni bir Semaphore instance’ı oluşturmak için çağırdık. Tüm goroutine’ler tamamlandığında kontrol sağlamak için ise isDone isimli bir channel oluşturduk. Döngünün dışında main metodu, isDone channel’ından bir değer almak için bekleyecek (<-isDone). Buradaki amacımız isDone değeri true olana kadar main goroutine’inini bloklamak aslında. Yani limit değerimize ulaştığımızda isDone true olacak ve main goroutine’i üzerindeki blok kalkacak.

Oluşturduğumuz bu yapı sayesinde en fazla 10 goroutine’nin aynı anda yürütülebilmesini sağlamış olduk.

Mutex ve Semaphore arasındaki farklar

  • Mutex tek bir kaynağa veya critical section’a erişimi kontrol etmek için tasarlanmıştır ve aynı anda yalnızca bir iş parçacığının veya goroutine’in bu alana erişmesine izin verir. Semaphore ise birden çok kaynağa erişimi kontrol edebilir ve belirli sayıda iş parçacığının veya goroutine’in bu alanlara aynı anda erişmesine izin verebilir.
  • Semaphore, kullanılabilir kaynakların sayısını temsil eden bir değer tutar. Thread’ler veya goroutine’ler, bu sayıyı artırarak veya azaltarak kaynakları alabilir veya serbest bırakabilir. Dolayısıyla mutex’e benzer şekilde count’u 1 olan bir binary semaphore da kullanılabiliriz. Ancak mutex’in binary semaphore’dan farkı, mutex’i kilitleyen thread veya goroutine’le kilidi açan thread veya goroutine’nin aynı olması gerektiğidir.
  • Mutex’ler genellikle hassas senkronizasyon işlemleri için kullanılır ve aynı anda yalnızca bir iş parçacığına izin verilmesi gereken critical section dediğimiz kısmı korur. Öte yandan semaphore’lar, genellikle daha geniş senkronizasyon amaçları için kullanılır ve belirli sayıda iş parçacığının veya goroutine’in paylaşılan bir kaynağa aynı anda erişmesine izin verir.

Bu yazımda mutex ve semaphore kavramlarına olabildiğince değinmeye çalıştım. Umarım faydalı bir içerik olmuştur. Keyifli okumalar :)

--

--