EDA Playground'da Dene

fork...join

Gün 4: Süreçler Arası İletişim (IPC) | Eşzamanlı süreçler: join, join_any, join_none

Bu derste SystemVerilog'da eşzamanlı (paralel) süreçleri başlatmanın temel yapı taşı olan fork...join bloklarını ve üç farklı sonlanma biçimini öğreneceksiniz. Bir testbench'te sürücü (driver), monitör ve sıralayıcı (sequencer) gibi bileşenlerin aynı anda çalışması bu yapı sayesinde mümkün olur.

01 — ÜÇ SONLANMA BİÇİMİ

Aynı fork. Üç farklı kader.

Alt süreçler hep aynı anda başlar. Fark, ana sürecin ne zaman devam ettiğinde.

HEPSİNİ BEKLE

join

Tüm alt süreçler bitene dek ana süreç durur. Süre = en uzun süreç.

İLK BİTENİ BEKLE

join_any

Biri bitti mi ana süreç devam eder. Diğerleri arka planda yaşar.

HİÇ BEKLEME

join_none

Ana süreç anında ilerler. Alt süreçler bağımsız yaşamaya başlar.

02 — ZAMAN LABORATUVARI

Zamanı kendin sür.

Zaman çizelgesine tıkla-sürükle, süreleri kaydırıcılarla değiştir, modu seç. Sağdaki kod ve $display çıktısı simülasyonla birlikte akar.

MOD
SENARYO
SONRA
SİMÜLASYON ZAMANI0ns
fork öncesi
1×

KAYNAK KODfork_join.sv

SİMÜLATÖR ÇIKTISI$display

03 — TUZAK

automatic yoksa, herkes aynı değişkeni paylaşır.

Aynı görevi fork içinde üç kez çağırdın. Değişkenler statikse tek bir bellek alanı var — son yazan kazanır.

BELLEK1 paylaşılan çerçeve

Veriler birbirine karışır

ÇIKTI$display

04 — MEYDAN OKUMA

Ana süreç ne zaman devam eder?

SERİ 0 · EN İYİ 0
05 — ÖZET

Aklında kalsın.

join → en uzun süreToplam bekleme, en uzun süreç kadar.
join_any → en kısa süreİlk biten ana süreci uyandırır; diğerleri hâlâ koşar.
join_none → 0Hiç beklemez. Süreçlerin başlaması için #0 ya da gecikme gerekebilir.
wait fork / disable forkjoin_any sonrası artıkları bekle ya da öldür; yoksa simülasyon beklenmedik uzar.
begin…end = tek süreçBlok içi sıralı, bloklar arası paralel.
Her zaman automaticFork içinde çağrılan task'lar kendi çerçevesini almalı.

fork...join Nedir?

fork...join bloğu, içindeki her bir ifadeyi (statement) ayrı bir paralel süreç (process/thread) olarak başlatır. Normal sıralı (sequential) kod yukarıdan aşağıya satır satır işlenirken, fork bloğunun içindeki satırlar aynı simülasyon zamanında eş zamanlı olarak hayata geçer. Bu, donanımın doğası gereği paralel olduğu bir dünyada gerçek davranışı modellemek için kritiktir.

fork ile açılan blok üç farklı anahtar kelimeyle kapatılabilir ve ana sürecin ne zaman devam edeceği bu seçime göre değişir:

  • join → Ana sürecin devam edebilmesi için forkdaki TÜM alt süreçlerin bitmesini bekler. Toplam süre, en uzun süren süreç kadardır.
  • join_any → Alt süreçlerden HERHANGİ BİRİ (yani ilk biten) tamamlandığında ana süreç devam eder. Diğer süreçler arka planda çalışmaya devam eder.
  • join_none → Ana süreç HİÇBİRİNİ BEKLEMEDEN anında devam eder; alt süreçler arka planda yaşamaya başlar.

Neden Önemli ve Nerede Kullanılır?

  • Doğrulama ortamları: UVM ve klasik testbench'lerde generator, driver, monitor gibi bileşenler genellikle fork...join_none ile başlatılıp birbirinden bağımsız çalışır.
  • Timeout / watchdog kalıpları: join_any ile "ya iş biter ya da zaman aşımı olur" mantığı kurulur.
  • Stimulus üretimi: Birden fazla arabirimi aynı anda sürmek için paralel süreçler şarttır.

Ayrıca fork içine begin...end blokları koyarak iç içe (nested) yapılar oluşturabilirsiniz; bu durumda her begin...end bloğu kendi içinde sıralı çalışan tek bir paralel süreç olur.

Kaynak Kod

// =============================================================================
// GUN 4 - Konu 1: fork...join, fork...join_any, fork...join_none
// =============================================================================

