Go to file
Sam Fredrickson 87212466ae
Some checks failed
Build & Test / Main (push) Failing after 5s
Add iterators for a couple queues.
2024-08-22 00:53:48 -07:00
.gitea/workflows Add Gitea Actions workflow. 2023-12-11 00:24:40 -08:00
binheap Add npq package. 2023-03-03 22:20:11 -08:00
mq Unlock() before panic() when already closed. 2023-07-10 10:55:25 -07:00
npq Unlock() before panic() when already closed. 2023-07-10 10:55:25 -07:00
pq Add iterators for a couple queues. 2024-08-22 00:53:48 -07:00
queue Add iterators for a couple queues. 2024-08-22 00:53:48 -07:00
.gitignore Various improvements. 2023-03-03 15:35:49 -08:00
go.mod Add iterators for a couple queues. 2024-08-22 00:53:48 -07:00
go.sum Implement a true priority queue. 2023-03-01 19:29:15 -08:00
lib.go Various improvements. 2023-03-03 15:35:49 -08:00
LICENSE Initial commit. 2023-02-28 20:33:22 -08:00
Makefile Various improvements. 2023-03-03 15:35:49 -08:00
README.md Add npq package. 2023-03-03 22:20:11 -08:00

priorityq - generic prioritized queues in Go

In Go, the builtin buffered channels provide a concurrent FIFO queue for passing messages between goroutines. Sometimes, however, it's convenient to be able to assign priority levels to messages, so that they get delivered to consumers more promptly.

The mq package in this module implements a concurrent, dual-priority message queue that guarantees receipt of a high-priority items before low-priority ones. There is a pattern using two channels and select statements to achieve similar functionality, but it's not exactly equivalent. (See the Background section for more details.)

Additionally, the pq package implements a concurrent, traditional priority queue, using a binary max-heap. This is more general than mq, because it allows multiple levels of priority. This, of course, also makes operations slower.

Lastly, the npq package implements a concurrent, n-priority message queue. It's similar to mq, except that it an arbitrary fixed number of priority levels. It can have better performance than pq for several hundred levels.

Benchmarks

Here are some benchmark results from running on a Mac Studio/M1 Ultra.

pkg: gogs.humancabbage.net/sam/priorityq/mq
Send-20                          13.93n  ± 0%
SendChan-20                      13.19n  ± 0%
Recv-20                          13.64n  ± 1%
RecvChan-20                      13.29n  ± 1%
ConcSendRecv-20                  97.60n  ± 1%
ConcSendRecvChan-20             171.8n   ± 5%
HighContention-20               128.2n   ± 0%
HighContentionChan-20            47.27n  ± 0%

pkg: gogs.humancabbage.net/sam/priorityq/npq
Send-20                          13.56n  ± 0%
Recv-20                          13.51n  ± 0%
ConcSendRecv-20                 176.3n   ± 8%
HighContention-20               121.0n   ± 0%

pkg: gogs.humancabbage.net/sam/priorityq/pq
Send-20                          18.79n  ± 1%
Recv-20                         268.1n   ± 3%
ConcSendRecv-20                 199.2n   ± 2%
HighContention-20               440.0n   ± 1%

pkg: gogs.humancabbage.net/sam/priorityq/binheap
Insert-20                        11.92n  ± 0%
Extract-20                      261.6n   ± 2%
RepeatedInsertExtract-20         25.68n  ± 1%

pkg: gogs.humancabbage.net/sam/priorityq/circ
Push-20                           2.196n ± 1%
Pop-20                            2.187n ± 0%

Background

This module was inspired by a reddit post wherein /u/zandery23 asked how to implement a prioritized message queue in Go. A fantastic solution was provided by /u/Ploobers. That's probably right for 99 out of 100 use cases, but it's not completely precise.

Particularly, the second select block does not guarantee that an item from the prioritized queue will be taken if there is also an item in the regular queue.

select {
case job := <-mq.priorityQueue:
    // ...
case job := <-mq.regularQueue:
    // ...
// ...
}

From the Go Language Specification:

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection.

Thus, it is possible for the second case to be chosen even if the first case is also ready.