Il metodo di limitazione chiama le richieste M in N secondi


137

Ho bisogno di un componente / classe che limiti l'esecuzione di un metodo al massimo M chiamate in N secondi (o ms o nanos, non importa).

In altre parole, devo assicurarmi che il mio metodo venga eseguito non più di M volte in una finestra scorrevole di N secondi.

Se non conosci la classe esistente, sentiti libero di pubblicare le tue soluzioni / idee su come implementarlo.



3
Ci sono alcuni grandi risposte a questo problema a stackoverflow.com/questions/667508/...
skaffman

> Devo assicurarmi che il mio metodo sia> eseguito non più di M volte in una> finestra scorrevole di N secondi. Di recente ho scritto un post sul blog su come eseguire questa operazione in .NET. Potresti essere in grado di creare qualcosa di simile in Java. Limitazione della tariffa migliore in .NET
Jack Leitch,

La domanda originale sembra molto simile al problema risolto in questo post del blog: [Throttler asincrono multicanale Java] ( cordinc.com/blog/2010/04/java-multichannel-asynchronous.html ). Per una frequenza di M chiamate in N secondi, il throttler discusso in questo blog garantisce che qualsiasi intervallo di lunghezza N sulla timeline non conterrà più di M chiamate.
Hbf,

Risposte:


81

Userei un ring buffer di timestamp con una dimensione fissa di M. Ogni volta che viene chiamato il metodo, controlli la voce più vecchia e se in passato è inferiore a N secondi, esegui e aggiungi un'altra voce, altrimenti dormi per la differenza di tempo.


4
Bello. Proprio quello di cui ho bisogno. I tentativi rapidi mostrano ~ 10 righe per implementare questo e il minimo ingombro di memoria. Devo solo pensare alla sicurezza del thread e alla messa in coda delle richieste in arrivo.
vtrubnikov,

5
Ecco perché usi DelayQueue di java.util.concurrent. Previene il problema di più thread che agiscono sulla stessa voce.
Erickson,

5
Per un caso multi-thread, l'approccio del token bucket potrebbe essere una scelta migliore, credo.
Michael Borgwardt,

1
Sai come viene chiamato questo algoritmo se ha qualche nome?
Vlado Pandžić,

80

Quello che ha funzionato per me era Google Guava RateLimiter .

// Allow one request per second
private RateLimiter throttle = RateLimiter.create(1.0);

private void someMethod() {
    throttle.acquire();
    // Do something
}

19
Non consiglierei questa soluzione poiché Guava RateLimiter bloccherà il thread e questo esaurirà facilmente il pool di thread.
kaviddiss,

18
@kaviddiss se non vuoi bloccare, usatryAquire()
slf

7
Il problema con l'implementazione attuale di RateLimiter (almeno per me) è che non consente periodi di tempo superiori a 1 secondo e pertanto tariffe di 1 esempio al minuto.
Giovanni B,

4
@John B Per quanto ne so, puoi raggiungere 1 richiesta al minuto con RateLimiter utilizzando RateLimiter.create (60.0) + rateLimiter.acquire (60)
divideByZero

2
@radiantRazor Ratelimiter.create (1.0 / 60) e acquisizione () ottiene 1 chiamata al minuto.
bizentass,

30

In termini concreti, dovresti essere in grado di implementarlo con a DelayQueue. Inizializza la coda con M Delayedistanze con il loro ritardo inizialmente impostato su zero. Quando arrivano richieste al metodo, takeun token, che provoca il blocco del metodo fino a quando non viene soddisfatto il requisito di limitazione. Quando è stato preso addun token , un nuovo token nella coda con un ritardo di N.


