blob: 94611b5ef33e4bbc71c0a61674293cd0fc573a81 [file] [log] [blame]
// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.concurrent;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import java.util.Set;
import java.util.concurrent.Semaphore;
/**
* A concurrency primitive for managing access to at most K unique things at once, for a fixed K.
*
* <p>You can think of this as a pair of a {@link Semaphore} with K total permits and a
* {@link Multiset}, with permits being doled out and returned based on the current contents of the
* {@link Multiset}.
*/
@ThreadSafe
public abstract class MultisetSemaphore<T> {
/**
* Blocks until permits are available for all the values in {@code valuesToAcquire}, and then
* atomically acquires these permits.
*
* <p>{@code acquireAll(valuesToAcquire)} atomically does the following
* <ol>
* <li>Computes {@code m}, the number of values in {@code valuesToAcquire} that are not
* currently in the backing {@link Multiset}.
* <li>Adds {@code valuesToAcquire} to the backing {@link Multiset}.
* <li>Blocks until {@code m} permits are available from the backing {@link Semaphore}.
* <li>Acquires these permits.
* </ol>
*/
public abstract void acquireAll(Set<T> valuesToAcquire) throws InterruptedException;
/**
* Atomically releases permits for all the values in {@code valuesToAcquire}.
*
* <p>{@code releaseAll(valuesToRelease)} atomically does the following
* <ol>
* <li>Computes {@code m}, the number of values in {@code valuesToRelease} that are currently in
* the backing {@link Multiset} with multiplicity 1.
* <li>Removes {@code valuesToRelease} from the backing {@link Multiset}.
* <li>Release {@code m} permits from the backing {@link Semaphore}.
* </ol>
*
* <p>Assumes that this {@link MultisetSemaphore} has already given out permits for all the
* values in {@code valuesToAcquire}.
*/
public abstract void releaseAll(Set<T> valuesToRelease);
public abstract int estimateCurrentNumUniqueValues();
/**
* Returns a {@link MultisetSemaphore} with a backing {@link Semaphore} that has an unbounded
* number of permits; that is, {@link #acquireAll} will never block.
*/
public static <T> MultisetSemaphore<T> unbounded() {
return UnboundedMultisetSemaphore.instance();
}
/** Builder for {@link MultisetSemaphore} instances. */
public static class Builder {
private static final int UNSET_INT = -1;
private int maxNumUniqueValues = UNSET_INT;
private Builder() {
}
/**
* Sets the maximum number of unique values for which permits can be held at once in the
* to-be-constructed {@link MultisetSemaphore}.
*/
public Builder maxNumUniqueValues(int maxNumUniqueValues) {
Preconditions.checkState(
maxNumUniqueValues > 0,
"maxNumUniqueValues must be positive (was %d)",
maxNumUniqueValues);
this.maxNumUniqueValues = maxNumUniqueValues;
return this;
}
public <T> MultisetSemaphore<T> build() {
Preconditions.checkState(
maxNumUniqueValues != UNSET_INT,
"maxNumUniqueValues(int) must be specified");
return new NaiveMultisetSemaphore<>(maxNumUniqueValues);
}
}
/** Returns a fresh {@link Builder}. */
public static Builder newBuilder() {
return new Builder();
}
private static class UnboundedMultisetSemaphore<T> extends MultisetSemaphore<T> {
private static final UnboundedMultisetSemaphore<Object> INSTANCE =
new UnboundedMultisetSemaphore<Object>();
private UnboundedMultisetSemaphore() {
}
@SuppressWarnings("unchecked")
private static <T> UnboundedMultisetSemaphore<T> instance() {
return (UnboundedMultisetSemaphore<T>) INSTANCE;
}
@Override
public void acquireAll(Set<T> valuesToAcquire) throws InterruptedException {
}
@Override
public void releaseAll(Set<T> valuesToRelease) {
}
@Override
public int estimateCurrentNumUniqueValues() {
// We can't give a good estimate since we don't track values at all.
return 0;
}
}
private static class NaiveMultisetSemaphore<T> extends MultisetSemaphore<T> {
private final int maxNumUniqueValues;
private final Semaphore semaphore;
private final Object lock = new Object();
// Protected by 'lock'.
private final HashMultiset<T> actualValues = HashMultiset.create();
private NaiveMultisetSemaphore(int maxNumUniqueValues) {
this.maxNumUniqueValues = maxNumUniqueValues;
this.semaphore = new Semaphore(maxNumUniqueValues);
}
@Override
public void acquireAll(Set<T> valuesToAcquire) throws InterruptedException {
int oldNumNeededPermits;
synchronized (lock) {
oldNumNeededPermits = computeNumNeededPermitsLocked(valuesToAcquire);
}
while (true) {
semaphore.acquire(oldNumNeededPermits);
synchronized (lock) {
int newNumNeededPermits = computeNumNeededPermitsLocked(valuesToAcquire);
if (newNumNeededPermits != oldNumNeededPermits) {
// While we were doing 'acquire' above, another thread won the race to acquire the first
// usage of one of the values in 'valuesToAcquire' or release the last usage of one of
// the values. This means we either acquired too many or too few permits, respectively,
// above. Release the permits we did acquire, in order to restore the accuracy of the
// semaphore's current count, and then try again.
semaphore.release(oldNumNeededPermits);
oldNumNeededPermits = newNumNeededPermits;
continue;
} else {
// Our modification to the semaphore was correct, so it's sound to update the multiset.
valuesToAcquire.forEach(actualValues::add);
return;
}
}
}
}
private int computeNumNeededPermitsLocked(Set<T> valuesToAcquire) {
// We need a permit for each value that is not already in the multiset.
return (int) valuesToAcquire.stream()
.filter(v -> actualValues.count(v) == 0)
.count();
}
@Override
public void releaseAll(Set<T> valuesToRelease) {
synchronized (lock) {
// We need to release a permit for each value that currently has multiplicity 1.
int numPermitsToRelease =
valuesToRelease
.stream()
.mapToInt(v -> actualValues.remove(v, 1) == 1 ? 1 : 0)
.sum();
semaphore.release(numPermitsToRelease);
}
}
@Override
public int estimateCurrentNumUniqueValues() {
return maxNumUniqueValues - semaphore.availablePermits();
}
}
}