एल्गोरिथ्म एक अच्छी दर सीमित करने वाला क्या है?


155

मैं कुछ छद्म कोड, या बेहतर, पायथन का उपयोग कर सकता था। मैं पायथन आईआरसी बॉट के लिए एक दर-सीमित कतार को लागू करने की कोशिश कर रहा हूं, और यह आंशिक रूप से काम करता है, लेकिन अगर कोई व्यक्ति सीमा से कम संदेश चलाता है (उदाहरण के लिए, दर सीमा प्रति सेकंड 5 संदेश है, और व्यक्ति केवल 4 चलाता है) और अगला ट्रिगर 8 सेकंड से अधिक है (उदाहरण के लिए, 16 सेकंड बाद), बॉट संदेश भेजता है, लेकिन कतार पूर्ण हो जाती है और बॉट 8 सेकंड इंतजार करता है, भले ही इसकी ज़रूरत नहीं है क्योंकि 8 सेकंड की अवधि व्यतीत हो गई है।

जवाबों:


231

यहां सरलतम एल्गोरिथ्म , यदि आप केवल संदेशों को छोड़ने के लिए चाहते हैं जब वे बहुत जल्दी पहुंचते हैं (बजाय उन्हें कतारबद्ध करने के, जो समझ में आता है क्योंकि कतार मनमाने ढंग से बड़ी हो सकती है):

rate = 5.0; // unit: messages
per  = 8.0; // unit: seconds
allowance = rate; // unit: messages
last_check = now(); // floating-point, e.g. usec accuracy. Unit: seconds

when (message_received):
  current = now();
  time_passed = current - last_check;
  last_check = current;
  allowance += time_passed * (rate / per);
  if (allowance > rate):
    allowance = rate; // throttle
  if (allowance < 1.0):
    discard_message();
  else:
    forward_message();
    allowance -= 1.0;

इस घोल में कोई डीटास्ट्रक्चर, टाइमर आदि नहीं होते हैं और यह साफ-सुथरा काम करता है :) इसे देखने के लिए, 'भत्ता' अधिकतम 5 सेकंड में गति 5/8 यूनिट तक बढ़ता है, यानी अधिकतम पांच यूनिट प्रति आठ सेकंड में। प्रत्येक संदेश जो अग्रेषित किया जाता है, एक इकाई को घटा देता है, इसलिए आप प्रत्येक आठ सेकंड में पांच से अधिक संदेश नहीं भेज सकते।

ध्यान दें कि rateपूर्णांक होना चाहिए, यानी बिना शून्य दशमलव भाग के, या एल्गोरिथ्म सही ढंग से काम नहीं करेगा (वास्तविक दर नहीं होगी rate/per)। ईजी rate=0.5; per=1.0;काम नहीं करता है क्योंकि allowanceकभी भी 1.0 तक नहीं बढ़ेगा। लेकिन rate=1.0; per=2.0;ठीक काम करता है।


4
यह भी इंगित करने योग्य है कि 'time_passed' का आयाम और पैमाना 'प्रति', जैसे सेकंड के समान होना चाहिए।
skaffman

2
हाय skaffman, तारीफ के लिए धन्यवाद --- मैंने इसे अपनी आस्तीन से बाहर फेंक दिया, लेकिन 99.9% संभावना के साथ किसी ने पहले भी इसी तरह के समाधान के साथ आया है :)
एंटी हुइमा

52
यह एक मानक एल्गोरिथ्म है - यह एक टोकन बाल्टी है, कतार के बिना। बाल्टी है allowance। बाल्टी आकार है rateallowance += …लाइन हर टोकन एक जोड़ने का एक अनुकूलन है दर ÷ प्रति सेकंड।
अपमानजनक

5
@zwirbeltier जो आप ऊपर लिख रहे हैं वह सच नहीं है। 'भत्ता' हमेशा 'दर' ("// थ्रॉटल" लाइन पर देखें) से छाया हुआ है, इसलिए यह केवल किसी विशेष समय पर वास्तव में 'दर' संदेशों के फटने की अनुमति देगा, अर्थात 5.
एंटी हुइमा

7
यह अच्छा है, लेकिन दर को पार कर सकता है। मान लीजिए कि आप 0 से 5 संदेश आगे भेजते हैं, तो उस समय N * (8/5) के लिए N = 1, 2, ... आप एक और संदेश भेज सकते हैं, जिसके परिणामस्वरूप 8 सेकंड की अवधि में 5 से अधिक संदेश
मिलेंगे