1
Sì, questo farebbe il trucco. Ma non mi piace particolarmente DelayQueue perché sta usando (tramite PriortyQueue) un hash binario bilanciato (il che significa molti confronti su offere possibile crescita dell'array), ed è tutto piuttosto pesante per me. Immagino che per altri questo potrebbe andare perfettamente bene.
vtrubnikov,

5
In realtà, in questa applicazione, poiché il nuovo elemento aggiunto all'heap sarà quasi sempre l'elemento massimo nell'heap (ovvero, avrà il ritardo più lungo), di solito è necessario un confronto per ogni aggiunta. Inoltre, l'array non crescerà mai se l'algoritmo è implementato correttamente, poiché un elemento viene aggiunto solo dopo aver preso un elemento.
Erickson,

3
Ho trovato questo utile anche nei casi in cui non si desidera che le richieste si verifichino a grandi raffiche mantenendo la dimensione M e ritardando N relativamente piccolo in ordine di millisecondi. per esempio. M = 5, N = 20ms fornirebbe un put through di 250 / sec che scoppierà in una dimensione di 5.
FUD

Questa scala per un milione di giri / min e quando sono consentite richieste simultanee? Avrei bisogno di aggiungere un milione di elementi in ritardo. Anche i casi angolari avranno un alto grado di latenza, caso in cui più thread chiamano poll () e si bloccherebbero ogni volta.
Aditya Joshee,

@AdityaJoshee Non l'ho confrontato, ma se avrò del tempo proverò a capire il sovraccarico. Una cosa da notare è che non è necessario 1 milione di token che scadono in 1 secondo. Potresti avere 100 token che scadono in 10 millisecondi, 10 token che scadono in millisecondi, ecc. Questo effettivamente costringe la velocità istantanea ad essere più vicina alla frequenza media, livellando i picchi, che possono causare backup sul client, ma questa è una conseguenza naturale di limitazione della velocità. Tuttavia, 1 milione di RPM non sembra quasi limitare. Se riesci a spiegare il tuo caso d'uso, potrei avere idee migliori.
Erickson,

21

Leggi l' algoritmo del bucket token . Fondamentalmente, hai un secchio con token. Ogni volta che si esegue il metodo, si prende un token. Se non ci sono più token, blocchi fino a quando non ne ottieni uno. Nel frattempo, c'è qualche attore esterno che riempie i token a un intervallo fisso.

Non sono a conoscenza di una biblioteca per fare questo (o qualcosa di simile). È possibile scrivere questa logica nel codice o utilizzare AspectJ per aggiungere il comportamento.


3
Grazie per il suggerimento, algo interessante. Ma non è esattamente quello di cui ho bisogno. Ad esempio, devo limitare l'esecuzione a 5 chiamate al secondo. Se uso il token bucket e arrivano 10 richieste contemporaneamente, le prime 5 chiamate prenderebbero tutti i token disponibili e verranno eseguite momentaneamente, mentre le restanti 5 chiamate verranno eseguite a intervalli fissi di 1/5 s. In tale situazione, ho bisogno di rimanere 5 chiamate da eseguire in un singolo burst solo dopo 1 secondo di passaggio.
vtrubnikov,

5
E se aggiungessi 5 token al secchio ogni secondo (o 5 - (5 rimanenti) invece di 1 ogni 1/5 di secondo?
Kevin

@Kevin no, questo non mi darebbe l'effetto 'finestra scorrevole'
vtrubnikov,

2
@valery sì, lo sarebbe. (Ricorda di limitare i token a M però)
nn

non c'è bisogno di un "attore esterno". Tutto può essere eseguito a thread singolo se si mantengono i metadati sui tempi delle richieste.
Marsellus Wallace,

8

Se hai bisogno di un limitatore di velocità per finestre scorrevoli basato su Java che funzionerà su un sistema distribuito, potresti dare un'occhiata al progetto https://github.com/mokies/ratelimitj .

Una configurazione supportata da Redis, per limitare le richieste da IP a 50 al minuto sarebbe simile a questa:

import com.lambdaworks.redis.RedisClient;
import es.moki.ratelimitj.core.LimitRule;

RedisClient client = RedisClient.create("redis://localhost");
Set<LimitRule> rules = Collections.singleton(LimitRule.of(1, TimeUnit.MINUTES, 50)); // 50 request per minute, per key
RedisRateLimit requestRateLimiter = new RedisRateLimit(client, rules);

boolean overLimit = requestRateLimiter.overLimit("ip:127.0.0.2");

Vedi https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redis per ulteriori dettagli sulla configurazione di Redis.


5

Questo dipende dall'applicazione.

Immaginate il caso in cui più thread vogliono un token per fare un po ' di azione a livello globale tasso-limitata con nessun scoppio consentito (cioè si vuole limitare 10 azioni ogni 10 secondi, ma non si vuole 10 azioni per accadere nel primo secondo e poi rimane 9 secondi fermati).

DelayedQueue presenta uno svantaggio: l'ordine in cui i thread richiedono i token potrebbe non essere l'ordine in cui ottengono la loro richiesta soddisfatta. Se più thread sono bloccati in attesa di un token, non è chiaro quale prenderà il prossimo token disponibile. Potrei anche avere discussioni che aspettano per sempre, dal mio punto di vista.

Una soluzione è avere un intervallo di tempo minimo tra due azioni consecutive e intraprendere azioni nello stesso ordine in cui sono state richieste.

Ecco un'implementazione:

public class LeakyBucket {
    protected float maxRate;
    protected long minTime;
    //holds time of last action (past or future!)
    protected long lastSchedAction = System.currentTimeMillis();

    public LeakyBucket(float maxRate) throws Exception {
        if(maxRate <= 0.0f) {
            throw new Exception("Invalid rate");
        }
        this.maxRate = maxRate;
        this.minTime = (long)(1000.0f / maxRate);
    }

    public void consume() throws InterruptedException {
        long curTime = System.currentTimeMillis();
        long timeLeft;

        //calculate when can we do the action
        synchronized(this) {
            timeLeft = lastSchedAction + minTime - curTime;
            if(timeLeft > 0) {
                lastSchedAction += minTime;
            }
            else {
                lastSchedAction = curTime;
            }
        }

        //If needed, wait for our time
        if(timeLeft <= 0) {
            return;
        }
        else {
            Thread.sleep(timeLeft);
        }
    }
}

cosa minTimesignifica qui? Che cosa fa? puoi spiegarlo?
lampeggia il

minTimeè la quantità minima di tempo che deve trascorrere dopo che un token è stato utilizzato prima che il token successivo possa essere utilizzato.
Duarte Meneses

3

Anche se non è quello che hai chiesto, ThreadPoolExecutorche è progettato per limitare M richieste simultanee invece di M richieste in N secondi, potrebbe anche essere utile.


2

Ho implementato un semplice algoritmo di limitazione. Prova questo link, http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

Un breve sull'algoritmo,

Questo algoritmo utilizza la capacità di Java Delayed Queue . Creare un oggetto ritardato con il ritardo previsto (qui 1000 / M per Millisecond TimeUnit ). Metti lo stesso oggetto nella coda ritardata che fornirà la finestra mobile per noi. Quindi, prima che ogni chiamata di metodo prenda l'oggetto dalla coda, take è una chiamata di blocco che tornerà solo dopo il ritardo specificato, e dopo la chiamata di metodo non dimenticare di mettere l'oggetto nella coda con il tempo aggiornato (qui millisecondi attuali) .

Qui possiamo anche avere più oggetti ritardati con ritardo diverso. Questo approccio fornirà anche un throughput elevato.


6
Dovresti pubblicare un riepilogo del tuo algoritmo. Se il tuo link scompare, la tua risposta diventa inutile.
jwr

Grazie, ho aggiunto il brief.
Krishas,

1

La mia implementazione di seguito può gestire la precisione temporale della richiesta arbitraria, ha una complessità temporale O (1) per ogni richiesta, non richiede alcun buffer aggiuntivo, ad esempio complessità spaziale O (1), inoltre non richiede thread in background per rilasciare token, invece i token vengono rilasciati in base al tempo trascorso dall'ultima richiesta.

class RateLimiter {
    int limit;
    double available;
    long interval;

    long lastTimeStamp;

    RateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;

        available = 0;
        lastTimeStamp = System.currentTimeMillis();
    }

    synchronized boolean canAdd() {
        long now = System.currentTimeMillis();
        // more token are released since last request
        available += (now-lastTimeStamp)*1.0/interval*limit; 
        if (available>limit)
            available = limit;

        if (available<1)
            return false;
        else {
            available--;
            lastTimeStamp = now;
            return true;
        }
    }
}

0

Prova a usare questo semplice approccio:

public class SimpleThrottler {

private static final int T = 1; // min
private static final int N = 345;

private Lock lock = new ReentrantLock();
private Condition newFrame = lock.newCondition();
private volatile boolean currentFrame = true;

public SimpleThrottler() {
    handleForGate();
}

/**
 * Payload
 */
private void job() {
    try {
        Thread.sleep(Math.abs(ThreadLocalRandom.current().nextLong(12, 98)));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.err.print(" J. ");
}

public void doJob() throws InterruptedException {
    lock.lock();
    try {

        while (true) {

            int count = 0;

            while (count < N && currentFrame) {
                job();
                count++;
            }

            newFrame.await();
            currentFrame = true;
        }

    } finally {
        lock.unlock();
    }
}

public void handleForGate() {
    Thread handler = new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(1 * 900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                currentFrame = false;

                lock.lock();
                try {
                    newFrame.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    });
    handler.start();
}

}



0

Questo è un aggiornamento al codice LeakyBucket sopra. Questo funziona per più di 1000 richieste al secondo.

import lombok.SneakyThrows;
import java.util.concurrent.TimeUnit;

class LeakyBucket {
  private long minTimeNano; // sec / billion
  private long sched = System.nanoTime();

  /**
   * Create a rate limiter using the leakybucket alg.
   * @param perSec the number of requests per second
   */
  public LeakyBucket(double perSec) {
    if (perSec <= 0.0) {
      throw new RuntimeException("Invalid rate " + perSec);
    }
    this.minTimeNano = (long) (1_000_000_000.0 / perSec);
  }

  @SneakyThrows public void consume() {
    long curr = System.nanoTime();
    long timeLeft;

    synchronized (this) {
      timeLeft = sched - curr + minTimeNano;
      sched += minTimeNano;
    }
    if (timeLeft <= minTimeNano) {
      return;
    }
    TimeUnit.NANOSECONDS.sleep(timeLeft);
  }
}

e il più semplice per sopra:

import com.google.common.base.Stopwatch;
import org.junit.Ignore;
import org.junit.Test;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class LeakyBucketTest {
  @Test @Ignore public void t() {
    double numberPerSec = 10000;
    LeakyBucket b = new LeakyBucket(numberPerSec);
    Stopwatch w = Stopwatch.createStarted();
    IntStream.range(0, (int) (numberPerSec * 5)).parallel().forEach(
        x -> b.consume());
    System.out.printf("%,d ms%n", w.elapsed(TimeUnit.MILLISECONDS));
  }
}

cosa minTimeNanosignifica qui? Puoi spiegare?
lampeggia il

0

Ecco una piccola versione avanzata del semplice limitatore di velocità

/**
 * Simple request limiter based on Thread.sleep method.
 * Create limiter instance via {@link #create(float)} and call {@link #consume()} before making any request.
 * If the limit is exceeded cosume method locks and waits for current call rate to fall down below the limit
 */
public class RequestRateLimiter {

    private long minTime;

    private long lastSchedAction;
    private double avgSpent = 0;

    ArrayList<RatePeriod> periods;


    @AllArgsConstructor
    public static class RatePeriod{

        @Getter
        private LocalTime start;

        @Getter
        private LocalTime end;

        @Getter
        private float maxRate;
    }


    /**
     * Create request limiter with maxRate - maximum number of requests per second
     * @param maxRate - maximum number of requests per second
     * @return
     */
    public static RequestRateLimiter create(float maxRate){
        return new RequestRateLimiter(Arrays.asList( new RatePeriod(LocalTime.of(0,0,0),
                LocalTime.of(23,59,59), maxRate)));
    }

    /**
     * Create request limiter with ratePeriods calendar - maximum number of requests per second in every period
     * @param ratePeriods - rate calendar
     * @return
     */
    public static RequestRateLimiter create(List<RatePeriod> ratePeriods){
        return new RequestRateLimiter(ratePeriods);
    }

    private void checkArgs(List<RatePeriod> ratePeriods){

        for (RatePeriod rp: ratePeriods ){
            if ( null == rp || rp.maxRate <= 0.0f || null == rp.start || null == rp.end )
                throw new IllegalArgumentException("list contains null or rate is less then zero or period is zero length");
        }
    }

    private float getCurrentRate(){

        LocalTime now = LocalTime.now();

        for (RatePeriod rp: periods){
            if ( now.isAfter( rp.start ) && now.isBefore( rp.end ) )
                return rp.maxRate;
        }

        return Float.MAX_VALUE;
    }



    private RequestRateLimiter(List<RatePeriod> ratePeriods){

        checkArgs(ratePeriods);
        periods = new ArrayList<>(ratePeriods.size());
        periods.addAll(ratePeriods);

        this.minTime = (long)(1000.0f / getCurrentRate());
        this.lastSchedAction = System.currentTimeMillis() - minTime;
    }

    /**
     * Call this method before making actual request.
     * Method call locks until current rate falls down below the limit
     * @throws InterruptedException
     */
    public void consume() throws InterruptedException {

        long timeLeft;

        synchronized(this) {
            long curTime = System.currentTimeMillis();

            minTime = (long)(1000.0f / getCurrentRate());
            timeLeft = lastSchedAction + minTime - curTime;

            long timeSpent = curTime - lastSchedAction + timeLeft;
            avgSpent = (avgSpent + timeSpent) / 2;

            if(timeLeft <= 0) {
                lastSchedAction = curTime;
                return;
            }

            lastSchedAction = curTime + timeLeft;
        }

        Thread.sleep(timeLeft);
    }

    public synchronized float getCuRate(){
        return (float) ( 1000d / avgSpent);
    }
}

E test unitari

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class RequestRateLimiterTest {


    @Test(expected = IllegalArgumentException.class)
    public void checkSingleThreadZeroRate(){

        // Zero rate
        RequestRateLimiter limiter = RequestRateLimiter.create(0);
        try {
            limiter.consume();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void checkSingleThreadUnlimitedRate(){

        // Unlimited
        RequestRateLimiter limiter = RequestRateLimiter.create(Float.MAX_VALUE);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) < 1000));
    }

    @Test
    public void rcheckSingleThreadRate(){

        // 3 request per minute
        RequestRateLimiter limiter = RequestRateLimiter.create(3f/60f);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 3; i++ ){

            try {
                limiter.consume();
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) >= 60000 ) & ((ended - started) < 61000));
    }



    @Test
    public void checkSingleThreadRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ));
    }

    @Test
    public void checkMultiThreadedRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreaded32RateLimit(){

        // 0,2 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(0.2f);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(8);
        ExecutorService exec = Executors.newFixedThreadPool(8);

        for ( int i = 0; i < 8; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 2; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreadedRateLimitDynamicRate(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {

                Random r = new Random();
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                        Thread.sleep(r.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

}

Il codice è piuttosto semplice. È sufficiente creare il limitatore con maxRate o con periodi e tariffa. E poi chiama consuma ogni richiesta. Ogni volta che la tariffa non viene superata, il limitatore ritorna immediatamente o attende qualche istante prima di tornare alla tariffa di richiesta corrente più bassa. Ha anche il metodo del tasso corrente che restituisce la media mobile del tasso corrente.
Leonid Astakhov,

0

La mia soluzione: un semplice metodo util, è possibile modificarlo per creare una classe wrapper.

public static Runnable throttle (Runnable realRunner, long delay) {
    Runnable throttleRunner = new Runnable() {
        // whether is waiting to run
        private boolean _isWaiting = false;
        // target time to run realRunner
        private long _timeToRun;
        // specified delay time to wait
        private long _delay = delay;
        // Runnable that has the real task to run
        private Runnable _realRunner = realRunner;
        @Override
        public void run() {
            // current time
            long now;
            synchronized (this) {
                // another thread is waiting, skip
                if (_isWaiting) return;
                now = System.currentTimeMillis();
                // update time to run
                // do not update it each time since
                // you do not want to postpone it unlimited
                _timeToRun = now+_delay;
                // set waiting status
                _isWaiting = true;
            }
            try {
                Thread.sleep(_timeToRun-now);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // clear waiting status before run
                _isWaiting = false;
                // do the real task
                _realRunner.run();
            }
        }};
    return throttleRunner;
}

Prendi da JAVA Thread Debounce and Throttle

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.