/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.application.impl; import com.intellij.diagnostic.ThreadDumper; import com.intellij.openapi.application.AccessToken; import com.intellij.openapi.application.ex.ApplicationUtil; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressManager; import com.intellij.util.containers.ConcurrentList; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.LockSupport; /** * Read-Write lock optimised for mostly reads. * Scales better than {@link java.util.concurrent.locks.ReentrantReadWriteLock} with a number of readers due to reduced contention thanks to thread local structures. * The lock has writer preference, i.e. no reader can obtain read lock while there is a writer pending. * NOT reentrant. * Writer assumed to issue write requests from the dedicated thread {@link #writeThread} only. * Readers must not issue read requests from the write thread {@link #writeThread}. * <br> * Based on paper <a href="http://mcg.cs.tau.ac.il/papers/ppopp2013-rwlocks.pdf">"NUMA-Aware Reader-Writer Locks" by Calciu, Dice, Lev, Luchangco, Marathe, Shavit.</a><br> * The elevator pitch explanation of the algorithm:<br> * Read lock: flips {@link Reader#readRequested} bit in its own thread local {@link Reader} structure and waits for writer to release its lock by checking {@link #writeRequested}.<br> * Write lock: sets global {@link #writeRequested} bit and waits for all readers (in global {@link #readers} list) to release their locks by checking {@link Reader#readRequested} for all readers. */ class ReadMostlyRWLock { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.application.impl.ReadMostlyRWLock"); private final Thread writeThread; volatile boolean writeRequested; // this writer is requesting or obtained the write access private volatile boolean writeAcquired; // this writer obtained the write lock // All reader threads are registered here. Dead readers are garbage collected in writeUnlock(). private final ConcurrentList<Reader> readers = ContainerUtil.createConcurrentList(); private final Map<Thread, SuspensionId> privilegedReaders = new ConcurrentHashMap<>(); private volatile SuspensionId currentSuspension; ReadMostlyRWLock(@NotNull Thread writeThread) { this.writeThread = writeThread; } // Each reader thread has instance of this struct in its thread local. it's also added to global "readers" list. private static class Reader { @NotNull private final Thread thread; // its thread private volatile boolean readRequested; // this reader is requesting or obtained read access. Written by reader thread only, read by writer. private volatile boolean blocked; // this reader is blocked waiting for the writer thread to release write lock. Written by reader thread only, read by writer. private boolean impatientReads; // true if should throw PCE on contented read lock Reader(@NotNull Thread readerThread) { thread = readerThread; } } private final ThreadLocal<Reader> R = ThreadLocal.withInitial(() -> { Reader status = new Reader(Thread.currentThread()); boolean added = readers.addIfAbsent(status); assert added : readers + "; "+Thread.currentThread(); return status; }); boolean isWriteThread() { return Thread.currentThread() == writeThread; } boolean isReadLockedByThisThread() { checkReadThreadAccess(); Reader status = R.get(); throwIfImpatient(status); return status.readRequested; } void readLock() { checkReadThreadAccess(); Reader status = R.get(); for (int iter = 0; ; iter++) { if (tryReadLock(status, true)) { return; } ProgressManager.checkCanceled(); waitABit(status, iter); } } private void waitABit(Reader status, int iteration) { if (iteration > SPIN_TO_WAIT_FOR_LOCK) { status.blocked = true; try { throwIfImpatient(status); LockSupport.parkNanos(this, 1000000); // unparked by writeUnlock } finally { status.blocked = false; } } else { Thread.yield(); } } private void throwIfImpatient(Reader status) { // when client explicitly runs in non-cancelable block do not throw from within nested read actions if (status.impatientReads && writeRequested && !ProgressManager.getInstance().isInNonCancelableSection()) { throw new ApplicationUtil.CannotRunReadActionException(); } } /** * Executes a {@code runnable} in an "impatient" mode. * In this mode any attempt to grab read lock * will fail (i.e. throw {@link ApplicationUtil.CannotRunReadActionException}) * if there is a pending write lock request. */ void executeByImpatientReader(@NotNull Runnable runnable) throws ApplicationUtil.CannotRunReadActionException { checkReadThreadAccess(); Reader status = R.get(); boolean old = status.impatientReads; try { status.impatientReads = true; runnable.run(); } finally { status.impatientReads = old; } } void readUnlock() { checkReadThreadAccess(); Reader status = R.get(); status.readRequested = false; if (writeRequested) { LockSupport.unpark(writeThread); // parked by writeLock() } } boolean tryReadLock() { checkReadThreadAccess(); Reader status = R.get(); return tryReadLock(status, true); } private boolean tryReadLock(Reader status, boolean checkPrivileges) { if (!writeRequested) { if (checkPrivileges && currentSuspension != null && !privilegedReaders.containsKey(Thread.currentThread())) { return false; } status.readRequested = true; if (!writeRequested) { return true; } status.readRequested = false; } return false; } private static final int SPIN_TO_WAIT_FOR_LOCK = 100; void writeLock() { checkWriteThreadAccess(); assert !writeRequested; assert !writeAcquired; writeRequested = true; for (int iter=0; ;iter++) { if (areAllReadersIdle()) { writeAcquired = true; break; } if (iter > SPIN_TO_WAIT_FOR_LOCK) { LockSupport.parkNanos(this, 1000000); // unparked by readUnlock } else { Thread.yield(); } } } AccessToken writeSuspend() { SuspensionId prevSuspension = currentSuspension; if (prevSuspension == null) { currentSuspension = new SuspensionId(); } writeUnlock(); return new AccessToken() { @Override public void finish() { writeLock(); currentSuspension = prevSuspension; if (prevSuspension == null) { ensureNoPrivilegedReaders(); } } }; } private void ensureNoPrivilegedReaders() { if (!privilegedReaders.isEmpty()) { List<String> offenderNames = ContainerUtil.map(privilegedReaders.keySet(), Thread::getName); privilegedReaders.clear(); LOG.error("Pooled threads created during write action suspension should have been terminated: " + offenderNames, new Attachment("threadDump.txt", ThreadDumper.dumpThreadsToString())); } } @Nullable SuspensionId currentReadPrivilege() { return privilegedReaders.get(Thread.currentThread()); } @NotNull AccessToken applyReadPrivilege(@Nullable SuspensionId context) { Reader status = R.get(); int iter = 0; while (context != null && context == currentSuspension) { if (tryReadLock(status, false)) { try { return context == currentSuspension ? grantReadPrivilege() : AccessToken.EMPTY_ACCESS_TOKEN; } finally { readUnlock(); } } waitABit(status, iter++); } return AccessToken.EMPTY_ACCESS_TOKEN; } @NotNull AccessToken grantReadPrivilege() { Thread thread = Thread.currentThread(); privilegedReaders.put(thread, currentSuspension); return new AccessToken() { @Override public void finish() { privilegedReaders.remove(thread); } }; } void writeUnlock() { checkWriteThreadAccess(); writeAcquired = false; writeRequested = false; List<Reader> dead = new ArrayList<>(readers.size()); for (Reader reader : readers) { if (reader.blocked) { LockSupport.unpark(reader.thread); // parked by readLock() } else if (!reader.thread.isAlive()) { dead.add(reader); } } readers.removeAll(dead); } private void checkWriteThreadAccess() { if (Thread.currentThread() != writeThread) { throw new IllegalStateException("Current thread: "+Thread.currentThread()+"; expected: "+ writeThread); } } private void checkReadThreadAccess() { if (Thread.currentThread() == writeThread) { throw new IllegalStateException("Must not start read from the write thread: "+Thread.currentThread()); } } boolean tryWriteLock() { checkWriteThreadAccess(); assert !writeRequested; assert !writeAcquired; writeRequested = true; if (areAllReadersIdle()) { writeAcquired = true; return true; } writeRequested = false; return false; } private boolean areAllReadersIdle() { for (Reader reader : readers) { if (reader.readRequested) { return false; } } return true; } boolean isWriteLocked() { return writeAcquired; } static class SuspensionId {} }