Siirry pääsisältöön

Asynchronous Updates

Avaa ChatGPT:ssä
25.02 Koe-aloite
Java API

Environment.runLater() -API tarjoaa mekanismin käyttöliittymän turvalliseen päivittämiseen taustasäikeistä webforJ-sovelluksissa. Tämä kokeellinen ominaisuus mahdollistaa asynkroniset toiminnot samalla pitäen käyttöliittymän muokkausten säikeiturvallisina.

Experimental feature
This feature is experimental and may change or be removed in a future release.

Ymmärtäminen säikeen mallista

webforJ noudattaa tiukkaa säikeiden mallia, jossa kaikki käyttöliittymätoiminnot on suoritettava Environment-säikeessä. Tämä rajoite johtuu siitä, että:

  1. webforJ API -rajoitukset: Taustalla oleva webforJ API sidotaan säikeeseen, joka loi istunnon.
  2. Komponenttien säikesuhteet: Käyttöliittymäkomponentit ylläpitävät tilaa, joka ei ole säikeen turvallista.
  3. Tapahtumien käsittely: Kaikki käyttöliittymä tapahtumat käsitellään peräkkäin yhdellä säikeellä.

Tämä yksisäikeinen malli estää kilpailutilanteet ja ylläpitää johdonmukaista tilaa kaikille käyttöliittymäkomponenteille, mutta luo haasteita integroitaessa asynkronisia, pitkiä laskentatehtäviä.

RunLater -API

Environment.runLater() -API tarjoaa kaksi menetelmää käyttöliittymän päivitysten aikatauluttamiseen:

Environment.java
// Aikatauluta tehtävä ilman palautusarvoa
public static PendingResult<Void> runLater(Runnable task)

// Aikatauluta tehtävä, joka palauttaa arvon
public static <T> PendingResult<T> runLater(Supplier<T> supplier)

Molemmat menetelmät palauttavat PendingResult -objektin, joka seuraa tehtävän valmistumista ja antaa pääsyn tulokseen tai mahdollisiin poikkeuksiin, jotka tapahtuivat.

Säikeen kontekstiin periytyminen

Automaattinen kontekstitietoisuus on kriittinen ominaisuus Environment.runLater() -metodissa. Kun Environment -säikeessä toimiva säie luo lapsisäikeitä, nämä lapset perivät automaattisesti kyvyn käyttää runLater() -metodia.

Kuinka periytyminen toimii

Mikä tahansa säie, joka on luotu Environment -säikeessä, pääsee automaattisesti siihen Environment-kontekstiin. Tämä periytyminen tapahtuu automaattisesti, joten konteksin siirtämistä tai mitään konfigurointia ei tarvita.

@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();

public DataView() {
// Tämä säie saa Environment-kontekstin

// Lapsisäikeet perivät kontekstin automaattisesti
executor.submit(() -> {
String data = fetchRemoteData();

// Voi käyttää runLateria, koska konteksti on peritty
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}

Säikeet ilman kontekstia

Säikeet, jotka on luotu Environment-kontekstin ulkopuolella, eivät voi käyttää runLater() -metodia, ja ne heittävät IllegalStateException -poikkeuksen:

// Staattinen alustaja - ei Environment-kontekstia
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Heittää IllegalStateException
}).start();
}

// Järjestelmäajurit - ei Environment-kontekstia
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Heittää IllegalStateException
}
}, 1000);

// Ulkoiset kirjastosäikeet - ei Environment-kontekstia
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Heittää IllegalStateException
});

Suoritus käyttäytyminen

runLater() -metodin suoritus käyttäytyminen riippuu siitä, mikä säie sitä kutsuu:

Käyttöliittymästä

Kun kutsutaan Environment -säikeeltä itseltään, tehtävät suoritetaan synkronisesti ja heti:

button.onClick(e -> {
System.out.println("Ennen: " + Thread.currentThread().getName());

PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Sisällä: " + Thread.currentThread().getName());
return "valmis";
});

System.out.println("Jälkeen: " + result.isDone()); // true
});

Tämän synkronisen käyttäytymisen avulla käyttöliittymän päivitykset tapahtuvat heti tapahtumakäsittelijöistä, eikä aiheuta turhaa jonottamista.

Taustasäikeistä

Kun kutsutaan taustasäikeestä, tehtävät jonotetaan asynkronista suorittamista varten:

@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Tämä suoritetaan ForkJoinPool säikeessä
System.out.println("Tausta: " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(() -> {
// Tämä suoritetaan Environment-säikeessä
System.out.println("Käyttöliittymän päivitys: " + Thread.currentThread().getName());
statusLabel.setText("Käsittely valmis");
});

// result.isDone() olisi false täällä
// Tehtävä on jonossa ja suoritetaan asynkronisesti
});
}

webforJ käsittelee taustasäikeistä lähetetyt tehtävät tiukassa FIFO-järjestyksessä, säilyttäen operaatioiden järjestyksen, vaikka ne olisi lähetetty useilta säikeiltä samanaikaisesti. Tämän järjestyksen takuurajaamisella käyttöliittymän päivitykset toteutuvat täsmälleen siinä järjestyksessä, jossa ne ovat lähetetty. Joten jos säie A lähettää tehtävän 1, ja sitten säie B lähettää tehtävän 2, tehtävä 1 suoritetaan aina ennen tehtävää 2 käyttöliittymässä. Tehtävien käsittely FIFO-järjestyksessä estää epäjohdonmukaisuudet käyttöliittymässä.

Tehtävän peruuttaminen

