/*
* JVSTM: a Java library for Software Transactional Memory
* Copyright (C) 2005 INESC-ID Software Engineering Group
* http://www.esw.inesc-id.pt
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* Author's contact:
* INESC-ID Software Engineering Group
* Rua Alves Redol 9
* 1000 - 029 Lisboa
* Portugal
*/
package jvstm;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import jvstm.util.Cons;
import jvstm.util.Pair;
/* This class contains information about the VBoxes modified by a transaction and the corresponding
* new values. It is used for two purposes: 1) to manage the concurrent write-back of VBox values;
* and 2) to enable other (future) transactions to validate their own commits.
*
* Regarding the write-back mechanism the idea is as follows: the transaction's write-set is divided
* into buckets, of approximately the same size each. Any transaction can help to write-back, by
* picking a unique bucket and copying the values of that bucket's VBoxes to their "public" place
* (the body of the VBox).
*/
public class WriteSet {
private static final ThreadLocal<Random> random = new ThreadLocal<Random>() {
@Override
protected Random initialValue() {
return new Random();
}
};
/*
* Represents the default number os VBoxes to include in the same block.
* This value is used to provide enough work to a helper thread and to avoid
* for example to give 1 box per thread to write, which would be a waste of
* context switching effort. The best value certainly depends on the
* architecture where this code will run.
*/
protected static final int DEFAULT_BLOCK_SIZE = 10;
protected BoxesToCommit normalWriteSet;
protected BoxesToCommit perTxBoxesWriteSet = BoxesToCommit.EMPTY_BOXES;
/* Support for VArray */
protected final VArrayCommitState[] arrayCommitState;
protected WriteSet(ReadWriteTransaction committer) {
this(committer.boxesWrittenInPlace, committer.mergedTxs, committer.boxesWritten, committer.arrayWrites, committer.arrayWritesCount, committer, DEFAULT_BLOCK_SIZE);
}
protected WriteSet(Cons<VBox> boxesWrittenInPlace, Cons<ParallelNestedTransaction> mergedTxs, Map<VBox, Object> boxesWritten, Map<VArrayEntry<?>, VArrayEntry<?>> arrayWrites, Map<VArray<?>, Integer> arrayWritesCount, ReadWriteTransaction committer, int blockSize) {
int boxesWrittenInPlaceSize = boxesWrittenInPlace.size();
for (ParallelNestedTransaction mergedTx : mergedTxs) {
boxesWrittenInPlaceSize += mergedTx.boxesWrittenInPlace.size();
}
int maxRequiredSize = boxesWrittenInPlaceSize + boxesWritten.size();
VBox[] vboxes = new VBox[maxRequiredSize];
Object[] values = new Object[maxRequiredSize];
int pos = 0;
// Deal with VBoxes written in place
for (VBox vbox : boxesWrittenInPlace) {
vboxes[pos] = vbox;
values[pos++] = vbox.inplace.tempValue;
vbox.inplace.next = null;
}
for (ParallelNestedTransaction mergedTx : mergedTxs) {
for (VBox vbox : mergedTx.boxesWrittenInPlace) {
vboxes[pos] = vbox;
values[pos++] = vbox.inplace.tempValue;
vbox.inplace.next = null;
}
}
// Deal with VBoxes written in the fallback write-set
for (Map.Entry<VBox, Object> entry : boxesWritten.entrySet()) {
VBox vbox = entry.getKey();
if (vbox.inplace.orec.owner == committer) {
// if we also wrote directly to the box, we just skip this value
continue;
}
vboxes[pos] = vbox;
values[pos++] = entry.getValue();
}
int writeSetLength = pos;
int nBlocksAux = writeSetLength / blockSize;
int nBlocks = (nBlocksAux == 0 && writeSetLength > 0) ? 1 : nBlocksAux;
Cons<GarbageCollectable>[] bodiesPerBlock = new Cons[nBlocks + arrayWritesCount.size()];
AtomicBoolean[] blocksDone = new AtomicBoolean[nBlocks];
for (int i = 0; i < nBlocks; i++) {
blocksDone[i] = new AtomicBoolean(false);
}
this.normalWriteSet = new BoxesToCommit(nBlocks, blockSize, vboxes, values, writeSetLength, bodiesPerBlock, blocksDone);
this.arrayCommitState = prepareArrayWrites(arrayWrites, arrayWritesCount);
}
protected void addPerTxBoxesWrites(Map<VBox, Object> perTxBoxesWrites) {
if (perTxBoxesWrites == ReadWriteTransaction.EMPTY_MAP) {
return;
}
int maxRequiredSize = perTxBoxesWrites.size();
VBox[] vboxes = new VBox[maxRequiredSize];
Object[] values = new Object[maxRequiredSize];
int pos = 0;
for (Map.Entry<VBox, Object> entry : perTxBoxesWrites.entrySet()) {
vboxes[pos] = entry.getKey();
values[pos++] = entry.getValue();
}
int writeSetLength = pos;
int nBlocksAux = writeSetLength / DEFAULT_BLOCK_SIZE;
int nBlocks = (nBlocksAux == 0 && writeSetLength > 0) ? 1 : nBlocksAux;
Cons<GarbageCollectable>[] bodiesPerBlock = new Cons[nBlocks];
AtomicBoolean[] blocksDone = new AtomicBoolean[nBlocks];
for (int i = 0; i < nBlocks; i++) {
blocksDone[i] = new AtomicBoolean(false);
}
this.perTxBoxesWriteSet = new BoxesToCommit(nBlocks, DEFAULT_BLOCK_SIZE, vboxes, values, writeSetLength, bodiesPerBlock, blocksDone);
}
// This constructor is used by InevitableTransactions. It is simpler
// because we know that everything was already written in place. There is
// only one bucket and it will already be written-back. The purpose is
// that when any transaction tries to helpWriteBack will simply quickly
// return and continue its work.
protected WriteSet(Cons<VBox> vboxesWrittenBack) {
int writeSetLength = vboxesWrittenBack.size();
int nBlocks = 1;
int blockSize = writeSetLength;
VBox[] vboxes = new VBox[writeSetLength];
Object[] values = new Object[writeSetLength];
Cons<GarbageCollectable>[] bodiesPerBlock = new Cons[nBlocks];
AtomicBoolean[] blocksDone = new AtomicBoolean[nBlocks];
int pos = 0;
Cons<GarbageCollectable> bodiesCommitted = Cons.empty();
for (VBox vbox : vboxesWrittenBack) {
vboxes[pos] = vbox;
values[pos++] = vbox.body.value;
bodiesCommitted = bodiesCommitted.cons(vbox.body);
}
bodiesPerBlock[0] = bodiesCommitted;
blocksDone[0] = new AtomicBoolean(true);
this.normalWriteSet = new BoxesToCommit(nBlocks, blockSize, vboxes, values, writeSetLength, bodiesPerBlock, blocksDone);
this.arrayCommitState = new VArrayCommitState[0];
}
protected WriteSet(VBox[] allWrittenVBoxes, int blockSize) {
int writeSetLength = allWrittenVBoxes.length;
int nBlocksAux = writeSetLength / blockSize;
int nBlocks = (nBlocksAux == 0 && writeSetLength > 0) ? 1 : nBlocksAux;
@SuppressWarnings("unchecked")
Cons<GarbageCollectable>[] bodiesPerBlock = new Cons[nBlocks];
AtomicBoolean[] blocksDone = new AtomicBoolean[nBlocks];
for (int i = 0; i < nBlocks; i++) {
blocksDone[i] = new AtomicBoolean(false);
}
this.normalWriteSet = new BoxesToCommit(nBlocks, blockSize, allWrittenVBoxes, null, writeSetLength, bodiesPerBlock, blocksDone);
this.arrayCommitState = new VArrayCommitState[0];
}
protected final void helpWriteBack(int newTxNumber) {
// It is important that this order or processing is preserved: perTxBoxes' commits' writes to VBoxes
// take precedence over the normal write set of the committing transaction
processBoxes(this.perTxBoxesWriteSet, newTxNumber);
processBoxes(this.normalWriteSet, newTxNumber);
// Writeback to arrays
// This uses locking, but locks are only used if the current transaction
// WROTE to an array
//
// Algorithm:
// - All threads tryLock each array: if they succeed, they writeback to
// the array, otherwise they
// move on to the other arrays.
// - Because no thread should leave this method until all arrays are
// written back, a second pass is
// done, locking each array again, and checking that writeback is
// complete -- it might not be, if
// tryLock failed because a thread helping an older commit was still
// holding the lock. This way,
// only after all arrays are written back can the thread continue.
if (this.arrayCommitState.length > 0) {
int nBlocks = this.normalWriteSet.nBlocks;
Cons<GarbageCollectable>[] bodiesPerBlock = this.normalWriteSet.bodiesPerBlock;
for (int i = 0; i < this.arrayCommitState.length; i++) {
VArrayCommitState cs = this.arrayCommitState[i];
if (cs.array.writebackLock.tryLock()) {
try {
if (cs.array.version >= newTxNumber) {
continue;
}
bodiesPerBlock[nBlocks + i] = cs.doWriteback(newTxNumber);
} finally {
cs.array.writebackLock.unlock();
}
}
}
// This loop is SIMILAR but not THE SAME as the one above: it uses
// lock instead of tryLock
for (int i = 0; i < this.arrayCommitState.length; i++) {
VArrayCommitState cs = this.arrayCommitState[i];
cs.array.writebackLock.lock();
try {
if (cs.array.version >= newTxNumber) {
continue;
}
bodiesPerBlock[nBlocks + i] = cs.doWriteback(newTxNumber);
} finally {
cs.array.writebackLock.unlock();
}
}
}
}
private void processBoxes(BoxesToCommit boxesToCommit, int newTxNumber) {
int nBlocks = boxesToCommit.nBlocks;
if (nBlocks > 0) {
AtomicBoolean[] blocksDone = boxesToCommit.blocksDone;
Cons<GarbageCollectable>[] bodiesPerBlock = boxesToCommit.bodiesPerBlock;
int finalBlock = random.get().nextInt(nBlocks); // start at a
// random
// position
int currentBlock = finalBlock;
do {
if (!blocksDone[currentBlock].get()) {
// smf: is this safe? multiple helping threads will write to
// this location, but since java
// does not allow values out of the blue, we will always
// have a reference to a Cons with the
// required bodies created, right?
bodiesPerBlock[currentBlock] = writeBackBlock(boxesToCommit, currentBlock, newTxNumber);
blocksDone[currentBlock].set(true);
}
currentBlock = (currentBlock + 1) % nBlocks;
} while (currentBlock != finalBlock);
}
}
private final Cons<GarbageCollectable> writeBackBlock(BoxesToCommit boxesToCommit, int block, int newTxNumber) {
int min = block * boxesToCommit.blockSize;
// max depends on whether this is the last block
int max = (block == (boxesToCommit.nBlocks - 1)) ? boxesToCommit.writeSetLength : (min + boxesToCommit.blockSize);
VBox[] vboxes = boxesToCommit.allWrittenVBoxes;
Object[] values = boxesToCommit.allWrittenValues;
Cons<GarbageCollectable> newBodies = writeBackLoop(newTxNumber, min, max, vboxes, values);
return newBodies;
}
/*
* We inverted the write-back loop to keep a direct correspondence between the vboxes array
* and the cons of vbodies.
* !!!! ATENTION => this is a requirement for the versioned history reversion process of
* the AOM (adaptive object metadata).
*/
protected Cons<GarbageCollectable> writeBackLoop(int newTxNumber, int min, int max, VBox[] vboxes, Object[] values) {
Cons<GarbageCollectable> newBodies = Cons.empty();
for (int i = max - 1; i >= min; i--) {
VBox vbox = vboxes[i];
Object newValue = values[i];
VBoxBody newBody = vbox.commit((newValue == ReadWriteTransaction.NULL_VALUE) ? null : newValue, newTxNumber);
newBodies = newBodies.cons(newBody);
}
return newBodies;
}
protected final int size() {
return this.normalWriteSet.writeSetLength + this.perTxBoxesWriteSet.writeSetLength;
}
protected static WriteSet empty() {
return new WriteSet(Cons.<VBox>empty(), Cons.<ParallelNestedTransaction>empty(), ReadWriteTransaction.EMPTY_MAP, ReadWriteTransaction.EMPTY_MAP, ReadWriteTransaction.EMPTY_MAP, null, DEFAULT_BLOCK_SIZE);
}
static final class VArrayCommitState {
final VArray<?> array;
final VArrayEntry<?>[] writesToCommit;
final int[] logEntryIndexes;
VArrayCommitState(VArray<?> array, VArrayEntry<?>[] writesToCommit, int[] logEntryIndexes) {
this.array = array;
this.writesToCommit = writesToCommit;
this.logEntryIndexes = logEntryIndexes;
}
private Cons<GarbageCollectable> doWriteback(int newTxNumber) {
GarbageCollectable newLogNode = array.commit(newTxNumber, writesToCommit, logEntryIndexes);
return Cons.<GarbageCollectable> empty().cons(newLogNode);
}
}
private VArrayCommitState[] prepareArrayWrites(Map<VArrayEntry<?>, VArrayEntry<?>> arrayWrites,
Map<VArray<?>, Integer> arrayWritesCount) {
if (arrayWrites.isEmpty()) {
return new VArrayCommitState[0];
}
// During commit, arrayWritebacks keeps the write-set divided into
// per-array lists
Map<VArray<?>, Pair<VArrayEntry<?>[], Integer>> arrayWritebacks = new HashMap<VArray<?>, Pair<VArrayEntry<?>[], Integer>>(
arrayWritesCount.size());
for (Map.Entry<VArray<?>, Integer> entry : arrayWritesCount.entrySet()) {
arrayWritebacks.put(entry.getKey(), new Pair<VArrayEntry<?>[], Integer>(new VArrayEntry[entry.getValue()], 0));
}
VArray<?> lastArray = null;
Pair<VArrayEntry<?>[], Integer> lastArrayEntries = null;
VArrayCommitState[] commitState = new VArrayCommitState[arrayWritesCount.size()];
int nextCommitStatePos = 0;
// Split array write-set into per-array lists
for (VArrayEntry<?> entry : arrayWrites.values()) {
VArray<?> array = entry.array;
if (array != lastArray) {
lastArray = array;
lastArrayEntries = arrayWritebacks.get(array);
}
// Don't ask. Just try to do lastArrayEntries.second++ and you'll
// see what I mean
int pos = ++lastArrayEntries.second - 1;
lastArrayEntries.first[pos] = entry;
if (lastArrayEntries.first.length == pos + 1) { // We have all the
// writes for the
// current array
VArrayEntry<?>[] writesToCommit = lastArrayEntries.first;
// Sort entries
java.util.Arrays.sort(writesToCommit);
// Create logEntryIndexes to be used in the log
int[] logEntryIndexes = new int[writesToCommit.length];
for (int i = 0; i < writesToCommit.length; i++) {
logEntryIndexes[i] = writesToCommit[i].index;
}
commitState[nextCommitStatePos++] = new VArrayCommitState(array, writesToCommit, logEntryIndexes);
}
}
return commitState;
}
}