Evrensel bir yapıyı nasıl daha verimli hale getirebilirim?


16

Bir "evrensel yapı", sıralı bir nesne için doğrusallaştırılmasını sağlayan bir sarıcı sınıftır (eşzamanlı nesneler için güçlü bir tutarlılık koşulu). Örneğin, burada Java'da, [1] 'den, arabirimi tatmin eden WFQ(iş parçacıkları arasında yalnızca bir kerelik bir konsensüs gerektirir) ve bir Sequentialarabirimi varsayan, beklemesiz bir kuyruğun varlığını varsayan, uyarlanmış bir beklemesiz yapı :

public interface WFQ<T> // "FIFO" iteration
{
    int enqueue(T t); // returns the sequence number of t
    Iterable<T> iterateUntil(int max); // iterates until sequence max
}
public interface Sequential
{
    // Apply an invocation (method + arguments)
    // and get a response (return value + state)
    Response apply(Invocation i); 
}
public interface Factory<T> { T generate(); } // generate new default object
public interface Universal extends Sequential {}

public class SlowUniversal implements Universal
{
    Factory<? extends Sequential> generator;
    WFQ<Invocation> wfq = new WFQ<Invocation>();
    Universal(Factory<? extends Sequential> g) { generator = g; } 
    public Response apply(Invocation i)
    {
        int max = wfq.enqueue(i);
        Sequential s = generator.generate();
        for(Invocation invoc : wfq.iterateUntil(max))
            s.apply(invoc);
        return s.apply(i);
    }
}

Bu uygulama gerçekten yavaş olduğu için çok tatmin edici değil (her çağrıyı hatırlıyorsunuz ve her uygulamada tekrar oynatmanız gerekiyor - geçmiş boyutunda doğrusal çalışma süremiz var). Yeni bir çağrı uygularken bazı adımları kaydetmemizi sağlamak için WFQve Sequentialarayüzlerini (makul yollarla) genişletebilmemizin bir yolu var mı ?

Bekleme özelliğini kaybetmeden bunu daha verimli hale getirebilir miyiz (geçmiş boyutunda doğrusal çalışma zamanı değil, tercihen bellek kullanımı da azalır)?

açıklama

Bir "evrensel yapı", Sequentialarayüz tarafından genelleştirilmiş bir iş parçacığı güvensiz ancak iş parçacığı uyumlu nesneyi kabul eden [1] tarafından oluşturulduğundan emin olduğum bir terimdir . Beklemesiz bir kuyruk kullanarak, ilk yapı nesnenin iş parçacığı için güvenli ve doğrusallaştırılabilir bir sürümünü de sunar apply.

Bu yöntem verimsizdir, çünkü yöntem her bir yerel iş parçacığının temiz bir sayfadan başlatılması ve kaydedilmiş her işlemi gerçekleştirmesidir. Her durumda, bu, WFQtüm işlemlerin uygulanması gereken sırayı belirlemek için kullanarak etkin bir şekilde senkronizasyon gerçekleştirdiği için çalışır : her iş parçacığı çağrısı apply, aynı yerel Sequentialnesneyi, aynı Invocations sırasına uygulanmış olarak görür .

Benim sorum sıfırdan yeniden başlatmak zorunda kalmamak için "başlangıç ​​durumunu" güncelleyen bir arka plan temizleme işlemi (örn.) Getirip getiremeyeceğimizdir. Bu, bir başlangıç ​​işaretçisi olan bir atomik işaretçiye sahip olmak kadar basit değildir - bu tür yaklaşımlar, beklemesiz garantiyi kolayca kaybeder. Benim şüphem, sıraya dayalı başka bir yaklaşımın burada işe yarayabileceğidir.

Jargon:

  1. bekleme - iş parçacığı sayısından veya zamanlayıcının karar alma applyişleminden bağımsız olarak, söz konusu iş parçacığı için yürütülen sınırlı sayıda komutla sona erer.
  2. lock-free - yukarıdakiyle aynıdır, ancak applydiğer iş parçacıklarında sınırsız sayıda işlem yapılması durumunda sınırsız yürütme süresi olasılığını kabul eder . Tipik olarak, iyimser senkronizasyon şemaları bu kategoriye girer.
  3. engelleme - programlayıcının merhametinde verimlilik.

İstendiği gibi çalışan bir örnek (artık süresi dolmayacak bir sayfada)