module fork_join;
  // Gorev tanimlari
  task automatic work(string name, int delay);
    $display("  [%0t] %s basladi (sure: %0dns)", $time, name, delay);
    #delay;
    $display("  [%0t] %s bitti", $time, name);
  endtask

  initial begin
    $display("=== fork...join Turleri ===\n");

    // --- fork...join: TUM surecler bitene kadar bekle ---
    $display("--- fork...join (hepsi bitsin) ---");
    $display("  [%0t] Baslangic", $time);
    fork
      work("Gorev_A", 30);
      work("Gorev_B", 10);
      work("Gorev_C", 20);
    join
    $display("  [%0t] fork...join tamamlandi (en uzun: 30ns)\n", $time);

    // --- fork...join_any: HERHANGI BIRI bitince devam et ---
    $display("--- fork...join_any (ilk biten) ---");
    $display("  [%0t] Baslangic", $time);
    fork
      work("Gorev_D", 30);
      work("Gorev_E", 10);
      work("Gorev_F", 20);
    join_any
    $display("  [%0t] fork...join_any devam etti (ilk biten: 10ns)\n", $time);
    #20;  // Digerlerinin bitmesini bekle

    // --- fork...join_none: HICBIRINI bekleme ---
    $display("--- fork...join_none (beklemeden devam) ---");
    $display("  [%0t] Baslangic", $time);
    fork
      work("Gorev_G", 30);
      work("Gorev_H", 10);
      work("Gorev_I", 20);
    join_none
    $display("  [%0t] fork...join_none hemen devam etti", $time);
    #0;  // Delta cycle - fork'lar baslasin
    $display("  [%0t] #0 sonrasi (gorevler basladi)", $time);
    #35;  // Tumunun bitmesini bekle

    // --- Çok satırlı fork ---
    $display("\n--- Ic Ice Fork ---");
    $display("  [%0t] Baslangic", $time);
    fork
      begin  // Sirali blok 1
        work("Sirali_1a", 5);
        work("Sirali_1b", 5);  // 1a'dan sonra baslar
      end
      begin  // Sirali blok 2
        work("Sirali_2a", 3);
        work("Sirali_2b", 7);
      end
    join
    $display("  [%0t] Ic ice fork tamamlandi\n", $time);

    $display("=== fork...join Sonu ===");
    $finish;
  end
endmodule

Kodun Açıklaması

  • work görevi, automatic olarak tanımlanmıştır. Bu çok önemlidir: fork içinde aynı görev birden fazla kez paralel çağrıldığında, automatic her çağrı için ayrı bir yığın (stack) çerçevesi oluşturur ve name ile delay argümanları birbirine karışmaz.
  • fork...join bölümü: Gorev_A (30ns), Gorev_B (10ns) ve Gorev_C (20ns) aynı anda başlar. Ana süreç en uzun süren Gorev_A bitene kadar, yani 30ns boyunca bekler.
  • fork...join_any bölümü: Gorev_D, Gorev_E, Gorev_F paralel başlar. En kısa süren Gorev_E (10ns) bitince ana süreç hemen devam eder. Hemen ardından gelen #20; ifadesi, hâlâ arka planda koşan diğer görevlerin bitmesine zaman tanımak için konmuştur.
  • fork...join_none bölümü: Süreçler başlatılır ama ana süreç beklemeden bir sonraki satıra geçer. Buradaki #0; (delta cycle gecikmesi) ilginçtir: ana süreç bir an için yürütmeyi bırakır ve böylece fork ile başlatılan süreçlerin ilk satırları çalışma fırsatı bulur. Sonraki #35; ise tüm görevlerin tamamlanmasını bekler.
  • İç içe fork: Bloğun içinde iki ayrı begin...end vardır. Her biri kendi içinde sıralı çalışır: örneğin Sirali_1a bittikten sonra Sirali_1b başlar. Ancak iki begin...end bloğu birbirine göre paraleldir. join sayesinde ana süreç her iki bloğun da bitmesini bekler.

Önemli Noktalar

  • fork içinde paralel çağrılan görevler/fonksiyonlar automatic olmalıdır; aksi halde paylaşılan statik değişkenler yüzünden veriler birbirine karışır.
  • join_none kullandıktan hemen sonra başlatılan süreçlerin gerçekten çalışmaya başladığını görmek için #0 veya bir gecikme gerekebilir; aksi halde ana süreç onlara fırsat vermeden ilerleyebilir.
  • join_any sonrası arka planda hâlâ koşan süreçler vardır. Bunları açıkça beklemek (wait fork) veya iptal etmek (disable fork) gerekebilir, yoksa simülasyon beklenmedik biçimde uzayabilir.
  • Bir fork bloğunun içine begin...end koymak, o blok içindeki ifadeleri tek bir paralel sürece dönüştürür; sıralı ve paralel davranışı bu şekilde harmanlayabilirsiniz.
  • Simülasyon zamanı ($time) çıktısını izleyerek hangi sürecin ne zaman başlayıp bittiğini takip etmek, paralel davranışı anlamanın en sağlam yoludur.
  • Modül içindeki task/function'lar varsayılan olarak static'tir; sınıf metodları ise varsayılan olarak automatic'tir. Bu yüzden sınıf tabanlı testbench'lerde (Gün 6–7) automatic yazmanız gerekmez, ama modül içi yardımcı task'larda mutlaka yazmalısınız.
  • fork...join_none ile başlatılan alt süreçler, ana süreç bir bekleme noktasına (#, @, wait) gelmeden çalışmaya başlamaz. Koddaki #0 tam da bu yüzden vardır; ancak #0 kullanımı gerçek projelerde kaçınılması gereken bir kalıptır (delta-cycle yarışları yaratır). Tercih edilen yol, alt süreçlerin başladığını event veya mailbox ile açıkça haber vermesidir.
  • join_any ve join_none sonrasında ana süreç biterse (örneğin initial bloğu sonlanırsa) alt süreçler ölmez, çalışmaya devam eder. Simülasyonun bitmesi için $finish ya da tüm süreçlerin doğal olarak sonlanması gerekir.

Üç join Türü Bir Bakışta

join join_any join_none
Ana süreç ne zaman devam eder? Tüm alt süreçler bitince İlk alt süreç bitince Hemen (hiç beklemez)
Örnekteki devam zamanı 30 ns 10 ns 0 ns
Arka planda süreç kalır mı? Hayır Evet (bitmemiş olanlar) Evet (hepsi)
Tipik kullanım "Hepsi tamamlansın" (paralel reset dizileri, çoklu arayüz sürme) Timeout/watchdog, "ilk cevap gelen kazanır" Sürekli çalışan monitor/driver başlatma
Sonrasında sık kullanılan — disable fork wait fork

Benzetme: Üç arkadaşınıza görev verdiğinizi düşünün. join: "Hepiniz dönmeden masadan kalkmam." join_any: "İlk dönen haber versin, ben yola çıkayım; diğerleri arkadan gelsin." join_none: "Görevleri dağıttım, hiç beklemeden kendi işime bakıyorum."

Döngü İçinde fork: Klasik Tuzak

Aşağıdaki kod, join_none ile ilgili en meşhur hatayı içerir:

for (int i = 0; i < 3; i++)
  fork
    $display("Surec %0d", i);   // HATA: hepsi "Surec 3" basabilir!
  join_none

Alt süreçler ana süreç bloklanınca (döngü bittikten sonra) çalışmaya başlar ve o anda i çoktan 3 olmuştur. Çözüm, her sürece kendi kopyasını vermektir:

for (int i = 0; i < 3; i++)
  fork
    automatic int idx = i;      // Her surec icin ayri kopya
    $display("Surec %0d", idx);
  join_none

Bu kalıp (automatic yerel kopya) çoklu arayüz sürme, paralel agent başlatma gibi her yerde karşınıza çıkar.

Sık Yapılan Hatalar

  • Modül task'ını automatic yapmamak: Paralel çağrılarda argümanlar birbirine karışır; Gorev_A 10 ns beklerken Gorev_B'nin adı basılabilir.
  • Döngüde join_none ile döngü değişkenini paylaşmak: Yukarıdaki tuzak. automatic kopya alın.
  • join_any sonrası arka plan süreçlerini unutmak: Bir sonraki fork'un disable fork'u, önceki join_any'den kalan süreçleri de öldürebilir. Sonraki ders bunun çözümünü gösterir.
  • fork içindeki tek bir ifadenin birden fazla satır olmasını istemek: begin...end ile sarmalamadan yazılan her satır ayrı süreç olur; sıralı akış istiyorsanız begin...end zorunludur.
  • join_none sonrası alt süreçlerin hemen başladığını sanmak: Ana süreç bekleme noktasına gelmeden başlamazlar.

Kendinizi Deneyin

  1. work task'ından automatic kelimesini silip çalıştırın. Çıktıdaki isim/süre karışıklığını gözlemleyin ve nedenini açıklayın.
  2. join_none bölümündeki #0; satırını silin. "#0 sonrasi" mesajı ile görevlerin "basladi" mesajlarının sırası nasıl değişti?
  3. Yukarıdaki "döngü içinde fork" örneğini automatic int idx = i; olmadan ve olduğu şekilde çalıştırıp farkı gözlemleyin.
  4. İç içe fork bölümünü join_any ile bitirin. Ana süreç hangi zamanda devam etti? Hangi görevlerin "bitti" mesajı daha sonra geldi?
Hızlı Kontrol: fork içinde üç görev 30, 10 ve 20 ns sürüyorsa join_any sonrasında ana süreç hangi zamanda devam eder?

10 ns sonra; ilk biten görev ana süreci serbest bırakır. 30 ve 20 ns'lik görevler arka planda çalışmaya devam eder; onları beklemek için wait fork, iptal etmek için disable fork gerekir.