Неблокирующий алгоритм для генерации уникальных отрицательных чисел - PullRequest
2 голосов
/ 24 февраля 2009

Я недавно рефакторил фрагмент кода, используемый для генерации уникальных отрицательных чисел.
edit: Несколько потоков получают эти идентификаторы и добавляют в качестве ключей к БД; числа должны быть отрицательными, чтобы их можно было легко идентифицировать - в конце сеанса тестирования они удаляются из БД.

Мой алгоритм Java выглядит следующим образом:

private final Set<Integer> seen = Collections.synchronizedSet(new HashSet<Integer>());
public Integer generateUniqueNegativeIds() {
    int result = 0;
    do {
        result = random.nextInt();
        if (result > 0) {
            result *= -1;
        }
    } while (!seen.add(result));
    return result;
}

Приведенная выше структура кода с ее умозрительным добавлением к циклу set и «retry» заставляет меня думать, что существует эквивалентный неблокирующий алгоритм, который заменяет синхронизированный набор любой из атомарных переменных .

Я сделал несколько попыток перезаписи с использованием атомарных переменных, но все они не прошли тест многопоточной атаки.

Есть ли элегантный неблокирующий эквивалент?

edit: ради любопытства вот ошибочная попытка использовать атомное целое число в качестве защитника

private final AtomicInteger atomi = new AtomicInteger(0);
public Integer generateUniqueNegativeIdsWithAtomicAlgo() {
    boolean added = false;
    int result = 0;
    do {
        result = random.nextInt();
        if (result > 0) {
            result *= -1;
        }
        if (atomi.compareAndSet(0, result)) {
            added = cache.add(result);
        }   
    } while (!added);
    return atomi.getAndSet(0);
}

изменить: Испытательный жгут ниже:

public static void main(String[] args) {
    final int NUMBER_OF_THREADS = 10000;
    final Set<Integer> uniques = Collections.synchronizedSet(new HashSet<Integer>());
    final List<Integer> positives = Collections.synchronizedList(new ArrayList<Integer>());
    final NegativeUniqueIdGenerator nuig = new NegativeUniqueIdGenerator();
    Thread[] workers = new Thread[NUMBER_OF_THREADS];
    long start = System.nanoTime();
    for (int i = 0; i < workers.length; i++) {
        Runnable runnable = new Runnable() {
            public void run() {
                int number = nuig.generateUniqueNegativeIds();
                if (number > 0) {
                    positives.add(number);
                }
                uniques.add(number);
            }
        };
        workers[i] = new Thread(runnable);
        workers[i].start();
    }
    for (int i = 0; i < workers.length; i++) {
        try {
            workers[i].join();
        } catch (InterruptedException ie) {}
    }
    long end = System.nanoTime();
    System.out.println(String.format("duration = %dns", (end - start)));
    System.out.println(String.format("#threads = %d", NUMBER_OF_THREADS));
    System.out.println(String.format("#uniques = %d", uniques.size()));
    System.out.println(String.format("#positives = %d", positives.size()));
    System.out.println(String.format("#duplicates = %d", NUMBER_OF_THREADS - uniques.size()));
    System.out.println(String.format("ratio = %f",
            ((double) NUMBER_OF_THREADS - uniques.size())
                    / NUMBER_OF_THREADS));
    assert uniques.size() == NUMBER_OF_THREADS;
}

Ответы [ 8 ]

9 голосов
/ 24 февраля 2009

Если вас не беспокоит случайность, вы можете просто уменьшить счетчик, например так:

private final AtomicInteger ai=new AtomicInteger(0);

public int nextID() {
  return ai.addAndGet(-1);
}

Edit:

Для случайных чисел вы можете просто использовать свое решение и использовать, например. ConcurrentHashMap или ConcurrentSkipListSet вместо синхронизированного набора. Вы должны убедиться, что разные потоки используют разные экземпляры генератора случайных чисел, и что эти генераторы не коррелированы.

6 голосов
/ 24 февраля 2009

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

Почему?

По сути, вероятность того, что вы получите повторяющееся целое число, очень очень (очень) (очень) мала, примерно 1, деленная на число целых чисел, которые вы еще не видели. Если вы уже сгенерировали N чисел, ожидаемое время выполнения алгоритма приблизительно линейно по N с коэффициентом 1/2 ^ 32, что означает, что вам нужно сгенерировать более миллиарда чисел, чтобы получить ожидаемое время выполнения превысит 2 итерации цикла! На практике проверка набора на наличие определенного числа сделает гораздо больше для расширения времени выполнения вашего алгоритма, чем возможность повторения цикла (ну, если вы не используете HashSet, может быть - я забыл, что его асимптотическое время выполнения есть).

Для чего стоит, точное ожидаемое количество итераций цикла составляет

2^64/(2^32 - N)^2

После того, как вы сгенерировали миллион чисел, это сработает до 1.00047 - что означает, скажем, чтобы сгенерировать числа от 1 000 001 до 1 002 000 000, вы, вероятно, получите одно повторное число, всего во всех этих звонках.

3 голосов
/ 24 февраля 2009

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

2 голосов
/ 25 февраля 2009

В масштабной библиотеке есть NonBlockingHashSet, который вы можете использовать. Просто замените установленный экземпляр экземпляром NonBlockingHashSet, и все готово.

