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 taustasuorittimilta webforJ-sovelluksissa. Tämä kokeellinen ominaisuus mahdollistaa asynkroniset toiminnot samalla kun säilyttää säikeiden turvallisuuden 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äikeen mallista

webforJ noudattaa tiukkaa säikeen mallia, jossa kaikki käyttöliittymän toiminnot on suoritettava Environment säikeessä. Tämä rajoitus on olemassa seuraavista syistä:

  1. webforJ API -rajoitukset: Peruksessa oleva webforJ API sitoo säikeen, joka loi istunnon
  2. Komponentin säikeen affiniteetti: Käyttöliittymäkomponentit ylläpitävät tilaa, joka ei ole säikeiden turvallinen
  3. Tapahtuman jako: Kaikki käyttöliittymän tapahtumat käsitellään järjestyksessä yhdellä säikeellä

Tämä yksisäikeinen malli estää kilpailutilanteet ja ylläpitää johdonmukaisen tilan kaikille käyttöliittymäkomponenteille, mutta se luo haasteita asynkronisten, pitkäkestoisten laskentatehtävien integroimisessa.

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, joka seuraa tehtävän suorittamista ja tarjoaa pääsyn tulokseen tai mahdollisiin poikkeuksiin.

Säikeen kontekstin perintä

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

Miten perintä toimii

Kaikilla säikeillä, jotka luodaan Environment-säikeen sisällä, on automaattisesti pääsy kyseiseen Environment-konseptiin. 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ämä säie on Environment-konteksti

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

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

Säikeet ilman kontekstia

Säikeet, jotka luodaan Environment-konteksti ulkopuolella, eivät voi käyttää runLater() ja heitetään IllegalStateException:

// Statistinen inicialisoija - ei Environment-konteksti
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Heittää IllegalStateException
}).start();
}

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

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

Suoritus käyttäytyminen

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

Käyttöliittymästä

Kun kutsutaan suoraan 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()); // true
});

Tämän synkronisen käyttäytymisen avulla käyttöliittymän päivitykset tapahtuvat heti tapahtumakäsittelijöistä, eivätkä ne vie turhaan jonoa.

Taustasäikeistä

Kun kutsutaan taustasäikeestä, tehtävät jonotetaan asynkroniseen suorittamiseen:

@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ässä
// Tehtävä on jonotettu ja suoritetaan asynkronisesti
});
}

webforJ käsittelee taustasäikeistä lähetettyjä tehtäviä tiukassa FIFO-järjestyksessä, säilyttäen operaatioiden sekvenssin, vaikka ne on lähetetty useista säikeistä samanaikaisesti. Tämän järjestyksen takia käyttöliittymän päivitykset suoritetaan täsmälleen siinä järjestyksessä, jossa ne on lähetetty. Jos siis 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än epäjärjestyksen.

Tehtävän peruutus

PendingResult, joka palautetaan Environment.runLater() -menetelmästä, tukee peruutusta, jolloin voit estää jonotettujen tehtävien suorittamisen. Peruuttamalla odottavat tehtävät voit välttää muistivuodot ja estää pitkäaikaiset toiminnot päivittämästä käyttöliittymää sen jälkeen, 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

Kun suoritat pitkäkestoisia toimintoja, joissa on usein käyttöliittymän päivityksiä, 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 mahdollisia peruutuksia
pendingUpdates.add(update);

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

public void cancelTask() {
isCancelled = true;

// Peruuta 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 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 muistivuotojen estämiseksi
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Suunnittelu näkökohtia

  1. Kontekstivaatimus: Säikeillä on oltava peritty Environment -konteksti. Ulkoiset kirjastosäikeet, järjestelmäajastimet ja staattiset inicialisoijat eivät voi käyttää tätä API:a.

  2. Muistivuotojen estäminen: Seuraa aina ja peruuta PendingResult -objekteja komponentin elinkaarimetodissa. Jonotetut lambdat vangitsevat viittauksia käyttöliittymäkomponentteihin, estäen roskat keräämisen, ellei niitä peruuteta.

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

  4. Peruutusrajoitukset: Peruuttaminen estää vain jonotettujen tehtävien suorittamisen. Jo suorittavat tehtävät suoritetaan normaalisti.

Täydellinen tapaustutkimus: LongTaskView

Seuraava on täydellinen, tuotantovalmiin toteutus, joka osoittaa kaikki parhaat käytännöt asynkronisessa käyttöliittymän päivityksessä:

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

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

// Käynnistä taustatehtävä eksplisiittisellä säikeellä
// Huom: cancel(true) keskeyttää säikeen, mikä saa Thread.sleep() -pytkimen heittämää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ä peruutettu", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

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

// Tee joitain laskelmia (deterministinen demo)
// Tuottaa lukuja 0 ja 1 välillä
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äsitellään... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Viimeinen päivitys tuloksella (tätä koodia kutsutaan vain, jos tehtävä suoritettu ilman
// peruutusta)
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ä valmis!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

private void cancelTask() {

Tapaustutkimuksen analyysi

Tämä toteutus osoittaa useita kriittisiä malleja:

1. Säiettä hallinnointi

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

2. Odottavien päivitysten seuraaminen

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

Jokainen Environment.runLater() -kutsu seurataan mahdollistamaan:

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

3. Yhteistyöperuutus

private volatile boolean isCancelled = false;

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

  • Välittömän reagoinnin peruutukseen
  • Puhdasta poistumista silmukasta
  • Lisäkäyttöliittymäpäivitysten estämistä

4. Elinkaaren hallinta

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

Kriittinen muistivuotojen estämiseksi:

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

5. Käyttöliittymän reagointikyvyn testaaminen

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

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