Защита без блокировки для синхронизированного захвата / выпуска

У меня есть общий ресурс tempfile, который разделен на куски по 4K (или какое-то подобное значение). Каждый 4K в файле представлен индексом, начинающимся с нуля. Для этого общего ресурса я отслеживаю используемые индексы блоков 4K и всегда возвращаю самый низкий индексированный блок 4K, который не используется, или -1, если все используются.

Этот класс ResourceSet для индексов имеет открытый метод получения и выпуска, оба из которых используют синхронизированную блокировку, длительность которой примерно равна длительности генерации 4-х случайных чисел (дорого, с точки зрения процессора).

Поэтому, как вы можете видеть из следующего кода, я использую AtomicInteger "подсчитывающий семафор", чтобы предотвратить одновременное поступление большого количества потоков в критическую секцию при приобретении (), возвращая -1 (сейчас недоступно), если слишком много тем.

В настоящее время я использую константу 100 для узкого цикла CAS, чтобы попытаться увеличить атомарное целое число при получении, и константу 10 для максимального количества потоков, которые затем допускаются в критическую секцию, что достаточно долго для создания конфликта, Мой вопрос заключается в том, какими должны быть эти константы для механизма сервлетов с умеренной или высокой нагрузкой, который имеет несколько потоков, пытающихся получить доступ к этим 4K-блокам?

public class ResourceSet {

    // ??? what should this be
    // maximum number of attempts to try to increment with CAS on acquire
    private static final int    CAS_MAX_ATTEMPTS = 50;

    // ??? what should this be
    // maximum number of threads contending for lock before returning -1 on acquire
    private static final int    CONTENTION_MAX = 10;

    private AtomicInteger        latch = new AtomicInteger(0);

    ... member variables to track free resources

    private boolean aquireLatchForAquire ()
    {
        for (int i = 0; i < CAS_MAX_ATTEMPTS; i++) {
            int val = latch.get();
            if (val == -1)
                throw new AssertionError("bug in ResourceSet");        // this means more threads than can exist on any system, so its a bug!
            if (!latch.compareAndSet(val, val+1))
                continue;
            if (val < 0 || val >= CONTENTION_MAX) {
                latch.decrementAndGet();
                // added to fix BUG that comment pointed out, thanks!
                return false;
            }
        }
        return false;
    }

    private void aquireLatchForRelease ()
    {
        do {
            int val = latch.get();
            if (val == -1)
                throw new AssertionError("bug in ResourceSet");    // this means more threads than can exist on any system, so its a bug!
            if (latch.compareAndSet(val, val+1))
                return;
        } while (true);
    }

    public ResourceSet (int totalResources)
    {
        ... initialize
    }

    public int acquire (ResourceTracker owned)
    {        
        if (!aquireLatchForAquire())
            return -1;

        try {
            synchronized (this) {
                ... algorithm to compute minimum free resoource or return -1 if all in use
                return resourceindex;
            }
        } finally {
            latch.decrementAndGet();
        }
    }

    public boolean release (ResourceIter iter)
    {
        aquireLatchForRelease();
        try {
            synchronized (this) {
                ... iterate and release all resources
            }
        } finally {
            latch.decrementAndGet();
        }
    }
}

3 ответа

Решение

Написание хорошего и производительного спин-блокировки на самом деле довольно сложно и требует хорошего понимания барьеров памяти. Простой выбор константы не приведет к ее сокращению и определенно не будет переносимым. В gperftools от Google есть пример, который вы можете посмотреть, но, вероятно, он намного сложнее, чем вам нужно.

Если вы действительно хотите уменьшить конкуренцию за блокировку, вы можете рассмотреть возможность использования более тонкой и оптимистичной схемы. Простым может быть разделение ваших чанков на n групп и связывание блокировки с каждой группой (также называемой разборкой). Это поможет уменьшить конкуренцию и увеличить пропускную способность, но не поможет уменьшить задержку. Вы также можете связать AtomicBoolean с каждым чанком и CAS, чтобы получить его (повторите попытку в случае сбоя). Будьте осторожны при работе с алгоритмами без блокировок, потому что они, как правило, сложно сделать правильно. Если вы все сделаете правильно, это может значительно сократить время получения чанка.

Обратите внимание, что трудно предложить более детальный подход, не зная, как выглядит ваш алгоритм выбора фрагментов. Я также предполагаю, что у вас действительно есть проблема с производительностью (она была профилирована и все такое).

Пока я в этом, ваша реализация спин-блокировки несовершенна. Вы никогда не должны вращаться непосредственно на CAS, потому что вы спам-барьеры памяти. Это будет невероятно медленно с любым серьезным спором (связанным с проблемой громового скота). Минимум будет состоять в том, чтобы сначала проверить переменную на доступность перед вашим CAS (просто, если нет барьера, то чтение будет делать). Еще лучше, если бы все ваши потоки не вращались с одинаковым значением. Это должно предотвратить связывание строки кэша между вашими ядрами.

Обратите внимание, что я не знаю, какой тип барьеров памяти связан с атомарными операциями в Java, поэтому мои приведенные выше предложения могут быть неоптимальными или правильными.

Наконец, "Искусство многопроцессорного программирования" - это забавная книга, которую можно почитать, чтобы лучше познакомиться со всеми бессмысленными словами, которые я излагал в этом ответе.

Ты можешь использовать Semaphore"s tryAcquire метод, если вы хотите, чтобы ваши темы блокировались no resource available,

Я бы просто подставил вашу synchronized Ключевое слово с ReentrantLock и использовать tryLock() метод на это. Если вы хотите, чтобы ваши темы немного подождали, вы можете использовать tryLock(timeout) на том же классе. Какой из них выбрать и какое значение использовать для тайм-аута, необходимо определить с помощью теста производительности.

Создание явных ворот кажется тем, что вы делаете, мне кажется ненужным. Я не говорю, что это никогда не может помочь, но IMO, скорее всего, на самом деле повредит производительности, и это, безусловно, дополнительное осложнение. Таким образом, если у вас нет проблем с производительностью (на основе проведенного вами теста) и вы не обнаружили, что этот тип гейтинга помогает, я бы порекомендовал перейти к самой простой реализации.

Я не уверен, нужно ли создавать собственный класс Lock для этого сценария. Поскольку JDK предоставил ReentrantLock, который также использует инструкцию CAS во время захвата блокировки. Производительность должна быть довольно хорошей по сравнению с вашим личным классом блокировки.

Другие вопросы по тегам