48

अपने फ़ंक्शन से पहले इस सज्जाकार @RateLimited (रेटपर्सक) का उपयोग करें जो enqueues।

मूल रूप से, यह जाँच करता है कि पिछली बार से 1 / दर सेकेंड गुजरा है या नहीं और शेष समय का इंतजार करता है, अन्यथा वह प्रतीक्षा नहीं करता है। यह प्रभावी रूप से आपको दर / सेकंड तक सीमित करता है। डेकोरेटर को किसी भी फ़ंक्शन पर लागू किया जा सकता है जिसे आप दर-सीमित चाहते हैं।

आपके मामले में, यदि आप प्रति 8 सेकंड में अधिकतम 5 संदेश चाहते हैं, तो अपने sendToQueue फ़ंक्शन से पहले @RateLimited (0.625) का उपयोग करें।

import time

def RateLimited(maxPerSecond):
    minInterval = 1.0 / float(maxPerSecond)
    def decorate(func):
        lastTimeCalled = [0.0]
        def rateLimitedFunction(*args,**kargs):
            elapsed = time.clock() - lastTimeCalled[0]
            leftToWait = minInterval - elapsed
            if leftToWait>0:
                time.sleep(leftToWait)
            ret = func(*args,**kargs)
            lastTimeCalled[0] = time.clock()
            return ret
        return rateLimitedFunction
    return decorate

@RateLimited(2)  # 2 per second at most
def PrintNumber(num):
    print num

if __name__ == "__main__":
    print "This should print 1,2,3... at about 2 per second."
    for i in range(1,100):
        PrintNumber(i)

मुझे इस उद्देश्य के लिए एक डेकोरेटर का उपयोग करने का विचार पसंद है। अंतिम सूची को अंतिम रूप क्यों दिया गया है? इसके अलावा, मुझे संदेह है कि यह तब काम करेगा जब कई धागे एक ही रेट लिस्टेड फंक्शन कह रहे हों ...
Stephan202

8
यह एक सूची है क्योंकि फ्लोट जैसे सरल प्रकार स्थिर होते हैं जब एक बंद द्वारा कब्जा कर लिया जाता है। इसे सूची बनाकर, सूची स्थिर है, लेकिन इसकी सामग्री नहीं है। हां, यह थ्रेड-सेफ नहीं है, लेकिन ताले के साथ इसे आसानी से तय किया जा सकता है।
कार्लोस ए। इबर्रा

time.clock()मेरे सिस्टम पर पर्याप्त रिज़ॉल्यूशन नहीं है, इसलिए मैंने कोड को अनुकूलित किया और उपयोग करने के लिए बदल दियाtime.time()
mtrbean

3
दर सीमित करने के लिए, आप निश्चित रूप से उपयोग नहीं करना चाहते हैं time.clock(), जो सीपीयू समय को समाप्त कर देता है। सीपीयू समय "वास्तविक" समय की तुलना में बहुत तेज या बहुत धीमा चल सकता है। आप time.time()इसके बजाय उपयोग करना चाहते हैं , जो दीवार समय ("वास्तविक" समय) को मापता है।
जॉन वाइसमैन

1
वास्तविक उत्पादन प्रणालियों के लिए BTW: एक नींद के साथ सीमित दर को लागू करना () कॉल एक अच्छा विचार नहीं हो सकता है क्योंकि यह थ्रेड को ब्लॉक करने जा रहा है और इसलिए किसी अन्य क्लाइंट को इसका उपयोग करने से रोक रहा है।
मार्श

28

एक टोकन बाल्टी लागू करने के लिए काफी सरल है।

5 टोकनों के साथ एक बाल्टी के साथ शुरू करें।

हर 5/8 सेकंड: अगर बाल्टी में 5 से कम टोकन हैं, तो एक जोड़ें।

हर बार जब आप एक संदेश भेजना चाहते हैं: यदि बाल्टी में send1 टोकन हैं, तो एक टोकन बाहर ले जाएं और संदेश भेजें। अन्यथा, प्रतीक्षा करें / संदेश छोड़ दें / जो भी हो।

(स्पष्ट रूप से, वास्तविक कोड में, आप वास्तविक टोकन के बजाय पूर्णांक काउंटर का उपयोग करेंगे और आप टाइमस्टैम्प्स को संग्रहीत करके प्रत्येक 5 / 8s चरण का अनुकूलन कर सकते हैं)