[1] Herlihy ve Shavit, Çok İşlemcili Programlama Sanatı .


Soru 1, ancak "işe yarayan" ın sizin için ne anlama geldiğini bildiğimiz takdirde cevaplanabilir.
Robert Harvey

@RobertHarvey Düzelttim - "çalışması" için gereken tek şey sargının beklemeden olması ve tüm işlemlerin CopyableSequentialgeçerli olması - lineerleştirilebilirlik bunu takip etmelidir Sequential.
VF1

Bu soruda çok sayıda anlamlı kelime var, ancak bunları tam olarak neyi başarmaya çalıştığınızı anlamak için bir araya getirmeye çalışıyorum. Hangi sorunu çözmeye çalıştığınıza ve belki jargonu biraz incelttiğinize dair bir açıklama yapabilir misiniz?
JimmyJames

@JimmyJames Sorunun içinde "genişletilmiş bir yorum" yazdım. Temizlenecek başka bir Jargon varsa lütfen bana bildirin.
VF1

yorumun ilk paragrafında "iş parçacığı güvensiz ancak iş parçacığı uyumlu nesne" ve "nesnenin doğrusallaştırılabilir sürümü" diyorsunuz. Bununla ne demek istediğiniz belli değil, çünkü thread-safe ve linearizable sadece çalıştırılabilir talimatlarla gerçekten ilgilidir, ancak bunları veri olan nesneleri tanımlamak için kullanıyorsunuz. O tahmin çağırma (tanımlanmamış) etkili bir şekilde yöntem işaretçidir ve evreli olmadığını bir yöntem. İplik uyumlu ne anlama geldiğini bilmiyorum .
JimmyJames

Yanıtlar:


1

İşte bunun nasıl yapıldığına dair bir açıklama ve örnek. Net olmayan parçalar varsa bana bildirin.

Kaynak ile Gist

Evrensel

Başlatma:

İplik indeksleri atomik olarak artırılır. Bu bir AtomicIntegeradlandırılmış kullanılarak yönetilir nextIndex. Bu dizinler, bir ThreadLocalsonraki dizini nextIndexalıp arttırarak kendisini başlatan bir örnek aracılığıyla iş parçacıklarına atanır . Bu, her iş parçacığının dizini ilk kez alındığında gerçekleşir. ThreadLocalBu iş parçacığının son oluşturduğu diziyi izlemek için A oluşturulur. 0 olarak başlatıldı. Sıralı fabrika nesnesi başvurusu aktarılır ve saklanır. AtomicReferenceArrayBoyut olarak iki örnek oluşturulur n. Kuyruk nesnesi, Sequentialfabrika tarafından sağlanan başlangıç ​​durumuyla başlatılan her bir referansa atanır . nizin verilen maksimum iş parçacığı sayısıdır. Bu dizilerdeki her eleman karşılık gelen evre dizinine 'aittir'.

Uygulama yöntemi:

İlginç işi yapan yöntem budur. Aşağıdakileri yapar:

  • Bu çağrı için yeni bir düğüm oluştur: benim
  • Bu yeni düğümü, geçerli iş parçacığının dizinindeki anons dizisinde ayarla

Sonra sıralama döngüsü başlar. Mevcut çağrı dizilenene kadar devam edecektir:

  1. bu iş parçacığı tarafından oluşturulan son düğümün sırasını kullanarak anons dizisindeki bir düğümü bulun. Bu konuyla ilgili daha sonra.
  2. 2. adımda bir düğüm bulunursa, henüz dizilenmez, onunla devam edin, aksi takdirde sadece geçerli çağrıya odaklanın. Bu, çağrı başına yalnızca bir düğüme yardımcı olmaya çalışacaktır.
  3. 3. adımda seçilen düğüm ne olursa olsun, son sıralı düğümden sonra sıralamaya çalışın (diğer dişler karışabilir.) Başarı ne olursa olsun, geçerli iş parçacığı baş referansını döndürülen diziye ayarlayın decideNext()

Yukarıda açıklanan iç içe döngünün anahtarı decideNext()yöntemdir. Bunu anlamak için Node sınıfına bakmamız gerekiyor.

Düğüm sınıfı

Bu sınıf, çift bağlantılı bir listedeki düğümleri belirtir. Bu sınıfta çok fazla eylem yok. Yöntemlerin çoğu, oldukça açıklayıcı olması gereken basit geri alma yöntemleridir.

