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?
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.
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.
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
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 .
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.
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).
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.
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/
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
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())
}
}
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
}