Çoklu iş parçacığı kullanmaya başlama
Kilitlenmeler
İki veya daha fazla iş parçacığından oluşan bir grubun her üyesi, devam etmeden önce diğer üyelerden birinin bir şey yapmasını (örneğin, bir kilidi serbest bırakmak için) beklemesi gerektiğinde bir kilitlenme oluşur. Müdahale olmadan, iplikler sonsuza kadar bekler.
Kilitlenmeye meyilli bir tasarımın sözde kod örneği:
thread_1 {
acquire(A)
...
acquire(B)
...
release(A, B)
}
thread_2 {
acquire(B)
...
acquire(A)
...
release(A, B)
}
“thread_1” “A"yı edindiğinde, ancak henüz “B"yi almadığında ve “thread_2”, “B"yi edindiği halde “A"yı almadığında bir kilitlenme meydana gelebilir. Aşağıdaki şemada gösterildiği gibi, her iki iş parçacığı sonsuza kadar bekleyecektir.
Kilitlenmelerden Nasıl Kaçınılır
Genel bir kural olarak, kilit kullanımını en aza indirin ve kilitleme ile kilit açma arasındaki kodu en aza indirin.
Aynı Sırada Kilitler Alın
“thread_2"nin yeniden tasarımı sorunu çözer:
thread_2 {
acquire(A)
...
acquire(B)
...
release(A, B)
}
Her iki iş parçacığı kaynakları aynı sırayla alır, böylece kilitlenmelerden kaçınır.
Bu çözüm, “Kaynak hiyerarşisi çözümü” olarak bilinir. Dijkstra tarafından “Yemek filozofları sorununa” bir çözüm olarak önerildi.
Bazen kilit alımı için katı bir sıra belirtseniz bile, bu tür statik kilit edinme sırası çalışma zamanında dinamik hale getirilebilir.
Aşağıdaki kodu göz önünde bulundurun:
void doCriticalTask(Object A, Object B){
acquire(A){
acquire(B){
}
}
}
Burada, kilit edinme sırası güvenli görünse bile, thread_1 bu yönteme örneğin Object_1 parametresi A olarak ve Object_2 parametresi B olarak ve thread_2 ters sırada, yani Object_2 parametresi A olarak ve Object_1 parametresi B olarak eriştiğinde bir kilitlenmeye neden olabilir.
Böyle bir durumda, bir tür hesaplama ile hem Object_1 hem de Object_2 kullanılarak türetilen benzersiz bir koşula sahip olmak daha iyidir, örn. her iki nesnenin karma kodunu kullanarak, bu nedenle, bu yönteme hangi parametrik sırayla farklı bir iş parçacığı girdiğinde, bu benzersiz koşul her zaman kilit edinme sırasını türetir.
Örneğin. Say Object’in benzersiz bir anahtarı var, ör. Hesap nesnesi durumunda accountNumber.
void doCriticalTask(Object A, Object B){
int uniqueA = A.getAccntNumber();
int uniqueB = B.getAccntNumber();
if(uniqueA > uniqueB){
acquire(B){
acquire(A){
}
}
}else {
acquire(A){
acquire(B){
}
}
}
}
Yarış koşulları
Bir veri yarışı veya yarış koşulu, çok iş parçacıklı bir program düzgün bir şekilde senkronize edilmediğinde ortaya çıkabilecek bir sorundur. İki veya daha fazla iş parçacığı aynı belleğe senkronizasyon olmadan erişirse ve erişimlerden en az biri ‘yazma’ işlemiyse, bir veri yarışı meydana gelir. Bu, programın platforma bağlı, muhtemelen tutarsız davranışına yol açar. Örneğin, bir hesaplamanın sonucu, iş parçacığı planlamasına bağlı olabilir.
writer_thread {
write_to(buffer)
}
reader_thread {
read_from(buffer)
}
Basit bir çözüm:
writer_thread {
lock(buffer)
write_to(buffer)
unlock(buffer)
}
reader_thread {
lock(buffer)
read_from(buffer)
unlock(buffer)
}
Bu basit çözüm, yalnızca bir okuyucu dizisi varsa iyi çalışır, ancak birden fazla varsa, okuyucu dizileri aynı anda okuyabildiğinden yürütmeyi gereksiz yere yavaşlatır.
Bu sorunu önleyen bir çözüm şunlar olabilir:
writer_thread {
lock(reader_count)
if(reader_count == 0) {
write_to(buffer)
}
unlock(reader_count)
}
reader_thread {
lock(reader_count)
reader_count = reader_count + 1
unlock(reader_count)
read_from(buffer)
lock(reader_count)
reader_count = reader_count - 1
unlock(reader_count)
}
Tüm yazma işlemi boyunca “okuyucu_sayısı"nın kilitli olduğuna dikkat edin, öyle ki, yazma işlemi bitmeden hiçbir okuyucu okumaya başlayamaz.
Artık birçok okuyucu aynı anda okuyabilir, ancak yeni bir sorun ortaya çıkabilir: ‘okuyucu_sayısı’ hiçbir zaman ‘0’a ulaşmayabilir, öyle ki yazar iş parçacığı asla arabelleğe yazamaz. Buna açlık denir), bundan kaçınmak için farklı çözümler vardır.
Doğru gibi görünen programlar bile sorunlu olabilir:
boolean_variable = false
writer_thread {
boolean_variable = true
}
reader_thread {
while_not(boolean_variable)
{
do_something()
}
}
Okuyucu dizisi, yazar dizisinden gelen güncellemeyi asla göremeyebileceğinden, örnek program hiçbir zaman sona ermeyebilir. Örneğin, donanım CPU önbelleklerini kullanıyorsa, değerler önbelleğe alınabilir. Normal bir alana yazma veya okuma, önbelleğin yenilenmesine yol açmadığından, değişen değer okuma dizisi tarafından asla görülmeyebilir.
C++ ve Java, uygun şekilde senkronize edilenin ne anlama geldiğini bellek modelinde tanımlar: C++ Bellek Modeli , Java Bellek Modeli.
Java’da bir çözüm, alanı geçici olarak ilan etmek olacaktır:
volatile boolean boolean_field;
C++‘da bir çözüm, alanı atomik olarak bildirmek olacaktır:
std::atomic<bool> data_ready(false)
Bir veri yarışı, bir tür yarış koşuludur. Ancak tüm yarış koşulları veri yarışları değildir. Birden fazla iş parçacığı tarafından çağrılan aşağıdakiler bir yarış durumuna yol açar, ancak bir veri yarışına yol açmaz:
class Counter {
private volatile int count = 0;
public void addOne() {
i++;
}
}
Java Bellek Modeli spesifikasyonuna göre doğru bir şekilde senkronize edilmiştir, bu nedenle veri yarışı değildir. Ama yine de bir yarış koşullarına yol açar, örn. sonuç, ipliklerin serpiştirilmesine bağlıdır.
Tüm veri yarışları hata değildir. Sözde iyi huylu yarış koşuluna bir örnek sun.reflect.NativeMethodAccessorImpl’dir:
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
...
}
Burada kodun performansı, numInvocation sayısının doğruluğundan daha önemlidir.
Hello Multithreading - Yeni ileti dizileri oluşturma
Bu basit örnek, Java’da birden çok iş parçacığının nasıl başlatılacağını gösterir. İş parçacıklarının sırayla yürütülmesinin garanti edilmediğini ve yürütme sırasının her çalıştırma için değişebileceğini unutmayın.
public class HelloMultithreading {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new MyRunnable(i));
t.start();
}
}
public static class MyRunnable implements Runnable {
private int mThreadId;
public MyRunnable(int pThreadId) {
super();
mThreadId = pThreadId;
}
@Override
public void run() {
System.out.println("Hello multithreading: thread " + mThreadId);
}
}
}
Aynı iş parçacığı iki kez çalışabilir mi?
Aynı iş parçacığının iki kez çalıştırılabileceği en sık sorulan soruydu.
Bunun cevabı, bir iş parçacığının yalnızca bir kez çalışabileceğini bilmektir.
Aynı iş parçacığını iki kez çalıştırmayı denerseniz, ilk kez yürütülür ancak ikinci kez hata verir ve hata IllegalThreadStateException olur.
örnek:
public class TestThreadTwice1 extends Thread{
public void run(){
System.out.println("running...");
}
public static void main(String args[]){
TestThreadTwice1 t1=new TestThreadTwice1();
t1.start();
t1.start();
}
}
çıktı:
running
Exception in thread "main" java.lang.IllegalThreadStateException
Amaç
İş parçacıkları, komut işlemenin gerçekleştiği bir bilgi işlem sisteminin düşük seviyeli parçalarıdır. CPU/MCU donanımı tarafından desteklenir/sağlanır. Bir de yazılım yöntemleri var. Çoklu iş parçacığının amacı, mümkünse birbirine paralel hesaplamalar yapmaktır. Böylece istenen sonuç daha küçük bir zaman diliminde elde edilebilir.