kuyruk yöntemi

bu, 0 dizisiyle özel bir düğüm örneği döndürür. Bir çağırma yerine geçene kadar yer tutucu görevi görür.

Özellikler ve başlatma

  • seq: -1 olarak başlatılmış sıra numarası (sıralanmamış anlamına gelir)
  • invocation: çağrılmasının değeri apply(). İnşaat üzerine ayarlayın.
  • next: AtomicReferenceileri bağlantı için. bir kez atandığında, bu asla değişmeyecek
  • previous: AtomicReferencesıralama sırasında atanan ve tarafından silinen geriye doğru bağlantı içintruncate()

Sonrakine Karar Ver

Bu yöntem, önemsiz mantığı olan Düğümde yalnızca bir tanesidir. Özetle, bir düğüm, bağlantılı listedeki bir sonraki düğüm olmaya aday olarak sunulur. compareAndSet()Yöntem 's referans boş olup olmadığını kontrol ve eğer öyleyse, adayın başvurusunu ayarlayacaktır. Referans zaten ayarlanmışsa, hiçbir şey yapmaz. Bu işlem atomiktir, bu yüzden aynı anda iki aday sunulursa, sadece bir aday seçilir. Bu, yalnızca bir düğümün bir sonraki düğümün seçileceğini garanti eder. Aday düğüm seçilirse, sırası bir sonraki değere ayarlanır ve önceki bağlantısı bu düğüme ayarlanır.

Evrensel sınıfa uygulama yöntemine geri dönme ...

Düğümümüzden veya decideNext()diziden bir düğümle son sıralı düğümü (işaretlendiğinde) çağırdıktan sonra, announceiki olası durum vardır: 1. Düğüm başarıyla dizildi 2. Başka bir iş parçacığı bu iş parçacığını önceden boşalttı.

Bir sonraki adım, bu çağırma için düğümün oluşturulup oluşturulmadığını kontrol etmektir. Bu, bu iş parçacığı başarıyla dizildi veya diziden diğer bir iş parçacığı announcediziden aldı ve bizim için dizildi çünkü olabilir. Sıralanmamışsa, işlem tekrarlanır. Aksi takdirde çağrı, bu iş parçacığının dizinindeki anons dizisini temizleyerek ve çağrının sonuç değerini döndürerek sona erer. Duyuru dizisi, düğümün çöp toplanmasını engelleyecek ve bu nedenle bağlı listedeki tüm düğümleri yığın üzerinde o noktadan canlı tutacak hiçbir düğüm referansı olmadığından emin olmak için temizlenir.

Değerlendirme yöntemi

Şimdi çağrının düğümü başarıyla dizildiğine göre çağrının değerlendirilmesi gerekir. Bunu yapmak için ilk adım, bundan önceki çağrıların değerlendirilmesini sağlamaktır. Eğer bu konu yoksa beklemeyecek, ancak hemen çalışacaktır.

EnsurePrior yöntemi

ensurePrior()Yöntem bağlantılı listedeki önceki düğümü kontrol ederek bu işi yapar. Durumu ayarlanmamışsa, önceki düğüm değerlendirilir. Bunun özyinelemeli düğüm. Önceki düğümden önceki düğüm değerlendirilmediyse, bu düğüm için değerlendirme vb.

Artık önceki düğümün bir durumu olduğu biliniyor, bu düğümü değerlendirebiliriz. Son düğüm alınır ve yerel bir değişkene atanır. Bu başvuru null ise, başka bir iş parçacığının bunu önlediği ve bu düğümü zaten değerlendirdiği anlamına gelir; ayarlıyoruz. Aksi takdirde, önceki düğümün durumu, Sequentialbu düğümün çağrılmasıyla birlikte nesnenin uygulama yöntemine geçirilir . Döndürülen durum düğümde ayarlanır ve truncate()yöntem artık çağrıldığında düğümden geriye doğru bağlantıyı temizleyerek çağrılır.

MoveForward yöntemi

İleri taşıma yöntemi, daha önce başka bir şeye işaret etmiyorlarsa tüm düğüm referanslarını bu düğüme taşımaya çalışır. Bu, bir iş parçacığının aramayı durdurması durumunda, başının artık gerekli olmayan bir düğüme referans tutmamasını sağlamak içindir. compareAndSet()O ele geçirildi yana bazı diğer parçacığı bunu değişmedi eğer yöntem emin biz sadece düğüm güncelleştirme yapacaktır.

