Как избежать одновременного выполнения трудоемкой задачи без блокировки?

1

Я хочу эффективно избегать параллельного выполнения трудоемкой задачи в сильно многопоточной среде, не заставляя потоки ждать блокировки, когда другой поток уже запускает задачу. Вместо этого в этом сценарии я хочу, чтобы они изящно терпели неудачу (т.е. пропустили попытку выполнить задачу) как можно быстрее. Другими словами: мне нужно попытаться снова запустить задачу, когда она уже выполняется, чтобы немедленно отступить, предпочтительно без стоимости синхронизации.

Чтобы проиллюстрировать идею, подумайте об этом небезопасном (есть условие гонки!):

private static boolean running = false;

public void launchExpensiveTask() {
    if (running) return; // Do nothing

    running = true;
    try {
        runExpensiveTask();
    } finally {
        running = false;
    }
}

Я хотя бы об использовании варианта Double-Checked Locking (считают, что running является примитивным 32-битным полем, поэтому он может работать нормально даже для Java ниже 5 без необходимости volatile). Это может выглядеть так:

private static boolean running = false;
private static Object execLock = new Object();

public void launchExpensiveTask() {
    if (running) return; // Do nothing

    synchronized (execLock) {
        if (running) return;

        running = true;
        try {
            runExpensiveTask();
        } finally {
            running = false;
        }
    }
}

Возможно, мне также следует использовать локальную копию поля (не уверен сейчас, скажите, пожалуйста).

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

Итак, вы могли бы подумать о лучшем подходе?

EDIT: я ранее пропустил часть контекста, для правильности здесь мне нужно поддерживать блокировку во время выполнения, чтобы удерживать другие методы, пытающиеся изменить какое-то внутреннее общее состояние. Чтобы быть справедливым, я до сих пор поддерживал полезные ответы, включая оба случая: с необходимостью блокировки и без нее после запуска задачи.

  • 1
    Лучший подход к параллельному программированию на JVM обычно включает Akka . Оставьте этот старомодный synchronized хлам и присоединяйтесь к будущему :)
  • 0
    Да, мне действительно нравится это и даже больше со Scala, но это решение не для меня в этом проекте. В любом случае, ценное предложение, спасибо.
Показать ещё 2 комментария
Теги:
concurrency
synchronization

5 ответов

1
Лучший ответ

С помощью Lock # tryLock() (API, доступного с Java 5), мы можем сделать это без блокировки:

private static boolean running = false;
private static Lock execLock = new ReentrantLock();

public void launchExpensiveTask() {
    if (running) return; // fast exit without sync

    if (!execLock.tryLock()) return; // quit if lock is not free

    try {
        running = true;
        runExpensiveTask();
    } finally {
        running = false;
        execLock.unlock();
    }

}

Если вам не нужно удерживать блокировку во время выполнения задачи, посмотрите следующий код:

private static boolean running = false;
private static Object execLock = new Object();

private boolean start() {
    synchronized (execLock) {
        boolean ret = running;
        running = true;
        return ret;
    }
}

private void end() {
    synchronized (execLock) {
        running = false;
    }
}

public void launchExpensiveTask() {
    if (running) return; // fast exit without sync

    if (start()) return; // already running, do nothing

    try {
        runExpensiveTask();
    } finally {
        end();
    }
}
3

Я думаю, что это имеет немного больше смысла:

 static volatile Boolean running = false;

    public static void launchTask()
    {
        synchronized(running)
        {
            if(running) return;
            running = true;
        }
            //DOSTUFF
            running = false;
    }

Поскольку вам действительно нужно только синхронизировать установку boolean: если несколько потоков задают одновременно, первая будет работать с true, а все остальные будут возвращены.

Тем не менее, для вашего дизайна может быть лучший общий шаблон. Что делать, если потоки отправили запросы в очередь (An ExecutorService?), Получили объекты Future или ListenableFuture (из Guava), а затем продолжали делать другие вещи до тех пор, пока фьючерсы не закончили свои вычисления?

  • 0
    Вы подняли хороший вопрос о том, чтобы не синхронизировать больше, чем нужно (я собираюсь уточнить это в вопросе). Upvote для вас :)
  • 0
    Я обеспокоен тем, что последняя запись в running не синхронизирована, особенно учитывая автобокс под капотами. Вы уверены, что этот код является потокобезопасным?