प्रश्न को फिर से पढ़ना, अगर दर सीमा पूरी तरह से प्रत्येक 8 सेकंड में रीसेट हो जाती है, तो यहां एक संशोधन है:

टाइमस्टैम्प से शुरू करें last_send, एक समय पहले (जैसे, युग में)। इसके अलावा, उसी 5-टोकन बाल्टी के साथ शुरू करें।

हर 5/8 सेकंड के नियम पर प्रहार करें।

हर बार जब आप एक संदेश भेजते हैं: पहले, जांचें कि message last_send8 सेकंड पहले। यदि ऐसा है, तो बाल्टी भरें (इसे 5 टोकन पर सेट करें)। दूसरा, अगर बाल्टी में टोकन हैं, तो संदेश भेजें (अन्यथा, ड्रॉप / प्रतीक्षा / आदि।)। तीसरा, last_sendअब के लिए सेट ।

उस परिदृश्य के लिए काम करना चाहिए।


मैंने वास्तव में इस तरह की रणनीति (पहला दृष्टिकोण) का उपयोग करके आईआरसी बॉट लिखा है। पर्थ में इसका, पायथन नहीं है, लेकिन यहाँ वर्णन करने के लिए कुछ कोड है:

यहाँ पहला भाग बाल्टी में टोकन जोड़ने का काम करता है। आप समय (दूसरी से अंतिम पंक्ति) के आधार पर टोकन जोड़ने का अनुकूलन देख सकते हैं और फिर अंतिम पंक्ति अधिकतम करने के लिए बाल्टी सामग्री (MESSAGE_BURST)

    my $start_time = time;
    ...
    # Bucket handling
    my $bucket = $conn->{fujiko_limit_bucket};
    my $lasttx = $conn->{fujiko_limit_lasttx};
    $bucket += ($start_time-$lasttx)/MESSAGE_INTERVAL;
    ($bucket <= MESSAGE_BURST) or $bucket = MESSAGE_BURST;

$ कॉन एक डेटा संरचना है जो चारों ओर से गुजरती है। यह एक विधि के अंदर है जो नियमित रूप से चलता है (यह गणना करता है कि अगली बार जब यह करने के लिए कुछ होगा, और तब तक या तो सोता है या जब तक यह नेटवर्क ट्रैफ़िक प्राप्त नहीं करता है)। विधि का अगला भाग भेजना संभालता है। यह बल्कि जटिल है, क्योंकि संदेशों में उनसे जुड़ी प्राथमिकताएँ होती हैं।

    # Queue handling. Start with the ultimate queue.
    my $queues = $conn->{fujiko_queues};
    foreach my $entry (@{$queues->[PRIORITY_ULTIMATE]}) {
            # Ultimate is special. We run ultimate no matter what. Even if
            # it sends the bucket negative.
            --$bucket;
            $entry->{code}(@{$entry->{args}});
    }
    $queues->[PRIORITY_ULTIMATE] = [];

यह पहली कतार है, जिसे चलाया नहीं जाता है। भले ही यह हमारे कनेक्शन को बाढ़ के लिए मार दिया जाए। अत्यंत महत्वपूर्ण चीजों के लिए उपयोग किया जाता है, जैसे कि सर्वर के पिंग का जवाब। अगला, शेष कतारें:

    # Continue to the other queues, in order of priority.
    QRUN: for (my $pri = PRIORITY_HIGH; $pri >= PRIORITY_JUNK; --$pri) {
            my $queue = $queues->[$pri];
            while (scalar(@$queue)) {
                    if ($bucket < 1) {
                            # continue later.
                            $need_more_time = 1;
                            last QRUN;
                    } else {
                            --$bucket;
                            my $entry = shift @$queue;
                            $entry->{code}(@{$entry->{args}});
                    }
            }
    }

अंत में, बाल्टी की स्थिति वापस $ कॉन डेटा संरचना में सहेज ली जाती है (वास्तव में विधि में थोड़ा बाद में; यह पहली बार गणना करता है कि यह कितनी जल्दी काम करेगा)

    # Save status.
    $conn->{fujiko_limit_bucket} = $bucket;
    $conn->{fujiko_limit_lasttx} = $start_time;

