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 taustakelloista webforJ-sovelluksissa. Tämä kokeellinen ominaisuus mahdollistaa asynkroniset toiminnot samalla, kun se ylläpitää säikeen turvallisuutta käyttöliittymän muutoksille.

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

The webforj-handling-timers-and-async skill can schedule timers, debouncers, and async work safely on the UI thread. After installing the webforJ AI plugin, ask your assistant:

  • "Refresh this dashboard every 30 seconds."
  • "Add a search-as-you-type debouncer."
  • "Run this CPU-heavy work in the background and update the progress bar."

Ymmärtäminen säie mallista

webforJ noudattaa tiukkaa säiemallia, jossa kaikki käyttöliittymän toiminnot on suoritettava Environment-säikeessä. Tämä rajoitus on olemassa, koska:

  1. webforJ API:n rajoitukset: Taustalla oleva webforJ API sitoo säikeen, joka loi istunnon
  2. Komponentin säie affiniteetti: Käyttöliittymäkomponentit ylläpitävät tilaa, joka ei ole säie turvallinen
  3. Tapahtuman käsittely: Kaikki käyttöliittymän tapahtumat käsitellään peräkkäin yhdessä säikeessä

Tämä yksisäikeinen malli estää kilpailutilanteita ja ylläpitää johdonmukaista tilaa kaikille käyttöliittymäkomponenteille, mutta luo haasteita, kun integroidaan asynkronisia, pitkäkestoisia laskentatehtäviä.

RunLater API

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

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

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

Molemmat menetelmät palauttavat PendingResult, joka seuraa tehtävän valmistumista ja tarjoaa pääsyn tulokseen tai mahdollisiin poikkeuksiin.

Säie kontekstin perintä

Automaattinen kontekstin perintä on kriittinen ominaisuus Environment.runLater():ssa. Kun Environment:ssä toimiva säie luo lapsisäikeitä, nämä lapset perivät automaattisesti kyvyn käyttää runLater().

Kuinka perintä toimii

Mikä tahansa säie, joka luodaan Environment-säieestä, saa automaattisesti pääsyn kyseiseen Environment:iin. Tämä perintä tapahtuu automaattisesti, joten sinun ei tarvitse siirtää mitään kontekstia tai konfiguroida mitään.

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

public DataView() {
// Tässä säikeessä on Environment-konteksti

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

// Voit käyttää runLateria, koska konteksti perittiin
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() ja heittävät IllegalStateException:

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

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

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

Suoritus käyttäytyminen

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

Käyttäjäliittymäsäikeestä

Kun se kutsutaan Environment-säikeestä, 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()); // tosi
});

Tämän synkronisen käyttäytymisen myötä käyttöliittymän päivitykset tapahtumankäsittelijöistä soveltuvat heti eivätkä aiheuta tarpeettomia jonottamisoverheadia.

Taustasäikeistä

Kun se kutsutaan taustasäikeestä, tehtävät ovat jonossa asynkronista suorittamista varten:

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

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

// result.isDone() olisi tässä epätosi
// Tehtävä on jonossa ja suoritetaan asynkronisesti
});
}

webforJ käsittelee taustasäikeistä lähetettyjä tehtäviä tiukassa FIFO-järjestyksessä, säilyttäen toimintojen sekvenssin jopa useista säikeistä samanaikaisesti lähetettyinä. Tämän järjestyksen takuumalla käyttöliittymän päivitykset sovelletaan tarkalleen siinä järjestyksessä, jossa ne lähetettiin. 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ää käyttöliittymässä esiintyviä epäjohdonmukaisuuksia.

Tehtävän peruutus

PendingResult, jonka Environment.runLater() palauttaa, tukee peruutusta, jolloin voit estää jonossa olevien tehtävien suorittamisen. Peruuttamalla odottamat tehtävät voit välttää muistivuotoja ja estää pitkäkestoisten operaatioiden päivittämisen käyttöliittymään niiden ollessa enää tarpeettomia.

Perusperuutus

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

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

Useiden päivitysten hallinta

Kun suoritat pitkäkestoisia operaatioita useilla tavoin käyttöliittymän päivityksillä, 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 peruutusta
pendingUpdates.add(update);

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

public void cancelTask() {
isCancelled = true;

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

Komponentin elinkaaren hallinta

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

@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();
}
}

Suunnittelun huomioita

  1. Kontekstivaatimus: Säikeiden on oltava perineet Environment-konteksti. Ulkoisten kirjastojen säikeet, järjestelmäajastimet ja staattiset alustajat eivät voi käyttää tätä API:a.

  2. Muistivuotojen estäminen: Seuraa aina ja peruuta PendingResult-objekteja komponentin elinkaarimenetelmissä. Jonossa olevat lambda-lausunnot kaappaavat viittauksia käyttöliittymäkomponentteihin, estäen roskankeruun, jos niitä ei peruuteta.

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

  4. Peruutuksen rajoitukset: Peruuttaminen estää vain jonossa olevien tehtävien suorittamisen. Jo käynnissä olevat tehtävät päättyvät normaalisti.

Täydellinen tapaustutkimus: LongTaskView

Seuraava on täydellinen, tuotantovalmiiksi toteutus, joka osoittaa kaikki parhaita käytäntöjä asynkronisille käyttöliittymän päivityksille:

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

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

// Aloita taustatehtävä erillisellä suorittajalla
// Huom: cancel(true) keskeyttää säikeen, mikä aiheuttaa Thread.sleep():n heittävän
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simuloi pitkää tehtävää 100 vaiheessa
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ä peruutettiin", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 10 sekuntia yhteensä
} catch (InterruptedException e) {
// Säie keskeytettiin - poistu välittömästi
Thread.currentThread().interrupt(); // Palauta keskeytysstatus
return;
}

// Suorita laskentaa (deterministinen demo)
// Tuottaa arvoja välillä 0 ja 1
result += Math.sin(i) * 0.5 + 0.5;

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

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

Tapaustutkimuksen analyysi

Tämä toteutus osoittaa useita kriittisiä malleja:

1. Säietehtäviin liittyvien hallinta

private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
  • Käyttää yksisäietehtävää resurssien loppumisen estämiseksi
  • Luo daemon säikeitä, jotka eivät estä JVM:n sammuttamista

2. Odottavien päivitysten seuranta

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

Jokainen Environment.runLater()-kutsu seurataan mahdollistamaan:

  • Peruuttaminen, kun käyttäjä napsauttaa peruutusta
  • Muistivuotojen estäminen onDestroy():ssa
  • Asianmukainen puhdistus komponentin elinkaaren aikana

3. Yhteistyöperuutus

private volatile boolean isCancelled = false;

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

  • Välitön vastaus peruutukseen
  • Puhdas poistuminen silmukasta
  • Lisäkäyttöliittymäpäivitysten estäminen

4. Elinkaaren hallinta

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

Kriittinen muistivuotojen estämiseksi:

  • Peruuttamalla kaikki odottavat käyttöliittymän päivitykset
  • Keskeyttämällä käynnissä olevat säikeet
  • Samalla suorittajalla

5. Käyttöliittymän reagointikyvyn testaaminen

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

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