> Atomik İşlemler: "thread" ler dünyasında atomiklik demek bir işlemin kesilmeden tek parça halinde yapılmasına denkmektedir. Genel olarak makina komutları atomiktir. Yani bir makina kodu çalıştırılırken kesilme olmaz, dolayısıyla "thread" ler arası geçiş oluşamaz. Çünkü bu geçiş donanım kesmeleri ile sağlanmaktadır ve donanım kesmeleri de ancak makine komutları arasında etkili olabilmektedir. Pekiyi iki işlemci ya da çekirdek aynı "global" değişkeni aynı anda değiştirmek isterse ne olur? Normal şartlarda hangi işlemci ya da çekirdek geç kalmışsa, onun ürettiği değer geçerli olacaktır. Fakat özel bazı durumlarda ilgili değişkende bozulmalar meydana gelebilmektedir. Aşağıdaki makina kodunu düşünelim; INC g_count, 1 Bu komut çalışırken "thread" ler arasında geçiş oluşmayacak olsa da Intel marka işlemci ya da çekirdek, bu işlem sırasında bellek-işlemci arasındaki "bus" hattını tutup işlemi atomik yapmamaktadır. Bu tarz işlemlerde Intel işlemciler, "bus" ı tutup bırakabilmektedir. Yani kesikli bir biçimde çalışmaktadır. İşte tam bu anda başka bir işlemci ya da çekirdek "bus" ın kontrolünü alarak oraya bir şeyler yazarsa, evvelki işlemci bu yeni değeri alacaktır. Bu da bozuk değerin üretilmesine yol açacaktır. Fakat bunun olma olasılığı çok düşüktür. Ancak aşağıdaki gibi bir durumda meydana gelebilir; /* Birinci çekirdek */ MOV g_val, 100 /* İkinci çekirdek */ MOV g_val, 200 Buradaki "g_val" içerisinde "100" ya da "200" olması normal karşılanırken, bozuk bir değerin olması istenmeyen bir durumdur. İşte Intel işlemcilerde, buradaki "g_val" değişkeni belleğe uygun şekilde hizalanmamışsa, bozuk bir değer üretilebilmektedir. Diğer yandan işlemciler bu biçimdeki erişimlerde ilgili makina komutunun sonuna kadar "bus" ın tutulması için de olanak sağlamaktadır. Örneğin, Intel işlemcilerinde komutun başına eklenen "LOCK" deyimi sayesinde ilgili "bus" sonuna kadar tutulacaktır: LOCK INC g_val, 1 Öte yandan bu "LOCK" deyimi komutu yavaşlatmaktadır. Dolayısıyla varsayılan durumda bu deyim kullanılmaz. Eğer tek makina komutu yerine aynı işi yapan üç makina komutu kullansak nasıl olur? Şöyleki; MOV reg, g_val INC reg, 1 MOV g_val, reg Tabii bu durumda da bu makina kodları arasında "thread" ler arası geçiş meydana gelebilir. Bu da ilgili değişkende bozulmaya yol açacaktır. Biz bu bozulmayı önlemek için de Kritik Kod bölgesi oluşturmalıyız. Şimdi buradan da görüleceği üzere bir işi yapmak için tek bir makina kodunu çalıştırmakla birden fazla makina kodunu çalıştırmak arasında avantaj ve dezavantaj bulunmaktadır. Pekiyi bizler hangi durumda hangi yaklaşımı seçmeliyiz? Burada C ile yazarken arada "inline assembly" kodu da yazabiliriz. Bu kullanım derleyiciye özgüdür. Ancak "inline assembly" yazmak hem zahmetli hem de makine dili bilmeyi gerektirir. Bir diğer alternatif ise şudur: Bazı C derleyicilerinde "built-in" ya da "intrinsic" fonksiyon denilen bir kavram vardır. Bu tip fonksiyonlar öyle fonksiyonlardır ki derleyiciler bu fonksiyonların ne yaptığını kafadan bilmektedir. C standartları ile bir alakası yoktur. Derleyici bu fonksiyonların çağrıldığı yerlerde fonksiyon çağrısı yerine direkt olarak kodu yerleştirmektedir. Böylelikle tek bir makina kodu ile bir iş yapacağız ve bu makine kodunun başına da "LOCK" getirilecek. Böylelikle hem "thread" ler arasında hem de "bus" alma/bırakma sırasında gerçekleşebilecek sorunların önüne geçmiş olacağız. Dolayısıyla bizler bir değişkenin değerini bir arttırma işini "multi-processor" işlemcilerde "mutex" kullanarak yapmak yerine ya "Inline Assembly" kullanmalı ya da derleyicilerin sunduğu "built-in"/"intrinsic" fonksiyonları kullanmalıyız. C11 ile birlikte C diline de "atomic" niteleyicisi eklenmiştir ki bu niteleyici sayesinde bizler ne "Inline Assembly" ne de ilgili "built-in"/"intrinsic" fonksiyonları kullanmaya gerek kalmamıştır. C++ dilinde ise "atomic" şablon sınıfı kullanılmaktadır. Şimdi buradaki durumu özetlersek; -> Tek bir makine komutu çalıştırılken "bus" hattının alınıp bırakılması sırasında, ilgili "bus" hattı başka işlemci tarafından alınırsa, değişkenimizin değeri bozulabilir. Bunu engellemek için ilgili makine kodunun başına "LOCK" deyimini eklemeliyiz. -> Birden fazla makine komutu çalıştırılken, komutlar arasındaki geçiş sırasında, "thread" ler arası geçiş meydana gelebilir. Bunu engellemek için de "mutex" nesneleri ile Kritik Kod bölgesi de oluşturabiliriz. -> Bu iki sıkıntıyı gidermek için ya "Inline Assembly" ya derleyicilerin sunduğu "built-in" fonksiyonlar ya da C11 ile dile eklenen "atomic" niteleyicisini kullanmalıyız. Böylelikle yukarıdaki iki problemin de önüne geçmiş olacağız. Fakat UNUTMAMALIYIZ Kİ HER İŞLEM ATOMİK YAPILAMAZ. Anımsanacağı üzere bazı C derleyicileri "built-in" ya da "intrinsic" fonksiyonlara sahiptir. Bu fonksiyonlar özel fonksiyonlar olup, derleyiciler tarafından ne yaptığı bilinmektedir. Bu tip fonksiyonlardan bazıları "macro" gibi açılırken bazıları açılmamaktadır. Yine bu fonksiyonların herhangi bir "prototipi" de bulunmamaktadır. Örneğin, aşağıdaki kodu inceleyelim: //... for(int i = 0; i < strlen(s); ++i){ //... } //... Normal şartlarda "strlen" fonksiyonu standart bir C fonksiyonudur ve derleyici bu fonksiyonun ne yaptığını normal şartlarda bilemediği için ilgili "for-loop" için herhangi bir optimizasyon uygulayamaz. Dolayısıyla döngünün her turunda "strlen" fonksiyonuna bir çağrı yapar. Fakat o derleyicide "strlen" fonksiyonu aynı zamanda "built-in" / "intrinsic" fonksiyonsa, burada bir optimizasyon yapılabilir. Pekiyi bu fonksiyonların neler olduğuna nasıl ulaşabiliriz? "gcc" derleyicileri için şu bağlantıyı kullanabiliriz; "https://gcc.gnu.org/onlinedocs/gcc/x86-Built-in-Functions.html". Bu listedeki fonksiyonlardan bazıları "atomic" işlemler için bulundurulmaktadır. Bu fonksiyonların isimleri ilk başlarda "__sync_" ön eki alırken, C++ diline "atomic" kütüphanesinin eklenmesi ile birlikte "__atomic_" ön eki almaktadır. Dolayısıyla artık bu ön eki alanların kullanılması tavsiye edilmektedir. Öte yandan "__atomic_" ön ekine sahip fakat "Memory Model" parametreli bazı fonksiyonlar vardır. Böylesi fonksiyonlara parametre olarak "__ATOMIC_SEQ_CST" değerini geçmeliyiz fakat bu değerin ne olduğunu, "Memory Model" ile neyin kastedildiğine bu kursta değinilmemektedir. Bu fonksiyonlardan bazıları şunlardır; "__atomic_fetch_add", "__atomic_store", "__atomic_load" vb. Şimdi de bu fonksiyonları kabaca inceleyelim: >> "__atomic_fetch_add" : Bir değişkenin değerini değiştirmek için kullanılır. * Örnek 1, Aşağıdaki örnekte bir değişkenin değeri bir arttırılmıştır. #include int g_count = 0; int main(int argc, char *argv[]) { /* # OUTPUT # g_count : 0 g_count : 1 */ printf("g_count : %d\n", g_count); __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); printf("g_count : %d", g_count); return 0; } * Örnek 2, Aşağıdaki örnekte ise "mutex", "spinlock" nesneleri ve "built-in" fonksiyonun kullanımı karşılaştırılmıştır: #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_spin_proc1(void* param); void* pthread_spin_proc2(void* param); void* pthread_mutex_proc1(void* param); void* pthread_mutex_proc2(void* param); void* pthread_builtIn_proc1(void* param); void* pthread_builtIn_proc2(void* param); void spin_lock(void); void mutex_lock(void); void built_in_ones(void); void exit_sys_errno(const char* msg, int eno); pthread_spinlock_t g_spinlock; pthread_mutex_t g_mutex; int g_count; int main(void) { /* # OUTPUT # Ok... 20000000, in 1.886349 seconds Ok... 40000000, in 1.951004 seconds Ok... 60000000, in 0.656282 seconds */ spin_lock(); mutex_lock(); built_in_ones(); return 0; } void* pthread_spin_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_spin_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_builtIn_proc1(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void* pthread_builtIn_proc2(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void spin_lock(void) { double start, end; int result; start = clock(); if((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_spin_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_spin_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void mutex_lock(void) { double start, end; int result; start = clock(); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_mutex_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_mutex_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void built_in_ones(void) { double start, end; int result; start = clock(); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_builtIn_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_builtIn_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } >> "__atomic_store" : Bir değişkene değer atamak için kullanılır. Bu fonksiyonun birde sonuna "_n" eki alan versiyonu da vardır ki bu versiyon değeri doğrudan almaktadır. >> "__atomic_load" : Bir değişkenin değerini temin etmek için kullanılır. Yine bu fonksiyonunda "_n" li versiyonu bulunmaktadır. Unutmamalıyız ki bu tip fonksiyonlar derleyiciye bağlı fonksiyonlardır. Böylesi "atomic" işlemler için taşınabilirliği sağlamak adına ilgili programlama diline "atomic" işlemleri yapabilme özelliği eklenmiştir. C dili söz konusu olduğunda, C11 standardı ile birlikte dile "thread" kavramı da eklendiği için, "__Atomic" anahtar sözcüğü de eklenmiştir. Bu anahtar sözcük bir "type qualifier" olup, tıpkı "const" ve "volatile" anahtar sözcükleri gibidir. Öte yandan "_Atomic" anahtar sözcüğü "type specifier" olarak parantezle birlikte de kullanılabilmektedir. Aşağıda her iki kullanıma da bir örnek verilmiştir; _Atomic int g_count = 0; _Atomic(int) g_count = 0; C11 ile birlikte ayrıca "stdatomic.h" başlık dosyası da C standartlarıan eklenmiştir. Bu dosya içerisinde çeşitli "atomic" fonksiyonların prototipleri, "_Atomic" ile oluşturulmuş bazı "typedef" isimleri de bulunmaktadır. Artık derleyicilere özgü "built-in" / "intrinsic" fonksiyonlar yerine, "_Atomic" anahtar kelimesini kullanabiliriz. * Örnek 1, Aşağıdaki örnekte "built-in" kullanımı ve "_Atomic" kullanımı karşılaştırılmıştır. #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_builtIn_proc1(void* param); void* pthread_builtIn_proc2(void* param); void* pthread_atomic_proc1(void* param); void* pthread_atomic_proc2(void* param); void built_in_ones(void); void atomic_ones(void); void exit_sys_errno(const char* msg, int eno); int g_Count; _Atomic int g_count; int main(void) { /* # OUTPUT # Ok... 20000000, in 0.522390 seconds Ok... 20000000, in 0.071704 seconds */ built_in_ones(); atomic_ones(); return 0; } void* pthread_builtIn_proc1(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void* pthread_builtIn_proc2(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void* pthread_atomic_proc1(void* param) { for(int i = 0; i < MAX_COUNT; ++i) ++g_Count; return NULL; } void* pthread_atomic_proc2(void* param) { for(int i = 0; i < MAX_COUNT; ++i) ++g_Count; return NULL; } void built_in_ones(void) { double start, end; int result; start = clock(); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_builtIn_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_builtIn_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void atomic_ones(void) { double start, end; int result; start = clock(); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_atomic_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_atomic_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } Son olarak C11 ile dile eklenen bu "atomic" konusu "optional" olarak bırakılmıştır. Bu da demektir ki her C derleyicisi bu konuyu desteklemeyebilir. Örneğin, gömülü sistemler söz konusu olduğunda ilgili derleyici destek vermeyebilir. C++ dilinde ise "atomic" isminde bir şablon sınıf mevcuttur. C++11 ile dile eklenmiştir. Kullanımı aşağıdaki gibidir: std::atomic g_count = 0; Sadece "g_count" değişkeninin değerini ekrana yazdırırken "int" türüne dönüştürmeliyiz. Böylelikle "atomic" sınıfının "operator int()" fonksiyonuna çağrı yapılsın.