web-dev-qa-db-fra.com

Y a-t-il une mise en œuvre de la file d'attente?

Quelqu'un peut-il suggérer un conteneur Go pour un fichier FIF/file simple et rapide, Go comporte 3 conteneurs différents: heap, list et vector. Lequel est le plus approprié pour implémenter une file d'attente?

43
rev

Un vecteur ou une liste devrait fonctionner, mais le vecteur est probablement le chemin à parcourir. Je dis cela parce que vector va probablement allouer moins souvent que list et garbage (dans l'implémentation actuelle de Go) est assez coûteux. Dans un petit programme, cela n'aura probablement pas d'importance.

12
Evan Shaw

En fait, si vous voulez une file d'attente fifo simple et facile à utiliser, slice vous fournit tout ce dont vous avez besoin.

queue := make([]int, 0)
// Push to the queue
queue = append(queue, 1)
// Top (just get next element, don't remove it)
x = queue[0]
// Discard top element
queue = queue[1:]
// Is empty ?
if len(queue) == 0 {
    fmt.Println("Queue is empty !")
}

Bien entendu, nous supposons que nous pouvons faire confiance à l'implémentation interne de append et slicing afin d'éviter des redimensionnements et réaffectations inutiles. Pour un usage de base, cela suffit parfaitement.

60
Marwan Burelle

Surpris de voir que personne n'a encore suggéré de canaux en mémoire tampon, de toute façon pour la file d'attente FIFO liée à la taille.

//Or however many you might need + buffer.
c := make(chan int, 300)

//Push
c <- value

//Pop
x <- c
40
saarrrr

Pour développer l'application, Moraes propose dans his Gist une structure provenant de la file d'attente et de la pile:

// Stack is a basic LIFO stack that resizes as needed.
type Stack struct {
    nodes   []*Node
    count   int
}
// Queue is a basic FIFO queue based on a circular list that resizes as needed.
type Queue struct {
    nodes   []*Node
    head    int
    tail    int
    count   int
}

Vous pouvez le voir en action dans cet exemple playground .

7
VonC

L'utilisation d'une tranche et d'un schéma d'indexation approprié ("circulaire") semble toujours être la solution. Voici mon point de vue: https://github.com/phf/go-queue Les tests de performance confirment également que les chaînes sont plus rapides, mais au prix de fonctionnalités plus limitées.

4
Peter Froehlich

La plupart des mises en œuvre de file d'attente se présentent sous l'une des trois formes suivantes: base slice, base liste liée et tampon circulaire (mémoire tampon circulaire).

  • Les files d'attente basées sur les tranches ont tendance à gaspiller de la mémoire car elles ne réutilisent pas la mémoire précédemment occupée par les éléments supprimés. De plus, les files d'attente basées sur les tranches ont tendance à n'être que asymétriques.
  • Les files d'attente de listes chaînées peuvent être meilleures en ce qui concerne la mémoire réutiliser, mais sont généralement un peu plus lentes et utilisent plus de mémoire dans l'ensemble en raison de la surcharge liée au maintien des liens. Ils peuvent offrir la possibilité d'ajouter et de supprimer des éléments du milieu de la file d'attente sans déplacer de mémoire, mais si vous effectuez l'essentiel de cette tâche, une file d'attente présente une structure de données incorrecte. 
  • Les files d'attente de mémoire tampon offrent toute l'efficacité des tranches, avec l'avantage de ne pas gaspiller de mémoire. Moins d'allocations signifie de meilleures performances. Ils sont tout aussi efficaces pour ajouter et supprimer des éléments à l'une ou l'autre extrémité, de sorte que vous obtenez naturellement une file d'attente à double extrémité. Donc, en tant que recommandation générale, je recommanderais une implémentation de file d'attente basée sur un tampon en anneau. C'est ce qui est discuté dans le reste de ce post.

La file d'attente basée sur le tampon circulaire réutilise la mémoire en encapsulant son stockage: Lorsque la file d'attente dépasse l'une des extrémités de la tranche sous-jacente, elle ajoute des nœuds supplémentaires à l'autre extrémité de la tranche. Voir diagramme de deque

En outre, un peu de code pour illustrer:

// PushBack appends an element to the back of the queue.  Implements FIFO when
// elements are removed with PopFront(), and LIFO when elements are removed
// with PopBack().
func (q *Deque) PushBack(elem interface{}) {
    q.growIfFull()
    q.buf[q.tail] = elem
    // Calculate new tail position.
    q.tail = q.next(q.tail)
    q.count++
}

// next returns the next buffer position wrapping around buffer.
func (q *Deque) next(i int) int {
    return (i + 1) & (len(q.buf) - 1) // bitwise modulus
}

Cette mise en œuvre particulière utilise toujours une taille de tampon d'une puissance de 2 et peut donc calculer le module au niveau du bit pour être un peu plus efficace.

Cela signifie que la tranche n'a besoin de croître que lorsque toute sa capacité est utilisée. Avec une stratégie de redimensionnement qui évite de développer et de réduire le stockage sur la même limite, cela le rend très efficace en termes de mémoire.

Voici le code qui redimensionne le tampon de tranche sous-jacent:

// resize resizes the deque to fit exactly twice its current contents. This is
// used to grow the queue when it is full, and also to shrink it when it is     
// only a quarter full.                                                         
func (q *Deque) resize() {
    newBuf := make([]interface{}, q.count<<1)
    if q.tail > q.head {
        copy(newBuf, q.buf[q.head:q.tail])
    } else {
        n := copy(newBuf, q.buf[q.head:])
        copy(newBuf[n:], q.buf[:q.tail])
    }
    q.head = 0
    q.tail = q.count
    q.buf = newBuf
}

Une autre chose à considérer est si vous voulez que la sécurité de la simultanéité soit intégrée à la mise en œuvre. Vous voudrez peut-être éviter cela afin de pouvoir faire tout ce qui convient le mieux à votre stratégie de concurrence, et vous ne le voudrez certainement pas si vous n'en avez pas besoin; même raison pour ne pas vouloir une tranche avec une sérialisation intégrée.

Il existe un certain nombre d'implémentations de files d'attente basées sur le tampon circulaire pour Go si vous effectuez une recherche sur godoc pour deque. Choisissez celui qui convient le mieux à vos goûts. 

3
gammazero

Malheureusement, les files d'attente ne font pas partie de la bibliothèque standard go, vous devez donc écrire votre propre solution/importer la solution de quelqu'un d'autre. C'est dommage car les conteneurs écrits en dehors de la bibliothèque standard ne peuvent pas utiliser les génériques.

Voici un exemple simple de file d'attente à capacité fixe:

type MyQueueElement struct {
  blah int // whatever you want
}

const MAX_QUEUE_SIZE = 16
type Queue struct {
  content  [MAX_QUEUE_SIZE]MyQueueElement
  readHead int
  writeHead int
  len int
}

func (q *Queue) Push(e MyQueueElement) bool {
  if q.len >= MAX_QUEUE_SIZE {
    return false
  }
  q.content[q.writeHead] = e
  q.writeHead = (q.writeHead + 1) % MAX_QUEUE_SIZE
  q.len++
  return true
}

func (q *Queue) Pop() (MyQueueElement, bool) {
  if q.len <= 0 {
    return MyQueueElement{}, false
  }
  result := q.content[q.readHead]
  q.content[q.readHead] = MyQueueElement{}
  q.readHead = (q.readHead + 1) % MAX_QUEUE_SIZE
  q.len--
  return result, true
}

Les pièges à éviter ici incluent le fait de ne pas avoir de croissance de tranche sans limite (en raison de l’utilisation de l’opération slice [1:] pour être rejetée), et la remise à zéro des éléments remplis pour garantir que leur contenu est disponible pour la récupération de place. Notez que, dans le cas d'une structure MyQueueElement contenant uniquement un int comme ici, cela ne fera aucune différence, mais si struct contenait des pointeurs, ce serait le cas.

La solution pourrait être étendue pour réaffecter et copier si une file d'attente à croissance automatique était souhaitée.

Cette solution n'est pas thread-safe, mais un verrou peut être ajouté à Push/Pop si cela est souhaité.

Terrain de jeu https://play.golang.org/

2
tul

J'ai également implémenter la file d'attente de tranche comme ci-dessus. Cependant, ce n'est pas thread-safe. J'ai donc décidé d'ajouter un verrou (verrou mutex) pour garantir la sécurité du thread.

package queue

import (
  "sync"
)

type Queue struct {
  lock *sync.Mutex
  Values []int
}

func Init() Queue {
  return Queue{&sync.Mutex{}, make([]int, 0)}
}

func (q *Queue) Enqueue(x int) {
  for {
    q.lock.Lock()
    q.Values = append(q.Values, x)
    q.lock.Unlock()
    return
  }
}

func (q *Queue) Dequeue() *int {
  for {
    if (len(q.Values) > 0) {
      q.lock.Lock()
      x := q.Values[0]
      q.Values = q.Values[1:]
      q.lock.Unlock()
      return &x
    }
    return nil
  }
  return nil
}

Vous pouvez vérifier ma solution sur github ici file d'attente simple

1
Dat Tran

J'ai mis en place une file d'attente qui étendra automatiquement le tampon sous-jacent:

package types

// Note: this queue does not shrink the underlying buffer.                                                                                                               
type queue struct {
        buf  [][4]int // change to the element data type that you need                                                                                                   
        head int
        tail int
}

func (q *queue) extend(need int) {
        if need-(len(q.buf)-q.head) > 0 {
                if need-len(q.buf) <= 0 {
                        copy(q.buf, q.buf[q.head:q.tail])
            q.tail = q.tail - q.head
                        q.head = 0
                        return
                }

                newSize := len(q.buf) * 2
                if newSize == 0 {
                    newSize = 100
            }
                newBuf := make([][4]int, newSize)
                copy(newBuf, q.buf[q.head:q.tail])
                q.buf = newBuf
        q.tail = q.tail - q.head
                q.head = 0
        }
}

func (q *queue) Push(p [4]int) {
        q.extend(q.tail + 1)
        q.buf[q.tail] = p
        q.tail++
}

func (q *queue) pop() [4]int {
        r := q.buf[q.head]
        q.head++
        return r
}

func (q *queue) size() int {
        return q.tail - q.head
}


// put the following into queue_test.go
package types

import (
        "testing"

        "github.com/stretchr/testify/assert"
)

func TestQueue(t *testing.T) {
        const total = 1000
        q := &queue{}
        for i := 0; i < total; i++ {
                q.Push([4]int{i, i, i, i})
                assert.Equal(t, i+1, q.size())
        }

    for i := 0; i < total; i++ {
                v := q.pop()
                assert.Equal(t, [4]int{i, i, i, i}, v)
                assert.Equal(t, total-1-i, q.size())
        }
}
0
Helin Wang

Implémentation double pile:

O(1)Enqueue and Dequeue et utilise slices (ce qui est généralement préférable pour les erreurs de mémoire cache).

type Queue struct{
    enqueue, dequeue Stack
}

func (q *Queue) Enqueue(n *Thing){
    q.enqueue.Push(n)
}

func (q *Queue) Dequeue()(*Thing, bool){
    v, ok := q.dequeue.Pop()
    if ok{
        return v, true
    }

    for {
        v, ok := d.enqueue.Pop()
        if !ok{
            break
        }

        d.dequeue.Push(v)
    }

    return d.dequeue.Pop()
}

type Stack struct{
    v []*Thing
}

func (s *Stack)Push(n *Thing){
    s.v=append(s.v, n)
}

func (s *Stack) Pop()(*Thing, bool){
    if len(s.v) == 0 {
        return nil, false
    }

    lastIdx := len(s.v)-1
    v := s.v[lastIdx]
    s.v=s.v[:lastIdx]
    return v, true
}
0
poy