Diziyi duyurun ve yardım edin

Bu yaklaşımı basitçe kilitlemenin aksine beklemeden yapmanın anahtarı, iş parçacığı zamanlayıcısının her iş parçasına ihtiyaç duyduğunda öncelik vereceğini varsayamayacağımızdır. Her iş parçacığı kendi düğümlerini sıralamaya çalışırsa, bir iş parçacığının yük altında sürekli olarak boşaltılması mümkündür. Bu olasılığı hesaba katmak için, her bir iş parçacığı önce sıralanamayan diğer iş parçacıklarına "yardım etmeye" çalışacaktır.

Temel fikir, her iş parçacığı başarıyla düğümler oluşturdukça, atanan dizilerin monoton olarak artmasıdır. Bir iş parçacığı veya iş parçacığı sürekli olarak başka bir iş parçacığının önüne geçiyorsa, announcedizideki sıralanmamış düğümleri bulmak için kullanılan dizin ileri doğru hareket eder. Belirli bir düğümü sıralamaya çalışan her iş parçacığı başka bir iş parçacığı tarafından sürekli olarak kullanılsa bile, sonunda tüm iş parçacıkları bu düğümü sıralamaya çalışacaktır. Örneklemek gerekirse, üç iş parçacığı içeren bir örnek oluşturacağız.

Başlangıç ​​noktasında, üç iş parçacığının baş ve anons öğelerinin hepsi taildüğüme yönlendirilir. lastSequenceHer bir iplik için 0'dır.

Bu noktada, Konu 1 bir çağırma ile yürütülür. Duyuru dizisini, şu anda dizine eklenmesi planlanan düğüm olan son sırası (sıfır) olup olmadığını denetler. Düğümü sıralar ve lastSequence1 olarak ayarlanır.

İş parçacığı 2 şimdi bir çağrı ile yürütülür, anons dizisini son sırasında (sıfır) kontrol eder ve yardıma ihtiyaç duymadığını görür ve bu nedenle çağrısını sıralamaya çalışır. Başarılı olur ve şimdi lastSequence2'ye ayarlanmıştır.

İş parçacığı 3 şimdi yürütülür ve aynı zamanda adresindeki düğümün announce[0]zaten dizildiğini ve kendi çağrışımını dizildiğini görür . Bu oluyor lastSequenceşimdi 3'e ayarlanmıştır.

Şimdi Thread 1 tekrar çağrıldı. Dizin 1'deki anons dizisini kontrol eder ve dizinin zaten sıralandığını bulur. Aynı zamanda, Konu 2 çağrılır. Dizin 2'deki anons dizisini kontrol eder ve dizinin zaten sıralandığını bulur. Hem İş Parçacığı 1 hem de İş parçacığı 2 artık kendi düğümlerini sıralamaya çalışır. Thread 2 kazanır ve onu çağrıştırır. Bu lastSequence4 olarak ayarlanmıştır. Bu arada, üç iplik çağrılmıştır. Dizini kontrol eder lastSequence(mod 3) ve adresindeki düğümün announce[0]dizilenmediğini bulur . İş parçacığı 2 ikinci iş parçacığıyla aynı anda çağrılır . Konu 1İş parçacığı 2announce[1] tarafından yeni oluşturulan düğüm olan sıralanmamış bir çağrı bulur . Konu 2'nin çağrılmasını sıralamaya çalışır ve başarılı olur. İş parçacığı 2 kendi düğümünü bulur ve sıralanır. Bu set en öyle 5. Konu 3 sonra yerleştirilen iplik 1 düğüm o çağrılır ve buluntular olduğunu hala sıralandı değildir ve girişimleri bunu. Bu arada Thread 2 de çağrıldı ve Thread 3'ü önceden boşaltır. Düğümünü sıralar ve 6'ya ayarlar .announce[1]lastSequenceannounce[0]lastSequence

Kötü İplik 1 . İş parçacığı 3 diziyi sıralamaya çalışsa da , her iki iş parçacığı da zamanlayıcı tarafından sürekli olarak engellendi. Ama bu noktada. Konu 2 şimdi de announce[0](6 mod 3) 'ü işaret ediyor . Her üç evre de aynı çağrıyı sıralamaya çalışacak şekilde ayarlanmıştır. Hangi iş parçacığı başarılı olursa olsun, sıralanacak sonraki düğüm İş Parçacığı 1'in beklemedeki çağrılması, yani başvurulan düğüm olacaktır announce[0].