Показать ещё 10 комментариев
1

Имейте в виду, что для большинства людей предпочтительным будет следующее решение:

private static Lock execLock = new ReentrantLock();

public void launchExpensiveTask() {
    if (!execLock.tryLock()) return; // skip if already running

    try {
        runExpensiveTask();
    } finally {
        lock.unlock();
    }
}

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

1

Не обращайте внимания на мой другой ответ. Но это то, что вы ищете. http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/Semaphore.html

Используя семафоры. Самый простой способ думать о семафоре - рассмотреть его как абстракцию, которая позволяет n единицам быть приобретены и предлагает механизмы получения и освобождения. TryAcquire является ключевым, потому что согласно документации java - приобретает разрешение от этого семафора, только если он доступен во время вызова. Попробуйте это для себя.

    private Semaphore semaphore = new Semaphore(1);

    public void launchExpensiveTask() {
        if (semaphore.tryAcquire()) {
            try {
               runExpensiveTask();
            } finally {
               semaphore.release();
            }
        }

    }
  • 0
    Мне нравится простота вашего кода, но этого недостаточно для удовлетворения моих потребностей. Кроме того, здесь нет реальной причины предпочитать двоичный семафор над ReentrantLock (см. Мой ответ ), поскольку все, что вы здесь делаете с двоичным семафором, может быть также сделано с помощью ReentrantLock и безопаснее.
  • 0
    Что касается моего предыдущего утверждения о двоичных семафорах, пожалуйста, взгляните на этот ответ .
Показать ещё 3 комментария
0

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

package toys;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class TwoQueues {

//tweak it for your purpose.
private final int CPU_COUNT = 4;
private BlockingQueue<Runnable> lightTaskQueue = new LinkedBlockingDeque<Runnable>();
private ThreadPoolExecutor lightExecutor = new ThreadPoolExecutor(CPU_COUNT, CPU_COUNT, 60L, TimeUnit.SECONDS, lightTaskQueue);
private BlockingQueue<Runnable> heavyTaskQueue = new LinkedBlockingDeque<Runnable>();
private ThreadPoolExecutor heavyExecutor = new ThreadPoolExecutor(1, 1, 60L, TimeUnit.SECONDS, heavyTaskQueue);

public static class SampleLightTask implements Runnable {

    @Override
    public void run() {
        System.out.println("I am " + this + " and running fast!");
    }

}

private static AtomicBoolean heavyTaskRunning = new AtomicBoolean();

public static class SampleHeavyTask implements Runnable {

    @Override
    public void run() {
        try {
            heavyTaskRunning.set(true);
            System.out.println("I am " + this + " and running quite slow!");
            final long start = System.currentTimeMillis();
            while (true) {
                //burn the CPU for ten senconds.
                if (System.currentTimeMillis()-start >= 10000L)
                    break;
            }
        } finally {
            heavyTaskRunning.set(false);;
        }
    }

}

public void shutDownNow() {
    this.lightExecutor.shutdownNow();
    this.heavyExecutor.shutdownNow();
}

public void runOrQueueLightTask(SampleLightTask lightOne) {
    this.lightExecutor.execute(lightOne);
}

public void runOrQueueHeavyTask(SampleHeavyTask heavyOne) {
    if (heavyTaskRunning.get()) {
        System.out.println("running, skipped new one: " + heavyOne);
        return;
    }

    this.heavyExecutor.execute(heavyOne);
}

public static void main(String[] args) throws Exception {
    TwoQueues q = new TwoQueues();

    final long start = System.currentTimeMillis();

    //Run the queues for 30 seconds, add CPU-light and CPU-weight tasks
    //every second.
    while (System.currentTimeMillis()-start<=30*1000L) {
        q.runOrQueueHeavyTask(new SampleHeavyTask());
        q.runOrQueueLightTask(new SampleLightTask());
        Thread.sleep(1000L);
    }

    q.shutDownNow();
}
}