PendingResult -objekti, joka palautetaan Environment.runLater() -metodista, tukee peruuttamista, jolloin voit estää jonossa olevien tehtävien suorittamisen. Peruuttamalla odottavat tehtävät, voit välttää muistivuotoja ja estää pitkäkestoisia operaatioita päivittämästä käyttöliittymää, kun niitä ei enää tarvita.

Perusperuutus

PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});

// Peruuta, jos ei ole vielä suoritettu
if (!result.isDone()) {
result.cancel();
}

Useiden päivitysten hallinta

Pitkäkestoisia toimintoja, joissa on tiheitä käyttöliittymäpäivityksiä, suorittaessasi seuraa kaikkia odottavia tuloksia:

public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;

public void startTask() {
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;

final int progress = i;
PendingResult<Void> update = Environment.runLater(() -> {
progressBar.setValue(progress);
});

// Seuraa mahdollista peruuttamista
pendingUpdates.add(update);

Thread.sleep(100);
}
});
}

public void cancelTask() {
isCancelled = true;

// Peru kaikki odottavat käyttöliittymäpäivitykset
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Komponentin elinkaaren hallinta

Kun komponentteja tuhotaan (esim. navigoinnin aikana), peruuta kaikki odottavat päivitykset estääksesi muistivuotoja:

@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();

@Override
protected void onDestroy() {
super.onDestroy();

// Peruuta kaikki odottavat päivitykset estääksesi muistivuotoja
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Suunnittelupohdinnat

  1. Kontekstivaatimus: Säikeiden on oltava perineet Environment-konteksti. Ulkoiset kirjastosäikeet, järjestelmäajurit ja staattiset alustajat eivät voi käyttää tätä APIa.

  2. Muistivuotojen estäminen: Seuraa aina PendingResult -objekteja komponentin elinkaarimenetelmissä. Jonotetut lambdat kaappaavat viittauksia käyttöliittymäkomponentteihin, estäen roskakeräyksen, jos niitä ei peruuteta.

  3. FIFO-suoritus: Kaikki tehtävät suoritetaan tiukassa FIFO-järjestyksessä merkityksestä riippumatta. Prioriteettijärjestelmää ei ole.

  4. Peruuttamisen rajoitukset: Peruuttaminen estää vain jonossa olevien tehtävien suorittamisen. Jo suorittavat tehtävät valmistuvat normaalisti.

Täydellinen tapaustutkimus: LongTaskView

Seuraava on täydellinen, tuotantovalmiiksi toteutus, joka osoittaa kaikki parhaat käytännöt asynkronisille käyttöliittymäpäivityksille:

LongTaskView.java
  startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Aloitetaan taustatehtävä...");
progressBar.setValue(0);
resultField.setValue("");

// Nollaa peruutettu lippu ja tyhjennä aikaisemmat odottavat päivitykset
isCancelled = false;
pendingUIUpdates.clear();

// Käynnistä taustatehtävä erikseen suorittajalta
// Huom: cancel(true) keskeyttää säikeen, mistä Thread.sleep() heittää
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simuloi pitkään tehtävää 100 vaihetta
for (int i = 0; i <= 100; i++) {
// Tarkista, onko peruutettu
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("Tehtävä peruutettu!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Tehtävää on peruutettu", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // Yhteensä 10 sekuntia
} catch (InterruptedException e) {
// Säie keskeytettiin - poistu heti
Thread.currentThread().interrupt(); // Palauta keskeytysohje
return;
}

// Tee laskentaa (deterministinen demolle)
// Tuottaa arvoja, jotka vaihtelevat 0 ja 1 välillä
result += Math.sin(i) * 0.5 + 0.5;

// Päivitä edistystä taustasäikeestä
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("Käsitellään... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Viimeinen päivitys tuloksella (tämä koodi saavutetaan vain, jos tehtävä päättyi ilman
// peruuttamista)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("Tehtävä valmis!");
resultField.setValue("Tulos: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Taustatehtävä on valmis!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

Tapaustutkimuksen analyysi

Tämä toteutus osoittaa useita kriittisiä malleja:

1. Säiettä hallinta

private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
  • Käyttää yhden säikeen suorittajaa estämään resurssien uupumista
  • Luo daemon-säikeitä, jotka eivät estä JVM:n sammutusta

2. Odottavien päivitysten seuraaminen

private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();

Jokainen Environment.runLater() -kutsu seurataan, mikä mahdollistaa:

  • Peruuttamisen, kun käyttäjä napsauttaa peruuta
  • Muistivuotojen estämisen onDestroy():ssa
  • Oikean puhdistuksen komponentin elinkaaren aikana

3. Yhteistyöperuuttaminen

private volatile boolean isCancelled = false;

Taustasäie tarkistaa tämän lipun jokaisessa iteraatiossa, mikä mahdollistaa:

  • Välittömän reagoinnin peruuttamiseen
  • Siistin poistumisen silmukasta
  • Lisäämällä käyttöliittymän päivityksiä

4. Elinkaaren hallinta

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Uudelleenkäyttää peruuttamislogiikan
currentTask = null;
executor.shutdown();
}

Kriittinen muistivuotojen estämiseksi:

  • Peruuta kaikki odottavat käyttöliittymäpäivitykset
  • Keskeytä käynnissä olevat säikeet
  • Sammuta suorittaja

5. Käyttöliittymän reagointikyvyn testaus

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Klikkaus #" + count + " - käyttöliittymä on reagoiva!", Theme.GRAY);
});

Todistaa, että käyttöliittymän säie pysyy reagoivana taustatoimintojen aikana.