Bu kaçınılmaz. İş parçacıklarının önceden boşaltılması için, diğer iş parçacıklarının sıralama düğümleri olması gerekir ve bu şekilde sürekli lastSequenceilerleyeceklerdir. Belirli bir iş parçacığının düğümü sürekli olarak sıralanmazsa, sonunda tüm iş parçacıkları anons dizisindeki dizinine işaret eder. Yardım etmeye çalıştığı düğüm sıralanana kadar hiçbir iş parçacığı başka bir şey yapmaz, en kötü durum senaryosu tüm iş parçacıklarının aynı sıralanmamış düğüme işaret etmesidir. Bu nedenle, herhangi bir çağrıyı sıralamak için gereken süre girdinin boyutunun değil, iş parçacığı sayısının bir fonksiyonudur.


Bazı kod alıntılarını yapıştırmak ister misiniz? Bir çok şey (lockfree bağlantılı liste gibi) basitçe böyle ifade edilebilir mi? Çok fazla ayrıntı olduğunda cevabınızı bir bütün olarak anlamak biraz zor. Her durumda, bu umut verici görünüyor, kesinlikle sağladığı garantileri araştırmak istiyorum.
VF1

Bu kesinlikle geçerli bir kilitsiz uygulama gibi görünüyor, ancak endişelendiğim temel sorun eksik. Doğrusallaştırılabilirlik şartı, bağlantılı liste uygulaması durumunda,previousnext durumunda geçerli olması için ve işaretçisine . Geçerli bir geçmişi beklemeden sürdürmek ve oluşturmak zor görünüyor.
VF1

@ VF1 Hangi sorunun çözülmediğinden emin değilim. Yorumun geri kalanında bahsettiğiniz her şey, verebileceğim örnekte verdiğim örnekte ele alınmıştır.
JimmyJames

Sen vazgeçtim bekleme gerektirmeyen özelliği.
VF1

@ VF1 Nasıl buluyorsunuz?
JimmyJames

0

Önceki cevabım soruyu tam olarak doğru cevaplamıyor ama OP'nin yararlı gördüğü gibi, onu olduğu gibi bırakacağım. Sorudaki bağlantıdaki koda dayanarak, benim girişimim. Ben bu konuda sadece temel test yaptım ama ortalamaları düzgün hesaplamak gibi görünüyor. Bu düzgün beklemek-ücretsiz olup olmadığı hakkında geri bildirim.

NOT : Evrensel arayüzü kaldırdım ve bir sınıf haline getirdim. Evrensel olmak Sıralılıklardan oluşmanın yanı sıra bir olmak da gereksiz bir komplikasyon gibi görünüyor ama bir şeyleri kaçırıyor olabilirim. Ortalama sınıfta, durum değişkenini işaretledim volatile. Kodun çalışması için bu gerekli değildir. Muhafazakar olmak (iş parçacığı ile iyi bir fikir) ve her bir iş parçacığının tüm hesaplamaları yapmasını (bir kez) önlemek.

Sıralı ve Fabrika

public interface Sequential<E, S, R>
{ 
  R apply(S priorState);

  S state();

  default boolean isApplied()
  {
    return state() != null;
  }
}

public interface Factory<E, S, R>
{
   S initial();

   Sequential<E, S, R> generate(E input);
}

Evrensel

import java.util.concurrent.ConcurrentLinkedQueue;

public class Universal<I, S, R> 
{
  private final Factory<I, S, R> generator;
  private final ConcurrentLinkedQueue<Sequential<I, S, R>> wfq = new ConcurrentLinkedQueue<>();
  private final ThreadLocal<Sequential<I, S, R>> last = new ThreadLocal<>();

  public Universal(Factory<I, S, R> g)
  { 
    generator = g;
  }

  public R apply(I invocation)
  {
    Sequential<I, S, R> newSequential = generator.generate(invocation);
    wfq.add(newSequential);

    Sequential<I, S, R> last = null;
    S prior = generator.initial(); 

    for (Sequential<I, S, R> i : wfq) {
      if (!i.isApplied() || newSequential == i) {
        R r = i.apply(prior);

        if (i == newSequential) {
          wfq.remove(last.get());
          last.set(newSequential);

          return r;
        }
      }

      prior = i.state();
    }

    throw new IllegalStateException("Houston, we have a problem");
  }
}