जैसा कि आप देख सकते हैं, वास्तविक बाल्टी हैंडलिंग कोड बहुत छोटा है - लगभग चार लाइनें। बाकी कोड प्राथमिकता कतार हैंडलिंग है। बॉट की प्राथमिकता कतारें हैं, जैसे कि, इसके साथ चैट करने वाला कोई व्यक्ति इसे अपने महत्वपूर्ण किक / प्रतिबंध कर्तव्यों को करने से नहीं रोक सकता है।


क्या मुझे कुछ याद आ रहा है ... ऐसा लगता है कि आपको हर 8 सेकंड में 1 संदेश तक सीमित कर देगा जब आप पहले 5 के माध्यम से प्राप्त करेंगे
chills42

@ chills42: हां, मैंने सवाल गलत पढ़ा है ... जवाब का दूसरा भाग देखें।
derobert

@chills: अगर last_send <8 सेकंड है, तो आप बाल्टी में कोई टोकन नहीं जोड़ते हैं। यदि आपकी बाल्टी में टोकन हैं, तो आप संदेश भेज सकते हैं; अन्यथा आप (आप पहले से ही 5 संदेशों पिछले 8 सेकेंड में भेज दिया है) नहीं कर सकते हैं
derobert

3
मैं इसकी सराहना करता हूँ अगर लोग इसे कम कर देते हैं तो कृपया यह समझाएँगे कि ... मैं आपके द्वारा देखी गई किसी भी समस्या को ठीक करना चाहता हूँ, लेकिन यह बिना प्रतिक्रिया के करना मुश्किल है!
२०:२२

10

संदेश को भेजे जाने तक प्रसंस्करण को अवरुद्ध करने के लिए, इस प्रकार आगे के संदेशों को कतारबद्ध करते हुए, एंटटी के सुंदर समाधान को भी इस तरह संशोधित किया जा सकता है:

rate = 5.0; // unit: messages
per  = 8.0; // unit: seconds
allowance = rate; // unit: messages
last_check = now(); // floating-point, e.g. usec accuracy. Unit: seconds

when (message_received):
  current = now();
  time_passed = current - last_check;
  last_check = current;
  allowance += time_passed * (rate / per);
  if (allowance > rate):
    allowance = rate; // throttle
  if (allowance < 1.0):
    time.sleep( (1-allowance) * (per/rate))
    forward_message();
    allowance = 0.0;
  else:
    forward_message();
    allowance -= 1.0;

यह संदेश भेजने के लिए पर्याप्त भत्ता होने तक इंतजार करता है। दो बार दर के साथ शुरू नहीं करने के लिए, भत्ता भी 0 से आरंभ किया जा सकता है।


5
जब आप सोते हैं (1-allowance) * (per/rate), तो आपको उसी राशि को जोड़ना होगा last_check
अल्प

2

समय को रखें कि पिछली पांच लाइनें भेजी गई थीं। तब तक कतारबद्ध संदेशों को पकड़ें जब तक कि पांचवां-सबसे हाल का संदेश (यदि यह मौजूद है) अतीत में कम से कम 8 सेकंड है (समय की एक सरणी के रूप में last_five के साथ):

now = time.time()
if len(last_five) == 0 or (now - last_five[-1]) >= 8.0:
    last_five.insert(0, now)
    send_message(msg)
if len(last_five) > 5:
    last_five.pop()

जब से आपने इसे संशोधित किया है मैं नहीं हूं।
पेस्तो

आप पाँच बार टिकट संग्रह कर रहे हैं और बार-बार उन्हें मेमोरी के माध्यम से स्थानांतरित कर रहे हैं (या लिंक किए गए सूची संचालन कर रहे हैं)। मैं एक पूर्णांक काउंटर और एक टाइमस्टैम्प स्टोर कर रहा हूं। और केवल अंकगणित और असाइन करना।
derobert

2
सिवाय इसके कि मेरा काम बेहतर होगा यदि 5 लाइनें भेजने की कोशिश की जाए लेकिन समय अवधि में केवल 3 और की अनुमति है। आपका पहला तीन भेजने की अनुमति देगा, और 4 और 5 भेजने से पहले एक 8 सेकंड प्रतीक्षा करने के लिए मजबूर करें। मेरा 4 और 5 को चौथी और पांचवीं सबसे हाल ही में लाइनों के बाद 8 सेकंड भेजने की अनुमति देगा।
पेस्तो

