SpinLock & Mutex Karşılaştırması
Mutex kullanımlarının performans problemi yaratabileceği senaryolar mevcuttur.
Örnek olarak A ve B thread'lerinin bir mutex kaynağını kilitleyip, çok hızlı biçimde işlemi gerçekleştirip kilidi kaldırdığını, bu işlemi de saniyede binlerce veya yüzbinlerce defa yaptığını düşünelim.
Mutex operasyonları context-switch gerektirdiğinden kilitli kalma süresi çok az olsa da bir yerden sonra her bir kilitleme işleminde thread'in Sleep durumuna geçmesi sonra tekrar uyandırılması sürecinin kendisi zaman alıcı bir işleme dönüşür.
Böyle bir durumda mutex kullanmak yerine spinlock kullanımı daha avantajlı olur.
Spinlock kullanıcı kipinde busy wait ile gerçekleştirilir. Dolayısıyla ilgili thread Sleep moduna geçmez, context-switch gerçekleşmez ve kullanıcı kipinde kendisine tahsis edilen cpu zamanını sonuna kadar kullanabilmiş olur.
Ancak eğer ilgili uygulama cpu kullanmaya devam ederek beklediği spinlock'a çok hızlı bir şekilde erişemez ise bu defa tersi bir etki yaratır ve Sleep moduna geçmesi halinde aynı zaman diliminde CPU ile başka bir işlem yapılabilecekken, bu imkan ortadan kalkar ve CPU yükünün artmasına neden olur.
Dolayısıyla spinlock kullanımının tüm senaryolarda daha iyi sonuç verecek olduğunu söylemek çok yanlış olur.
Dahası spinlock kullanımı özellikle Linux mutex performansının çok iyileştirilmiş olmasından ötürü, pek çok senaryoda mutex kullanımının daha iyi sonuç vermesini sağlar. Emin değilseniz, risk almayın, mutex kullanın.
Probleminizi iyi tanıdıysanız ve spinlock kullanımını iyice test ettiyseniz kullanabilirsiniz.
Aşağıdaki örnek uygulama kodunu locktest.c
adıyla kaydedin. Normal mutex versiyonunu ek bir parametre vermeden, Spinlock versiyonunu -DUSE_SPINLOCK
parametresiyle aşağıdaki gibi derleyip 100 milyon loop parametresi ile çalıştırıp işlem süresini ölçün.
Teste başlamadan önce Cpu Frequency Scaling aktif ise işlemci hızını aşağıdaki gibi maksimuma ayarlayıp, bu sebeple sonuçlarda oluşacak ek dalgalanmayı bertaraf etmeniz de önerilir (işlemin tüm cpu çekirdekleri için tekrar edilmesi gereklidir):
# echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
/* locktest.c */
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <errno.h>
#include <sys/time.h>
#include "../common/utlist.h"
#include "../common/debug.h"
#define DEFAULT_LOOP_COUNT 10000000
struct list {
int no;
struct list *next;
};
struct timespec timespec_diff (struct timespec before, struct timespec after)
{
struct timespec res;
if ((after.tv_nsec - before.tv_nsec) < 0) {
res.tv_sec = after.tv_sec - before.tv_sec - 1;
res.tv_nsec = 1000000000 + after.tv_nsec - before.tv_nsec;
} else {
res.tv_sec = after.tv_sec - before.tv_sec;
res.tv_nsec = after.tv_nsec - before.tv_nsec;
}
return res;
}
#ifdef USE_SPINLOCK
pthread_spinlock_t spinlock;
#else
pthread_mutex_t mutex;
#endif
struct list *mylist;
pid_t gettid() { return syscall( __NR_gettid ); }
void *worker (void *args)
{
(void) args;
struct list *tmp;
infof("Worker thread %lu started", (unsigned long )gettid());
while (1) {
#ifdef USE_SPINLOCK
pthread_spin_lock(&spinlock);
#else
pthread_mutex_lock(&mutex);
#endif
if (mylist == NULL) {
#ifdef USE_SPINLOCK
pthread_spin_unlock(&spinlock);
#else
pthread_mutex_unlock(&mutex);
#endif
break;
}
tmp = mylist;
LL_DELETE(mylist, tmp);
#ifdef USE_SPINLOCK
pthread_spin_unlock(&spinlock);
#else
pthread_mutex_unlock(&mutex);
#endif
}
return NULL;
}
int main (int argc, char *argv[])
{
int i;
int loop_count;
pthread_t thr1, thr2;
struct timespec before, after;
struct timespec result;
struct list *el;
#ifdef USE_SPINLOCK
pthread_spin_init(&spinlock, 0);
#elif USE_HYBRID
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ADAPTIVE_NP);
#else
pthread_mutex_init(&mutex, NULL);
#endif
if (argc == 2) {
loop_count = atoi(argv[1]);
} else {
loop_count = DEFAULT_LOOP_COUNT;
}
infof("Preparing list");
struct list *container = calloc(loop_count, sizeof(struct list));
for (i = 0; i < loop_count; i++) {
el = &container[i];
el->no = i;
LL_PREPEND(mylist, el);
}
clock_gettime(CLOCK_MONOTONIC, &before);
pthread_create(&thr1, NULL, worker, NULL);
pthread_create(&thr2, NULL, worker, NULL);
pthread_join(thr1, NULL);
pthread_join(thr2, NULL);
clock_gettime(CLOCK_MONOTONIC, &after);
result = timespec_diff(before, after);
debugf("Elapsed time with %d loops: %li.%06li", loop_count, result.tv_sec,
result.tv_nsec / 1000);
#ifdef USE_SPINLOCK
pthread_spin_destroy(&spinlock);
#else
pthread_mutex_destroy(&mutex);
#endif
return 0;
}
Şimdi normal mutex ve spinlock versiyonlarını derleyelim:
$ gcc -o locktest_normal locktest.c -lrt -lpthread
$ gcc -DUSE_SPINLOCK -o locktest_spinlock locktest.c -lrt -lpthread
Her iki uygulamayı 100 milyon loop için aşağıdaki gibi çalıştırdığımızda performans farklılıklarını görebiliriz:
$ ./locktest_normal 100000000
debug: Elapsed time with 100000000 loops: 19.487106 (main locktest.c:120)
$ ./locktest_spinlock 100000000
debug: Elapsed time with 100000000 loops: 8.115992 (main locktest.c:120)
Testleri bir kaç defa daha üstüste çalıştırdığımızda bir miktar değişiklikler görebiliriz ancak normal mutex versiyonu, spinlock kullanan versiyondan en az 2 kat daha yavaş çalışmaktadır.
Hibrid Yaklaşım
Bu senaryo için spinlock'un daha iyi olduğunu gördük. Bir çok senaryo için de mutex kullanımının toplam sistem performansı için daha olumlu olacağını söyledik.
POSIX standardında her iki yaklaşımı birden kullanan hibrid bir modele de yer verilmiş olup Linux NPTL implementasyonunda böyle bir kullanım da desteklenmektedir.
Hibrid modelde mutex lock işlemi önce spinlock kullanılarak (try_lock mekanizmalarıyla) maksimum N defa denenenir (bu değerin hesaplaması glibc içerisinde mevcuttur). Ardından spinlock ile elde edilemiyorsa bu defa geleneksel mutex lock modeline geri dönülür.
Elbette hibrit modelin de tüm senaryolara uygulanabileceğini söylemek doğru olmaz. Spinlock kullanım avantajlarını hiç üretemeyen bir yazılım akışınız var ise, hibrid mod da performans kaybına yol açacaktır.
Hibrid modun kullanılabilmesi için, mutex initialize işleminde PTHREAD_MUTEX_ADAPTIVE_NP
tipinin seçilmiş olması gereklidir.
Yukarıdaki örnek uygulamamızın -DUSE_HYBRID
parametresiyle üçüncü bir versiyonunu derleyip test edelim:
$ gcc -DUSE_HYBRID -o locktest_hybrid locktest.c -lrt -lpthread
$ ./locktest_hybrid 100000000
debug: Elapsed time with 100000000 loops: 11.823482 (main locktest.c:120)
Görüldüğü üzere doğrudan spinlock kullandığımız versiyona oranla biraz daha kötü ama ona çok yakın bir değer elde etmiş olduk.