Ortalama

public class Average implements Sequential<Integer, Average.State, Double>
{
  private final Integer invocation;
  private volatile State state;

  private Average(Integer invocation)
  {
    this.invocation = invocation;
  }

  @Override
  public Double apply(State prior)
  {
    System.out.println(Thread.currentThread() + " " + invocation + " prior " + prior);

    state = prior.add(invocation);

    return ((double) state.sum)/ state.count;
  }

  @Override
  public State state()
  {
    return state;
  }

  public static class AverageFactory implements Factory<Integer, State, Double> 
  {
    @Override
    public State initial()
    {
      return new State(0, 0);
    }

    @Override
    public Average generate(Integer i)
    {
      return new Average(i);
    }
  }

  public static class State
  {
    private final int sum;
    private final int count;

    private State(int sum, int count)
    {
      this.sum = sum;
      this.count = count;
    }

    State add(int value)
    {
      return new State(sum + value, count + 1);
    }

    @Override
    public String toString()
    {
      return sum + " / " + count;
    }
  }
}

Demo kodu

private static final int THREADS = 10;
private static final int SIZE = 50;

public static void main(String... args)
{
  Average.AverageFactory factory = new Average.AverageFactory();

  Universal<Integer, Average.State, Double> universal = new Universal<>(factory);

  for (int i = 0; i < THREADS; i++)
  {
    new Thread(new Test(i * SIZE, universal)).start();
  }
}

static class Test implements Runnable
{
  final int start;
  final Universal<Integer, Average.State, Double> universal;

  Test(int start, Universal<Integer, Average.State, Double> universal)
  {
    this.start = start;
    this.universal = universal;
  }

  @Override
  public void run()
  {
    for (int i = start; i < start + SIZE; i++)
    {
      System.out.println(Thread.currentThread() + " " + i);

      System.out.println(System.nanoTime() + " " + Thread.currentThread() + " " + i + " result " + universal.apply(i));
    }
  }
}

Burada yayınlarken kodda bazı düzenlemeler yaptım. Tamam olmalı ama bununla ilgili sorunlarınız varsa bana bildirin.


Benim için diğer cevabınızı devam ettirmek zorunda değilsiniz (daha önce, ilgili herhangi bir sonuca varmak için sorumu güncelledim). Ne yazık ki, bu cevap da soruyu cevaplamıyor, çünkü içindeki hafızanın hiçbirini serbest bırakmıyor wfq, bu yüzden hala tüm tarih boyunca geçiş yapmanız gerekiyor - çalışma süresi sabit bir faktör hariç iyileşmedi.
VF1

@ Vf1 Hesaplanıp hesaplanmadığını kontrol etmek için listenin tamamında gezinmek için geçen süre, her bir hesaplamaya kıyasla minik olacaktır. Önceki durumlar gerekli olmadığından, başlangıç ​​durumlarının kaldırılması mümkün olmalıdır. Test etmek zor ve özelleştirilmiş bir koleksiyonun kullanılması gerekebilir, ancak küçük bir değişiklik ekledim.
JimmyJames

@ VF1 Temel üstünkörü testlerle çalışıyor gibi görünen bir uygulamaya güncellendi. Güvenli olduğundan emin değilim ama başımın üstünden, evrensel onunla çalışan ipliklerin farkındaysa, her bir ipliği takip edebilir ve tüm iplikler güvenli bir şekilde geçtikten sonra elemanları kaldırabilir.
JimmyJames

@ VF1 ConcurrentLinkedQueue koduna baktığımızda, teklif yönteminin diğer cevabı beklemeden yaptığını iddia ettiğiniz gibi bir döngüye sahiptir. "Kayıp CAS yarışı başka bir iş parçacığına; sonraki tekrar okuyun"
yorumuna bakın

"Başlangıç ​​durumlarını kaldırmak mümkün olmalıdır" - tam olarak. Öyle olmalı , ancak bekleme özgürlüğünü kaybeden kodu ustaca tanıtmak kolaydır. Bir iş parçacığı izleme şeması işe yarayabilir. Son olarak, CLQ kaynağına erişimim yok, bağlantı kurmayı düşünür müsünüz?
VF1
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.