等待异步任务的安全有效方法

时间:2019-03-07 22:11:25

标签: java multithreading concurrency

在系统中,我有一个对象-我们称之为TaskProcessor。它保存任务队列,这些任务由一些线程池(ExecutorService + PriorityBlockingQueue)执行 每个任务的结果都以唯一标识符的形式保存在数据库中。

知道此唯一标识符的用户可以检查此任务的结果。结果可能在数据库中,但是任务仍然可以在队列中等待执行。在这种情况下,UserThread应该等待任务完成。

此外,以下假设是有效的:

  • 其他人可以将任务排队TaskProcessor,如果他知道唯一标识符,则随机的UserThread可以访问结果。

  • UserThreadTaskProcess在同一应用中。 TaskProcessor包含一个线程池,而UserThread就是servlet线程。

  • 询问结果时,应阻止
  • UserThread,并且结果尚未完成。 UserThread应在TaskProcessor完成由唯一标识符分组的一个或多个任务后立即解除阻止

我的第一次尝试(天真)是在循环中检查结果并休眠一段时间:

// UserThread
while(!checkResultIsInDatabase(uniqueIdentifier))
  sleep(someTime)

但是我不喜欢它。首先,我在浪费数据库连接。此外,如果任务在睡眠后立即完成,那么即使结果刚刚出现,用户也将等待。

下次尝试基于等待/通知:

//UserThread 
while (!checkResultIsInDatabase())
  taskProcessor.wait()

//TaskProcessor
... some complicated calculations
this.notifyAll()

但是我也不喜欢。如果更多的UserThreads将使用TaskProcessor,那么每次完成某些任务时,它们都会被不必要地唤醒,而且它们还会进行不必要的数据库调用。

最后一次尝试是基于我称为waitingRoom的东西:

//UserThread
Object mutex = new Object();
taskProcessor.addToWaitingRoom(uniqueIdentifier, mutex)
while (!checkResultIsInDatabase())
  mutex.wait()

//TaskProcessor
... Some complicated calculations
if (uniqueIdentifierExistInWaitingRoom(taskUniqueIdentifier))
  getMutexFromWaitingRoom(taskUniqueIdentifier).notify()

但这似乎并不安全。在数据库检查和wait()之间,任务可以完成(notify()尚未生效,因为UserThread尚未调用wait()),可能会导致死锁

看来,我应该在某个地方同步它。但是,恐怕它不会有效。 有没有办法纠正我的任何尝试,使它们安全有效?也许还有其他更好的方法可以做到这一点?

4 个答案:

答案 0 :(得分:10)

您似乎正在寻找某种未来 / 承诺抽象。看一下CompletableFuture,从Java 8开始可用。

CompletableFuture<Void> future = CompletableFuture.runAsync(db::yourExpensiveOperation, executor);

// best approach: attach some callback to run when the future is complete, and handle any errors
future.thenRun(this::onSuccess)
        .exceptionally(ex -> logger.error("err", ex));

// if you really need the current thread to block, waiting for the async result:
future.join(); // blocking! returns the result when complete or throws a CompletionException on error

您还可以从异步操作中返回一个(有意义的)值,并将结果传递给回调。要使用此功能,请查看supplyAsync()thenAccept()thenApply()whenComplete()等。

您还可以将多个期货组合成一个或更多。

答案 1 :(得分:4)

我相信在mutex方法中将CountDownLatch替换为waitingRoom可以防止死锁。

CountDownLatch latch = new CountDownLatch(1)
taskProcessor.addToWaitingRoom(uniqueIdentifier, latch)
while (!checkResultIsInDatabase())
  // consider timed version
  latch.await()

//TaskProcessor
... Some complicated calculations
if (uniqueIdentifierExistInWaitingRoom(taskUniqueIdentifier))
  getLatchFromWaitingRoom(taskUniqueIdentifier).countDown()

答案 2 :(得分:2)

使用CompletableFutureConcurrentHashMap,您可以实现它:

/* Server class, i.e. your TaskProcessor */
// Map of queued tasks (either pending or ongoing)
private static final ConcurrentHashMap<String, CompletableFuture<YourTaskResult>> tasks = new ConcurrentHashMap<>();

// Launch method. By default, CompletableFuture uses ForkJoinPool which implicitly enqueues tasks.
private CompletableFuture<YourTaskResult> launchTask(final String taskId) {
    return tasks.computeIfAbsent(taskId, v -> CompletableFuture // return ongoing task if any, or launch a new one
            .supplyAsync(() -> 
                    doYourThing(taskId)) // get from DB or calculate or whatever
            .whenCompleteAsync((integer, throwable) -> {
                if (throwable != null) {
                    log.error("Failed task: {}", taskId, throwable);
                }
                tasks.remove(taskId);
            })
    );


/* Client class, i.e. your UserThread */
// Usage
YourTaskResult taskResult = taskProcessor.launchTask(taskId).get(); // block until we get a result

每当用户要求taskId的结果时,他们要么:

  • 如果他们是第一个要求此taskId的人,则请他们排队;或
  • 如果其他人先将其排队,则获取ID为taskId的正在进行的任务的结果。

这是当前数百个用户同时使用的生产代码
在我们的应用程序中,用户通过REST端点(每个用户在其自己的线程上)请求任何给定的文件。我们的taskId是文件名,而我们的doYourThing(taskId)是从本地文件系统中检索文件,或者从不存在的S3存储桶中下载文件。
显然,我们不想一次下载同一文件。使用我实现的此解决方案,任何数量的用户都可以在相同或不同的时间请求相同的文件,并且该文件将被下载一次。所有在下载时要求它的用户都将在下载完成的同时获得它。所有稍后要求它的用户,都将立即从本地文件系统中获取它。

像魅力一样

答案 3 :(得分:0)

我从问题详细信息中了解到的是-

当UserThread请求结果时,有3种可能性:

  1. 任务已经完成,因此不会阻塞用户线程,直接从数据库获取结果。
  2. 任务处于队列中或正在执行中,但尚未完成,因此请阻塞用户线程(到现在为止应该没有任何数据库查询),并且紧接任务完成之后(此时任务结果必须保存在DB中),取消阻止用户线程(现在用户线程可以查询数据库以获取结果)
  3. 用户请求的给定uniqueIdentifier从未提交任何任务,在这种情况下,db的结果为空。

对于第1点和第3点,它很简单,不会对UserThread造成任何阻塞,只需从DB查询结果即可。

对于第2点-我编写了TaskProcessor的简单实现。在这里,我使用ConcurrentHashMap来保留当前尚未完成的任务。该映射包含UniqueIdentifier和相应任务之间的映射。我使用了computeIfPresent()的{​​{1}}(在JAVA-1.8中引入)方法,该方法保证对于相同的键,该方法的调用是线程安全的。以下是java doc所说的内容: Link

  

如果存在指定键的值,则尝试计算   给定键及其当前映射值的新映射。整个   方法调用是原子执行的。一些尝试更新   其他线程对此地图进行的操作可能会在   计算正在进行中,因此计算应简短且   简单,并且不得尝试更新此地图的任何其他映射。

因此,使用此方法,只要有用户线程请求任务T1,并且如果任务T1在队列中或正在执行但尚未完成,则用户线程将等待该任务。 当任务T1完成时,将通知所有正在等待任务T1的用户请求线程,然后我们将从上图中删除任务T1。

link中提供了以下代码中使用的其他类引用。

TaskProcessor.java:

ConcurrentHashMap

如果您有任何疑问,请在评论中告诉我。 谢谢。