1
लेकिन विषय पर, लंबाई 5 की एक गोलाकार लिंक्ड सूची का उपयोग करके प्रदर्शन में सुधार किया जा सकता है, पांचवे-सबसे हाल के संकेत की ओर इशारा करते हुए, इसे नए भेजने पर ओवरराइट करके और पॉइंटर को आगे बढ़ाते हुए।
पेस्तो

दर सीमा गति के साथ एक irc बॉट के लिए एक मुद्दा नहीं है। मैं सूची समाधान पसंद करता हूं क्योंकि यह अधिक पठनीय है। बकेट उत्तर दिया गया है कि पुनरीक्षण के कारण भ्रमित किया गया है, लेकिन इसमें कुछ भी गलत नहीं है।
जेरिको

2

एक समाधान प्रत्येक कतार आइटम के लिए टाइमस्टैम्प संलग्न करना है और 8 सेकंड बीतने के बाद आइटम को त्यागना है। प्रत्येक बार कतार में जुड़ने पर आप यह जांच कर सकते हैं।

यह केवल तभी काम करता है जब आप कतार के आकार को 5 तक सीमित करते हैं और कतार के पूर्ण होने पर किसी भी अतिरिक्त को छोड़ देते हैं।


1

अगर कोई अभी भी दिलचस्पी रखता है, तो मैं इस साधारण कॉल करने योग्य वर्ग का उपयोग समयबद्ध LRU कुंजी मूल्य संग्रहण के साथ प्रति आईपी दर अनुरोध दर को सीमित करने के लिए करता हूं। एक छल का उपयोग करता है, लेकिन इसके बजाय एक सूची के साथ उपयोग करने के लिए फिर से लिखा जा सकता है।

from collections import deque
import time


class RateLimiter:
    def __init__(self, maxRate=5, timeUnit=1):
        self.timeUnit = timeUnit
        self.deque = deque(maxlen=maxRate)

    def __call__(self):
        if self.deque.maxlen == len(self.deque):
            cTime = time.time()
            if cTime - self.deque[0] > self.timeUnit:
                self.deque.append(cTime)
                return False
            else:
                return True
        self.deque.append(time.time())
        return False

r = RateLimiter()
for i in range(0,100):
    time.sleep(0.1)
    print(i, "block" if r() else "pass")

1

स्वीकृत उत्तर से एक कोड का सिर्फ एक पायथन कार्यान्वयन।

import time

class Object(object):
    pass

def get_throttler(rate, per):
    scope = Object()
    scope.allowance = rate
    scope.last_check = time.time()
    def throttler(fn):
        current = time.time()
        time_passed = current - scope.last_check;
        scope.last_check = current;
        scope.allowance = scope.allowance + time_passed * (rate / per)
        if (scope.allowance > rate):
          scope.allowance = rate
        if (scope.allowance < 1):
          pass
        else:
          fn()
          scope.allowance = scope.allowance - 1
    return throttler

यह मुझे सुझाव दिया गया है कि मैं आपको अपने कोड का उपयोग उदाहरण जोड़ने का सुझाव देता हूं ।
ल्यूक

0

इस बारे में कैसा है:

long check_time = System.currentTimeMillis();
int msgs_sent_count = 0;

private boolean isRateLimited(int msgs_per_sec) {
    if (System.currentTimeMillis() - check_time > 1000) {
        check_time = System.currentTimeMillis();
        msgs_sent_count = 0;
    }

    if (msgs_sent_count > (msgs_per_sec - 1)) {
        return true;
    } else {
        msgs_sent_count++;
    }

    return false;
}

0

मुझे स्काला में बदलाव की जरूरत थी। यह रहा:

case class Limiter[-A, +B](callsPerSecond: (Double, Double), f: A  B) extends (A  B) {

  import Thread.sleep
  private def now = System.currentTimeMillis / 1000.0
  private val (calls, sec) = callsPerSecond
  private var allowance  = 1.0
  private var last = now

  def apply(a: A): B = {
    synchronized {
      val t = now
      val delta_t = t - last
      last = t
      allowance += delta_t * (calls / sec)
      if (allowance > calls)
        allowance = calls
      if (allowance < 1d) {
        sleep(((1 - allowance) * (sec / calls) * 1000d).toLong)
      }
      allowance -= 1
    }
    f(a)
  }

}

यहाँ इसका उपयोग कैसे किया जा सकता है:

val f = Limiter((5d, 8d), { 
  _: Unit  
    println(System.currentTimeMillis) 
})
while(true){f(())}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.