http://sourceforge.net/projects/high-scale-lib

2 голосов
/ 25 февраля 2009

Я бы объединил ответ ОП с ответом jpalecek:

private final AtomicInteger ai=new AtomicInteger(0);

public int nextID() {
    return ai.addAndGet(-1 - random.nextInt(1000));
}
2 голосов
/ 25 февраля 2009
2 голосов
/ 24 февраля 2009

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

Например, использование 32-битного генератора XORShift произведет все 2 ^ 31 отрицательных 4-байтовых целых в «случайном» порядке перед повторением шаблона. Если вам нужно больше чисел, чем это, вы, вероятно, не хотите помещать их в хэш-набор. Так что-то вроде этого (предупреждение: непроверенный код ...):

int seed = (int) System.nanoTime();
final int origSeed = seed;

public int nextUniqueNegativeNumber() {
  int n = seed;
  do {
    n ^= (n << 13);
    n ^= (n >>> 17);
    n ^= (n << 5);
    seed = n;
    if (n == origSeed) {
      throw new InternalError("Run out of numbers!");
    }
  } while (n > 0);
  return n;
}

Я оставлю на усмотрение читателя конвертировать "seed" в использование AtomicInteger, если требуется параллелизм ...

Редактировать: на самом деле, чтобы оптимизировать параллельный случай, вы, возможно, захотите записать обратно в "seed" после получения следующего отрицательного номера.

ОК, по многочисленным просьбам атомная версия будет выглядеть примерно так:

  AtomicInteger seed = new AtomicInteger((int) System.nanoTime());

  public int nextUniqueNegativeNumber() {
    int oldVal, n;
    do {
      do {
        oldVal = seed.get();
        n = oldVal ^ (oldVal << 13); // Added correction
        n ^= (n >>> 17);
        n ^= (n << 5);
      } while (seed.getAndSet(n) != oldVal);
    } while (n > 0);
    return n;
  }
1 голос
/ 24 февраля 2009

Я думаю, что вы имеете в виду неблокирующее и реентерабельное.

edit: (заменяет мой оригинал, потому что это намного лучше)

Только что пришла в голову потоковая опция, которая на самом деле довольно производительная (по крайней мере, более производительная, чем ваш оригинал). Если вы создали слабую хэш-карту с объектом потока в качестве «Ключа» и «Значения», поместите объект с возможностью создания серии, скажем, 1000 чисел из определенного диапазона.

Таким образом, вы назначаете каждому потоку свой собственный диапазон номеров 1000, из которого он будет выделяться. Когда у объекта заканчиваются числа, пусть он вернет недопустимое число (0?), И вы будете знать, что вам нужно выделить новый диапазон этому объекту.

Не было бы никакой синхронизации где-либо (edit: whoops, было немного неправильно. См. Ниже), слабая хеш-карта автоматически освобождала нити, которые были уничтожены (без специального обслуживания), а самой медленной частью был бы поиск одного хеш нити, которая на самом деле очень быстро.

получить текущий запущенный поток с:

Thread currThread=Thread.getCurrentThread();

Также я могу ошибаться, и вам просто нужно синхронизировать метод, тогда это будет работать:

int n=-1;
synchronized int getNegativeNumber() {
    return n--;
}

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

Хотелось бы, чтобы был способ создать связанный кодовый блок вместо встроенного, как это, не уходя с сайта - извините за длину.

package test;

import java.util.WeakHashMap;

public class GenNumber {
    // Static implementation goes first.
    private static int next = -1;
    private static final int range = 1000;

    private static WeakHashMap<Thread, GenNumber> threads = new WeakHashMap<Thread, GenNumber>();

    /**
     * Generate a unique random number quickly without blocking
     * 
     * @return the random number < 0
     */
    public static int getUniqueNumber() {
        Thread current = Thread.currentThread();
        int next = 0;

        // Have to synchronize some, but let's get the very
        // common scenario out of the way first without any
        // synchronization. This will be very fast, and will
        // be the case 99.9% of the time (as long as range=1000)
        GenNumber gn = threads.get(current);
        if (gn != null) {
            next = gn.getNext();
            if (next != 0)
                return next;
        }

        // Either the thread wasn't found, or the range was
        // used up. Do the rest in a synchronized block.
        // The three lines tagged with the comment "*" have
        // the potential to collide if this wasn't synchronized.
        synchronized (threads) {
            if (gn == null) {
                gn = new GenNumber(next -= range); // *
                threads.put(current, gn); // *
                return gn.getNext(); // can't fail this time
            }
            // now we know the range has run out

            gn.setStart(next -= range); // *
            return gn.getNext();
        }
    }

    // Instance implementation (all private, nobody needs to see this)
    private int start;
    private int count;

    private GenNumber(int start) {
        setStart(start);
    }

    private int getNext() {
        if (count < range)
            return start - count;
        return 0;
    }

    private GenNumber setStart(int start) {
        this.start = start;
        return this;
    }
}

Меня просто поразило, что вместо одного большого синхронизированного блока можно заменить 2 очень маленьких, синхронизированных на разных объектах, один для "+ = count" и один для .put (). Если коллизии все еще замедляют вас, это может помочь (хотя, если коллизии все еще замедляют вас (ДЕЙСТВИТЕЛЬНО ???), вам лучше обслужить, просто повысив счет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...