Example #1
0
class Buffer():
    """
    An implementation of a concurrent buffer that runs a generator in a
    separate processor.

    .. note:: The default halting condition requires the values be uniformly
              non decreasing (like the primes, or positive fibonnaci sequence).
              This halting condition currently stops the search once the value
              reached is greater than or equal to the one being searched for.

    The resultant buffered object can be referenced as a list or an iterable.
    The easiest way to use this class is by utlizing the utility function
    :func:`~pyspeedup.concurrent.buffer`.

    For example, one can use it in the following way::

        >>> @buffer(4)
        ... def count():
        ...     i=0
        ...     while 1:
        ...         yield i
        ...         i+=1
        ...
        >>> count[0]
        0
        >>> count[15]
        15
        >>> for v,i in enumerate(count):
        ...     if v!=i:
        ...         print("Fail")
        ...     if v==5:
        ...         print("Success")
        ...         break
        ...
        Success

    It can also be used as a generator by calling the object like so::

        >>> for v,i in enumerate(count()):
        ...     if v!=i:
        ...         print("Fail")
        ...     if v==5:
        ...         print("Success")
        ...         break
        ...
        Success

    The sequence generated is cached, so the output stored will be static.

    To create your own halting condition, you need to provide a function with
    the first argument taken in as the buffer object, the second argument for
    the item that we're deciding whether we've passed (or given up on) during
    the search, and the third argument being how many times we've checked the
    halting condition during this search.

    For example, if your buffered sequence isn't uniformly non decreasing, but
    is instead absolutely non decreasing, you could create the following halting
    condition function:

        >>> def absolutelyNonDecreasing(buffer, item, attempts):
        ...     if abs(buffer._cache[-1])>abs(item):
        ...         return True
        ...     return False
        ...
        >>> @buffer(haltCondition = absolutelyNonDecreasing)
        ... def complexSpiral():
        ...     i = 1
        ...     while True:
        ...         yield i
        ...         i *= 1.1j
        ...
        >>> complexSpiral[1]
        1.1j
        >>> -1.21 in complexSpiral
        True

    Be careful in creating your halting condition, as if it is false for the
    sequence you are buffering, you may not see expected results, or you may
    find your program in an infinite loop. Be sure to consider asymptotes
    and other possibilities in your results. It may not be a bad idea to
    have it bail out after a certain number of attempts.

    .. note:: As of yet all values are stored in a list on the backend.
              There is no memory management built in to this version, but
              is planned to be integrated soon. Be careful not to accidentally
              cache too many or too large of values, as you may use up all of
              your RAM and slow down computation immensely.
    """
    def __init__(self,generator,buffersize=16,haltCondition=uniformlyNonDecreasing):
        for n in list(n for n in set(dir(generator)) - set(dir(self)) if n != '__class__'):
            setattr(self, n, getattr(generator, n))
        setattr(self, "__doc__", getattr(generator, "__doc__"))
        self._generator,self._buffersize=generator,buffersize
        self._m=Manager()
        self._e=self._m.Event()
        self._g=dumps(generator.__code__)
        self._n=generator.__name__
        self._cache=self._m.list()
        self.haltCondition = haltCondition #This will make non-uniformly increasing generators usable without introducing a halting problem in the code (just in the userspace).
        self._q=Queue(self._buffersize)
        self._thread=Process(target=_run,args=(self._q,self._g,self._n,self._cache,self._e))
        self._thread.daemon=True
        self._thread.start()
    def __del__(self):
        self._thread.terminate()
        del self._thread
    def __call__(self):
        """ Creates a generator that yields the values from the original
        starting with the first value.

        """
        i=0
        while True:
            try:
                yield self._cache[i]
                i+=1
            except:
                self._e.wait(.1)
                self.pull_values()
        raise Exception("Deadlocked...")
    def __contains__(self,item):
        attempts = 0
        prevCount = 0
        while self.haltCondition(self,item,attempts):
            currentCount=len(self._cache)
            if currentCount == prevCount:
                currentCount += 1
                self[prevCount]
            if item in self._cache[prevCount:currentCount]:
                return True 
            prevCount = currentCount
            attempts += 1
        currentCount=len(self._cache)
        if item in self._cache[prevCount:currentCount]:
            return True 
        return False
    def __getitem__(self,key):
        cache_len=len(self._cache)
        if key+self._buffersize>cache_len:
            self.pull_values()
        if key<cache_len:
            return self._cache[key]
        else:
            while True:
                if self._q.empty():
                    self._e.wait(.1)
                    self._e.clear()
                else:
                    try:
                        self._cache.append(self._q.get(True,10))
                        cache_len+=1
                        self._e.set()
                        self._e.clear()
                        if cache_len==key+1:
                            return self._cache[key]
                    except:
                        print("Starts failing at {}. Manager debug info is {}.".format(cache_len, self._m._debug_info()))
    def pull_values(self):
        """ A utility method used to pull and cache values from the
        concurrently run generator.

        """
        try:
            for i in range(self._buffersize):
                self._cache.append(self._q.get(False))
        except Exception as e:
            pass
    def __repr__(self):
        return 'concurrent._Buffer('+self.func.__repr__()+','+str(self._buffersize)+',None)'