И работающий выход:

I am toys.TwoQueues$SampleHeavyTask@6d0cecb2 and running quite slow!
I am toys.TwoQueues$SampleLightTask@6b87d20c and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@2ce07e6b
I am toys.TwoQueues$SampleLightTask@7fa0d111 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@16fdf48d
I am toys.TwoQueues$SampleLightTask@5fbd7d0e and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@115d533d
I am toys.TwoQueues$SampleLightTask@59c27402 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@6d4e5d57
I am toys.TwoQueues$SampleLightTask@33d232d1 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@79ec3264
I am toys.TwoQueues$SampleLightTask@1e081c5 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@3a67ad79
I am toys.TwoQueues$SampleLightTask@6cae00e3 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@13bc6ed3
I am toys.TwoQueues$SampleLightTask@380fe8c4 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@1c7ab89d
I am toys.TwoQueues$SampleLightTask@3cee5a06 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@44585f2a
I am toys.TwoQueues$SampleLightTask@5cfe174 and running fast!
I am toys.TwoQueues$SampleLightTask@12da89a7 and running fast!
I am toys.TwoQueues$SampleHeavyTask@49833c9c and running quite slow!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@47004b78
I am toys.TwoQueues$SampleLightTask@645ad7b2 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@8071a97
I am toys.TwoQueues$SampleLightTask@a62b39f and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@55fe910c
I am toys.TwoQueues$SampleLightTask@3be4d6ef and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@2cdb03a1
I am toys.TwoQueues$SampleLightTask@5ecb5608 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@777d57d6
I am toys.TwoQueues$SampleLightTask@4611dfe3 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@3f81d405
I am toys.TwoQueues$SampleLightTask@6486b4d5 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@47ca3f82
I am toys.TwoQueues$SampleLightTask@2f0f94a0 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@27e6ac83
I am toys.TwoQueues$SampleLightTask@1947e0ec and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@3dffb2eb
I am toys.TwoQueues$SampleLightTask@5e3b8219 and running fast!
I am toys.TwoQueues$SampleLightTask@14da67a4 and running fast!
I am toys.TwoQueues$SampleHeavyTask@eca4aae and running quite slow!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@2eced18
I am toys.TwoQueues$SampleLightTask@10c1c428 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@213526b0
I am toys.TwoQueues$SampleLightTask@287efdd8 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@294b84ad
I am toys.TwoQueues$SampleLightTask@1cf38f09 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@3a33a6b8
I am toys.TwoQueues$SampleLightTask@150697e2 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@63dd8136
I am toys.TwoQueues$SampleLightTask@634e3372 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@2313b44d
I am toys.TwoQueues$SampleLightTask@62a23d38 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@9615a1f
I am toys.TwoQueues$SampleLightTask@5663ae08 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@2a36bb87
I am toys.TwoQueues$SampleLightTask@6f51b1b7 and running fast!
running, skipped new one: toys.TwoQueues$SampleHeavyTask@5c6a9e79
I am toys.TwoQueues$SampleLightTask@5bca4955 and running fast!

//////////////////////////////////Старый ответ ////////////////////////////////////

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

Почему бы не использовать два пула потоков и две очереди для этого? это идеальное совпадение. Для задач типа A назначьте их в пуле потоков с максимальным параллелизмом, установленным в 1; и для типа B, в другом пуле потоков с максимальным количеством параллелизма, установленном на ваши номера ядра ядра/нити или все, что подходит для ваших нужд. Здесь нет необходимости "проверять и изящно терпеть неудачу".

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

  • 0
    Но я не хочу ставить сложные задачи, мне действительно нужно, чтобы, когда он уже запущен, пропустил остальные и как можно скорее продолжил.
  • 0
    Хорошо, понял вашу проблему. Этот дизайн может решить вашу проблему с небольшим изменением: просто пропустите постановку тяжелой задачи, если она есть. Я лично до сих пор предпочитаю этот дизайн, так как он проще, выполнение тяжелых и легких задач не будет мешать друг другу, простота отладки и т. Д.
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню