Лекция 6. FAA-Based Queue & Flat Combining

FAA

FAA это Fetch-And-Add(addr, delta), атомарно увеличивает значение на delta и возвращает старое значение. Это лучше масштабируется, чем CAS цикл, так как операция всегда успешна.

Obstruction-free queue

Пусть у нас есть бесконечный массив с двумя указателями enqIdx, deqIndx. Что делать, если dequeue не увидел никакую запись? Тогда пометим ячейку как сломанную и обе операции начнутся заново.

fun enqueue(x: T) = while (true) {
    val enqIdx = FAA(&enqIdx, 1)
    if (CAS(&data[enqIdx], null, x))
    return
}

fun dequeue() = while (true) {
    if (isEmpty()) return null
    val deqIdx = FAA(&deqIdx, 1)
    val res = SWAP(&data[deqIdx], BROKEN)
    if (res == null) continue
    return res
}

fun isEmpty(): Boolean = deqIdx >= enqIdx

Это не lock-free, но на практике он почти lock-free. Кроме того, lock-free алгоритмы на практике почти всегда являются wait-free в силу честности планировщиков ОС.

В реальности у нас не бесконечный массив. Поэтому будем хранить Michal-Scott очередь сегментов.

fun enqueue(x: T) = while (true) {
    val tail = this.tail
    val enqIdx = FAA(&tail.enqIdx, 1)
    if (enqIdx >= NODE_SIZE) {
        // try to insert new node with “x”
    } else {
        if (CAS(&tail.data[enqIdx], null, x))
            return
    }
}

fun dequeue(): T = while (true) {
    val head = this.head
    val deqIdx = FAA(&head.deqIdx, 1)
    if (deqIdx >= NODE_SIZE) {
        val headNext = head.next ?: return null
        CAS(&this.head, head, headNext)
        continue
    }
    val res = SWAP(&head.data[deqIdx], BROKEN)
    if (res == null) continue
    return res
}

Тогда алгоритм lock-free.

Flat combining

Идея: будем выполнять операции последовательно, структура данных защищена блокировкой. Поток, который держит блокировку — комбайнер, он выполняет как свою, так и чужие операции. Таким образом, мы не проигрываем по кешу и не так часто берем блокировку.

Это делается через массив, в который (случайно) кладутся дескрипторы операций. Держащий блокировку поток обходит весь массив и выполняет все операции, которые видит. Массив очевидно атомарный.

Очередь на flat combining куда быстрее, чем michael-scott. Однако FAA еще быстрее. В целом flat combining полезно, когда грубая/тонкая блокировка слишком медленно, а flat combining хватает. Бывает, что flat combining быстрее чем lock-free (см. очередь Michael-Scott).

Лок для flat combining очень простой: просто атомарный бул:

var locked = false

fun tryLock() = CAS(&lock, false, true)

fun unlock() { locked = false }