Avoid many threads blocking on AbstractChangeSet#processSet (#2226)

This commit is contained in:
Hannes Greule 2023-06-02 11:40:34 +02:00 committed by GitHub
parent 03c4bafc45
commit 7646a067eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 64 additions and 44 deletions

View File

@ -39,19 +39,27 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/**
* This batch processor writes changes to a concrete implementation.
* {@link #processSet(IChunk, IChunkGet, IChunkSet)} is synchronized to guarantee consistency.
* To avoid many blocking threads on this method, changes are enqueued in {@link #queue}.
* This allows to keep other threads free for other work.
*/
public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor { public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
private static final Logger LOGGER = LogManagerCompat.getLogger(); private static final Logger LOGGER = LogManagerCompat.getLogger();
private final World world; private final World world;
private final AtomicInteger lastException = new AtomicInteger(); private final AtomicInteger lastException = new AtomicInteger();
protected AtomicInteger waitingCombined = new AtomicInteger(0); private final Semaphore workerSemaphore = new Semaphore(1, false);
protected AtomicInteger waitingAsync = new AtomicInteger(0); private final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
protected volatile boolean closed;
protected boolean closed;
public AbstractChangeSet(World world) { public AbstractChangeSet(World world) {
this.world = world; this.world = world;
@ -65,16 +73,11 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
if (closed) { if (closed) {
return; return;
} }
waitingAsync.incrementAndGet();
TaskManager.taskManager().async(() -> { TaskManager.taskManager().async(() -> {
waitingAsync.decrementAndGet();
synchronized (waitingAsync) {
waitingAsync.notifyAll();
}
try { try {
close(); close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.catching(e);
} }
}); });
} }
@ -82,20 +85,10 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
@Override @Override
public void flush() { public void flush() {
try { try {
if (!Fawe.isMainThread()) { // drain with this thread too
while (waitingAsync.get() > 0) { drainQueue(true);
synchronized (waitingAsync) { } catch (Exception e) {
waitingAsync.wait(1000); LOGGER.catching(e);
}
}
}
while (waitingCombined.get() > 0) {
synchronized (waitingCombined) {
waitingCombined.wait(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} }
} }
@ -125,7 +118,7 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
} }
@Override @Override
public synchronized IChunkSet processSet(IChunk chunk, IChunkGet get, IChunkSet set) { public final synchronized IChunkSet processSet(IChunk chunk, IChunkGet get, IChunkSet set) {
int bx = chunk.getX() << 4; int bx = chunk.getX() << 4;
int bz = chunk.getZ() << 4; int bz = chunk.getZ() << 4;
@ -306,12 +299,12 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
BaseBlock to = change.getCurrent(); BaseBlock to = change.getCurrent();
add(loc, from, to); add(loc, from, to);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.catching(e);
} }
} }
public boolean isEmpty() { public boolean isEmpty() {
return waitingCombined.get() == 0 && waitingAsync.get() == 0 && size() == 0; return queue.isEmpty() && workerSemaphore.availablePermits() == 1 && size() == 0;
} }
public void add(BlockVector3 loc, BaseBlock from, BaseBlock to) { public void add(BlockVector3 loc, BaseBlock from, BaseBlock to) {
@ -353,7 +346,7 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
add(x, y, z, combinedFrom, combinedTo); add(x, y, z, combinedFrom, combinedTo);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.catching(e);
} }
} }
@ -362,7 +355,6 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
} }
public Future<?> addWriteTask(final Runnable writeTask, final boolean completeNow) { public Future<?> addWriteTask(final Runnable writeTask, final boolean completeNow) {
AbstractChangeSet.this.waitingCombined.incrementAndGet();
Runnable wrappedTask = () -> { Runnable wrappedTask = () -> {
try { try {
writeTask.run(); writeTask.run();
@ -372,25 +364,55 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
} else { } else {
int hash = t.getMessage().hashCode(); int hash = t.getMessage().hashCode();
if (lastException.getAndSet(hash) != hash) { if (lastException.getAndSet(hash) != hash) {
t.printStackTrace(); LOGGER.catching(t);
}
}
} finally {
if (AbstractChangeSet.this.waitingCombined.decrementAndGet() <= 0) {
synchronized (AbstractChangeSet.this.waitingAsync) {
AbstractChangeSet.this.waitingAsync.notifyAll();
}
synchronized (AbstractChangeSet.this.waitingCombined) {
AbstractChangeSet.this.waitingCombined.notifyAll();
} }
} }
} }
}; };
if (completeNow) { if (completeNow) {
wrappedTask.run(); wrappedTask.run();
return Futures.immediateCancelledFuture(); return Futures.immediateVoidFuture();
} else { } else {
return Fawe.instance().getQueueHandler().submit(wrappedTask); CompletableFuture<?> task = new CompletableFuture<>();
queue.add(() -> {
wrappedTask.run();
task.complete(null);
});
// make sure changes are processed
triggerWorker();
return task;
}
}
private void triggerWorker() {
if (workerSemaphore.availablePermits() == 0) {
return; // fast path to avoid additional tasks: a worker is already draining the queue
}
// create a new worker to drain the current queue
Fawe.instance().getQueueHandler().submit(() -> drainQueue(false));
}
private void drainQueue(boolean ignoreRunningState) {
if (!workerSemaphore.tryAcquire()) {
if (ignoreRunningState) {
// ignoreRunningState means we want to block
// even if another thread is already draining
try {
workerSemaphore.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
return; // another thread is draining the queue already, ignore
}
}
try {
Runnable next;
while ((next = queue.poll()) != null) { // process all tasks in the queue
next.run();
}
} finally {
workerSemaphore.release();
} }
} }

View File

@ -25,8 +25,6 @@ public class AbstractDelegateChangeSet extends AbstractChangeSet {
public AbstractDelegateChangeSet(AbstractChangeSet parent) { public AbstractDelegateChangeSet(AbstractChangeSet parent) {
super(parent.getWorld()); super(parent.getWorld());
this.parent = parent; this.parent = parent;
this.waitingCombined = parent.waitingCombined;
this.waitingAsync = parent.waitingAsync;
} }
public final AbstractChangeSet getParent() { public final AbstractChangeSet getParent() {

View File

@ -258,7 +258,7 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
if (blockSize > 0) { if (blockSize > 0) {
return false; return false;
} }
if (waitingCombined.get() != 0 || waitingAsync.get() != 0) { if (!super.isEmpty()) {
return false; return false;
} }
flush(); flush();