воскресенье, 20 декабря 2009 г.

Unfair lock can produce VERY ASYMMETRICAL results

Ceki Gülcü (author of Log4j) пишет в своем блоге про крайне неравномерные (максимально неравномерные) результаты при использовании synchronized:
"
runnable[0]: counter=1002
runnable[1]: counter=0
runnable[2]: counter=0
runnable[3]: counter=0
runnable[4]: counter=0
"
При том, что ожидалось "примерно по 200".

Это к тому, что
1) симметричные уравнения и симметричные граничные условия могут давать асимметричные решения;
2) unfair примитивы синхронизации МОГУТ, но НЕ ОБЯЗАНЫ обеспечивать "статистическую честность".

P.S. Кстати, всегда интересовало что говорят заказчику, если в проекте используются unfair примитивы синхронизации, которые могут привести к такому поведению (фактически livelock для некоторых потоков), но во всех тестах ведут себя "хорошо". Что будет если в какой-то момент времени заказчик потеряет деньги из-за таких вот особенностей поведения ПО? Т.е. считать ли проект "выполненным/готовым"?

9 комментариев:

  1. Ceki Gülcü написал бред.

    Если он хотел посмотреть до куда досчитает thread-private counter в каждом трэде, то не надо было делать synchronized(LOCK){counter++}.

    Если же он хотел посчитать throughput для shared counter-а(до куда он досчитается в условиях конкурентной среды), то надо было сделать counter shared-ом.

    На самом деле он доказал что biased locking рулит: пока в jdk1.5 thread-scheduler "даёт играеться" в concurrency всем желающим thread-ам(синхронизирует их working memory с main memory, т.е. делает context switching) и тем самым накручивает counter только до 201, в jdk1.6 - только "hot" thread-ы(чей working memory еще тёпленький) получает LOCK, и тем самым counter накручивается до 1002!

    ОтветитьУдалить
  2. Я полагаю Ceki рассматривал немного иную ситуацию:
    допустим Вы создаете экземпляр класса
    class MyConsole {
    public void synchronized printIt(String msg) {...}
    }
    это будет Singleton для Вашего сервера. Сервер использует Thread-per-Request. То может оказаться, что весьма симметрично делящий процессорное время между потоками шедулер в осутствии вызовов "synchrinized printIt(....)" из потоков пользователей будет очень неравномерно делить время если этот метод вызывать.
    Т.е. обеспечивая свойства Visibility и Mutual Exclusion лочка типа synchronized приводит к совершенно неприемлемым перекосам и, соответственно, в большинстве случаев ее просто необходимо заменить на, скажем, j.u.c.ReentrantLock(true).

    ОтветитьУдалить
  3. Т.е. выходит что любой Singleton защищенный synchronized неприемлем для Thread-per-Request.

    ОтветитьУдалить
  4. Пример такого синглетона из JDK:
    java.util.logging.LogManager {
    public static java.util.logging.LogManager getLogManager() {...}
    public synchronized java.util.logging.Logger getLogger(java.lang.String s) {...}
    }
    т.е. вызов в потоках
    LogManager.getLogManager().getLogger(...) приводит к "перекосам" в шедулере.

    ОтветитьУдалить
  5. Чтобы случился перекос нужно иметь код по типу:

    new Runnable {
    void run() {
    for(;;) {
    // NO CODE HERE
    console.printIt(s);
    // NO CODE HERE
    }
    }
    }

    Т.е. все thread-ы должны только того и делать что писать в консоль. Это нереально. Обычно все имеют дело с кодом типа:

    new Runnable {
    void run() {
    for(;;) {
    console.printIt("logging in " + env);
    logService.login(env)
    console.printIt("logged! " + env);
    }
    }
    }

    ОтветитьУдалить
  6. А вот еще перекос:

    // WRITER
    new Runnable() {
    void run() {
    for(;;) {
    oldStyleCounter.incr();
    }
    }
    }

    // READER
    new Runnable() {
    void run() {
    for(;;) {
    oldStyleCounter.get();
    }
    }
    }

    Но стоит добавить Thread.sleep() в одном из них и перекос исчезнет.

    Т.е. говоря строго академически то да - будет "перекос", но если практичнее посмотреть - то его не будет.

    ОтветитьУдалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. Хорошо, но что будет если синхронизирован метод синглетона, который работает долго. Скажем некоторый org.apache.log4j.FileAppender у которого setBufferedIO(false) или соотношение BufferSize и сообщения(большое, скажем очень объемный stacktrace) таково, что каждое обращение doAppend(LoggingEvent event) будет вызывать запись в файл?
    Как по Вашему - будет ли перекос в таком случае? Окажется ли что, операция логирования, по факту, доступна только одному потоку?
    ----
    Я согласен, что ситуация немного синтетическая. Просто я, как программист такой "кривой" системы или системы в таком "кривом" режиме ожидаю, что все потоки будут "тормозить" "примерно равномерно", но окажется, что прогресс наблюдается исключительно у одного потока.
    ----
    Заранее согласен, что синхронизировать долгий метод синглетона - зло. Но вдруг он долгим стал при некоторых неожиданных условиях. В моем примере с FileAppender дефолтный размер буфера - 8К. Вот допустим, что полезли огромные стектрейсы из всех потоков(соизмеримые с 8К), но в логе будут только от одного потока - того, что первый начал писать.

    ОтветитьУдалить
  9. Согласен, что ситуации синтетические, но лично для меня и результаты немного неожиданны. Ожидал, что программисты synchronized сделают некоторую "статистическую неабсолютную нечестность", т.е. 1)на колве обращений стремящемся к бесконечности и конечном кол-ве потоков(К) каждый получит долю не стремящуюся к нулю (не обязательно стремящуюся к 1/К), 2)на колве обращений стремящемся к бесконечности и конечном кол-ве потоков(К) никакой поток не будет генерить неограниченно растущих последовательных цепочек.

    ОтветитьУдалить