/* An object for synchronization and version tracking of groups of objects. Copyright (c) 1997-2014 The Regents of the University of California. All rights reserved. Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. PT_COPYRIGHT_VERSION_2 COPYRIGHTENDKEY Made _writer, _lastReader, _lastReaderRecord, and _readerRecords transient so that object would be serializable. However, serialization is probably not right if there are outstanding read or write permissions. -- eal */ package ptolemy.kernel.util; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /////////////////////////////////////////////////////////////////// //// Workspace /** An instance of Workspace is used for synchronization and version tracking of interdependent groups of objects. These objects are said to be in the workspace. This is not the same as the container association in Ptolemy II. A workspace is never returned by a getContainer() method.
The workspace provides a rudimentary directory service that can be used to keep track of the objects within it. It is not required to use it in order to use the workspace for synchronization. Items are added to the directory by calling add(). The names of the items in the directory are not required to be unique.
The synchronization model of the workspace is a multiple-reader, single-writer model. Any number of threads can simultaneously read the workspace. Only one thread at a time can have write access to the workspace, and while the write access is held, no other thread can get read access.
When reading the state of objects in the workspace, a thread must ensure that no other thread is simultaneously modifying the objects in the workspace. To read-synchronize on a workspace, use the following code:
try { _workspace.getReadAccess(); // ... code that reads } finally { _workspace.doneReading(); }We assume that the _workspace variable references the workspace, as for example in the NamedObj class. The getReadAccess() method suspends the current thread if another thread is currently modifying the workspace, and otherwise returns immediately. Note that multiple readers can simultaneously have read access. The finally clause is executed even if an exception occurs. This is essential because without the call to doneReading(), the workspace will never again allow any thread to modify it.
To make safe changes to the objects in a workspace, a thread must write-synchronize using the following code:
try { _workspace.getWriteAccess(); // ... code that writes } finally { _workspace.doneWriting(); }Again, the call to doneWriting() is essential, or the workspace will remain permanently locked to either reading or writing.
Note that it is not necessary to obtain a write lock just to add an item to the workspace directory. The methods for accessing the directory are all synchronized, so there is no risk of any thread reading an inconsistent state.
A major subtlety in using this class concerns acquiring a write lock while holding a read lock. If a thread holds a read lock and calls getWriteAccess(), if any other thread holds a read lock, then the call to getWriteAccess() will block the calling thread until those other read accesses are released. However, while the thread is blocked, it yields its read permissions. This prevents a deadlock from occurring, but it means that the another thread may acquire write permission while the thread is stalled and modify the model. Specifically, the pattern is:
try { _workspace.getReadAccess(); ... do things ... try { _workspace.getWriteAccess(); ... at this point, the structure of a model may have changed! ... ... make my own changes knowing that the structure may have changed... } finally { _workspace.doneWriting(); } ... continue doing things, knowing the model may have changed... } finally { _workspace.doneReading(); }Unfortunately, a user may acquire a read access and invoke a method that, unknown to the user, acquires write access. For this reason, it is very important to document methods that acquire write access, and to avoid invoking them within blocks that hold read access. Note that there is no difficulty acquiring read access from within a block holding write access. @author Edward A. Lee, Mudit Goel, Lukito Muliadi, Xiaojun Liu @version $Id: Workspace.java 70048 2014-09-14 02:30:22Z cxh $ @since Ptolemy II 0.2 @Pt.ProposedRating Green (liuxj) @Pt.AcceptedRating Green (liuxj) */ public final class Workspace implements Nameable { // Note that Nameable extends ModelErrorHandler, so this class // need not declare that it directly implements ModelErrorHandler. // NOTE: it would make sense to have Workspace extend Observable // in order to notify observers of changes (marten 29-10-2012). /** Create a workspace with an empty string as its name. */ public Workspace() { super(); setName(""); } /** Create a workspace with the specified name. This name will form the * prefix of the full name of all contained objects. If the name * argument is null, then an empty string "" is used as the name. * @param name Name of the workspace. */ public Workspace(String name) { super(); setName(name); } /////////////////////////////////////////////////////////////////// //// public methods //// /** Add an item to the directory. The names of the objects * in the directory are not required to be unique. * Only items with no container can be added. Items with * a container are still viewed as being within the workspace, but * they are not explicitly listed in the directory. Instead, * their top-level container is expected to be listed (although this * is not enforced). Increment the version number. * @param item Item to list in the directory. * @exception IllegalActionException If the item has a container, is * already in the directory, or is not in this workspace. */ public synchronized void add(NamedObj item) throws IllegalActionException { if (item.workspace() != this) { throw new IllegalActionException(this, item, "Cannot add an item to the directory of a workspace " + "that it is not in."); } if (item.getContainer() != null) { throw new IllegalActionException(this, item, "Cannot add an object with a container to a workspace " + "directory."); } if (_directory.indexOf(item) >= 0) { throw new IllegalActionException(this, item, "Object is already listed in the workspace directory."); } _directory.add(item); incrVersion(); } /** Return a full description of the workspace and everything in its * directory. This is accomplished * by calling the description method with an argument for full detail. * @return A description of the workspace. * @exception IllegalActionException If thrown while getting the * description of subcomponents. */ @Override public synchronized String description() throws IllegalActionException { // NOTE: It is not strictly needed for this method to be // synchronized, since _description is. However, by making it // synchronized, the documentation shows this on the public // interface, not just the protected one. return description(NamedObj.COMPLETE); } /** Return a description of the workspace. The level of detail depends * on the argument, which is an or-ing of the static final constants * defined in the NamedObj class. This method returns an empty * string (not null) if there is nothing to report. If the contents * are requested, then the items in the directory are also described. * @param detail The level of detail. * @return A description of the workspace. * @exception IllegalActionException If thrown while getting the * description of subcomponents. */ public synchronized String description(int detail) throws IllegalActionException { // NOTE: It is not strictly needed for this method to be // synchronized, since _description is. However, by making it // synchronized, the documentation shows this on the public // interface, not just the protected one. return _description(detail, 0, 0); } /** Enumerate the items in the directory, in the order in which * they were added. * @deprecated Use directoryList() instead. * @return An enumeration of NamedObj objects. */ @Deprecated public synchronized Enumeration directory() { return Collections.enumeration(_directory); } /** Return an unmodifiable list of the items in the directory, * in the order in which they were added. * @return A list of instances of NamedObj. */ public synchronized List
* This method suspends the calling thread until such access * has been obtained. * If the calling thread is interrupted while waiting to get write * access, an InternalErrorException is thrown, and the thread does * not have write permission to the workspace. * It is essential that a call to this method is matched by a call to * doneWriting(), regardless of whether this method returns normally or * an exception is thrown. This is to ensure that the workspace is in a * consistent state, otherwise read or write access may never again * be granted in this workspace. * @exception InternalErrorException If the calling thread is interrupted * while waiting to get write access. * @see #doneWriting() */ public final synchronized void getWriteAccess() { // This method should throw an InterruptedException when the // calling thread is interrupted. InterruptedException is a // checked exception, so changing this will lead to changes // everywhere this method is called, which is a huge amount // of work. Thread current = Thread.currentThread(); if (current == _writer) { // Already have write permission. _writeDepth++; return; } AccessRecord record = null; if (current == _lastReader) { record = _lastReaderRecord; } else { record = _getAccessRecord(current, true); } // Probably need to wait for write access. // First increment this to make the record not empty, so as to // prevent the record from being deleted from the _readerRecords // table by other threads. record.failedWriteAttempts++; // Go into an infinite 'while (true)' loop and check if this thread // can get a write access. If yes, then return, if not then perform // a wait() on the workspace. while (true) { if (_writer == null) { // There are no writers. Are there any readers? if (_numReaders == 0 || _numReaders == 1 && record.readDepth > 0) { // No readers // or the only reader is the current thread _writer = current; _writeDepth = 1; record.failedWriteAttempts--; return; } } int depth = 0; try { // If there is already another waiting write request, then // we have to release any read permissions that we hold or a // deadlock could occur. There is no need to release those // read permissions if this is the only pending write // request. If another write request shows up during the // wait(), then in effect this first arrived write request // will have priority because it holds a read permission. // If the other thread also holds a read permission, it // will have to release it upon issuing the write request. // Thus, the subtlety described in the class comment // will only occur if the second thread to attempt a // write access has the problematic // pattern of acquiring write requests inside of read // permission blocks. This doesn't eliminate the problems // associated with this subtlety, it just makes it // somewhat less likely that they will occur. if (_waitingWriteRequests > 0) { // Bert Rodier suggests that we should throw an // exception in this circumstance instead of just // releasing read requests. This would have the // advantage that the problematic cases cited in // class comment may be identified (but only if // the exception is actually thrown, which depends // on an accident of thread scheduling). I would // prefer to use static analysis to identify cases // where write permissions are accessed within // read permission blocks, and analyze those // cases for correct usage. EAL 11/12/08. depth = _releaseAllReadPermissions(); } _waitingWriteRequests++; wait(); } catch (InterruptedException ex) { throw new InternalErrorException(current.getName() + " - thread interrupted while waiting to get " + "write access: " + ex.getMessage()); } finally { _waitingWriteRequests--; if (depth > 0) { _reacquireReadPermissions(depth); } } } } /** Handle a model error by throwing the specified exception. * @param context The object in which the error occurred. * @param exception An exception that represents the error. * @return Never returns. * @exception IllegalActionException The exception passed * as an argument is always thrown. * @since Ptolemy II 2.1 */ public boolean handleModelError(NamedObj context, IllegalActionException exception) throws IllegalActionException { throw exception; } /** Increment the version number by one. */ public final synchronized void incrVersion() { _version++; } /** Reacquire read permission on the workspace for * the current thread. Call this after a call to * releaseReadPermissions(). * @param depth The depth of the permissions to reacquire. * @see #releaseReadPermission() */ public void reacquireReadPermission(int depth) { _reacquireReadPermissions(depth); } /** Release read permission on the workspace * held by the current thread, and return the depth of the * nested calls to getReadAccess(). It is essential that * after calling this, you also call * reacquireReadPermission(int), passing it as an argument * the value returned by this method. Hence, you should * use this as follows: *
* int depth = releaseReadPermission(); * try { * ... do whatever here ... * } finally { * reacquireReadPermission(depth); * } ** Note that this is done automatically by the * wait(Object) method, so if you can use that instead, * please do. * @return The depth of read permissions held by the current * thread. * @see #reacquireReadPermission(int) * @see #wait(Object) * @see #wait(Object, long) */ public synchronized int releaseReadPermission() { return _releaseAllReadPermissions(); } /** Remove the specified item from the directory. * Note that that item will still refer to this workspace as * its workspace (its workspace is immutable). If the object is * not in the directory, do nothing. * Increment the version number. * @param item The NamedObj to be removed. */ public synchronized void remove(NamedObj item) { _directory.remove(item); incrVersion(); } /** Remove all items from the directory. * Note that those items will still refer to this workspace as * their workspace (their workspace is immutable). * Increment the version number. */ public synchronized void removeAll() { _directory.clear(); incrVersion(); } /** Set or change the name. If a null argument is given the * name is set to an empty string. * Increment the version number. * @param name The new name. * @see #getName() */ @Override public synchronized void setName(String name) { if (name == null) { name = ""; } _name = name; incrVersion(); } /** Return a concise description of the object. * @return The class name and name. */ @Override public String toString() { return getClass().getName() + " {" + getFullName() + "}"; } /** Release all the read accesses held by the current thread and suspend * the thread by calling Object.wait() on the specified object. When the * call returns, re-acquire all the read accesses held earlier by the * thread and return. * If the calling thread is interrupted while waiting to re-acquire read * accesses, an InternalErrorException is thrown, and the thread no longer * has read access to the workspace. * This method helps prevent deadlocks caused when a thread that * waits for another thread to do something prevents it from doing * that something by holding read access on the workspace. * IMPORTANT: The calling thread should not hold a lock * on obj when calling this method, unlike a direct call to * obj.wait(). Holding such a lock can lead to deadlock because * this method can block for an indeterminate amount of time while trying * to reacquire read permissions that it releases. Moreover, holding such * a lock is pointless since this method internally calls * obj.wait() (within its own synchronized(obj) block, * so the calling method cannot assume that the lock on obj was * held during the entire execution of this method. * If the calling thread needs to hold a lock on obj until * obj.wait() is called, then you should manually release * read permissions and release the obj before reacquiring them, * as follows: * *
* int depth = 0; * try { * synchronized(obj) { * ... * depth = releaseReadPermission(); * obj.wait(); * } * } finally { * if (depth > 0) { * reacquireReadPermission(depth); * } * } ** * @param obj The object that the thread wants to wait on. * @exception InterruptedException If the calling thread is interrupted * while waiting on the specified object and all the read accesses held * earlier by the thread are re-acquired. * @exception InternalErrorException If re-acquiring the read accesses * held earlier by the thread fails. */ public void wait(Object obj) throws InterruptedException { int depth = 0; depth = _releaseAllReadPermissions(); try { synchronized (obj) { obj.wait(); } } finally { // NOTE: If obj != this, and this method is called // inside a synchronized(obj) block, then the following // call can lead to deadlock because it does a wait() on // this workspace. It has to, since notification that will // free the reacquire occurs on the workspace. But it holds // a lock on obj during that wait. Meanwhile, another thread // with write permission may attempt to acquire a lock on obj. // Deadlock. Hence the warning in the method comment. _reacquireReadPermissions(depth); } } /** This method is equivalent to the single argument version except that * you can specify a timeout, which is in milliseconds. If value of the * timeout argument is zero, then the method is exactly equivalent * to the single argument version, and no timeout is implemented. If * the value is larger than zero, then the method returns if either * the thread is notified by another thread or the timeout expires. * IMPORTANT: The calling thread should not hold a lock * on obj when calling this method, unlike a direct call to * obj.wait(). Holding such a lock can lead to deadlock because * this method can block for an indeterminate amount of time while trying * to reacquire read permissions that it releases. Moreover, holding such * a lock is pointless since this method internally calls * obj.wait() (within its own synchronized(obj) block, * so the calling method cannot assume that the lock on obj was * held during the entire execution of this method. * @param obj The object that the thread wants to wait on. * @param timeout The maximum amount of time to wait, in milliseconds, * or zero to not specify a timeout. * @exception InterruptedException If the calling thread is interrupted * while waiting on the specified object and all the read accesses held * earlier by the thread are re-acquired. * @exception InternalErrorException If re-acquiring the read accesses * held earlier by the thread fails. * @see #wait(Object) */ public void wait(Object obj, long timeout) throws InterruptedException { int depth = 0; depth = _releaseAllReadPermissions(); try { synchronized (obj) { obj.wait(timeout); } } finally { _reacquireReadPermissions(depth); } } /////////////////////////////////////////////////////////////////// //// protected methods //// /** Return a description of the workspace. The level of detail depends * on the argument, which is an or-ing of the static final constants * defined in the NamedObj class. If the contents are requested, * then the items in the directory are also described. * Zero, one or two brackets can be specified to surround the returned * description. If one is specified it is the the leading bracket. * This is used by derived classes that will append to the description. * Those derived classes are responsible for the closing bracket. * An argument other than 0, 1, or 2 is taken to be equivalent to 0. * @param detail The level of detail. * @param indent The amount of indenting. * @param bracket The number of surrounding brackets (0, 1, or 2). * @return A description of the workspace. * @exception IllegalActionException If thrown while getting the * description of subcomponents. */ protected synchronized String _description(int detail, int indent, int bracket) throws IllegalActionException { StringBuffer result = new StringBuffer( NamedObj._getIndentPrefix(indent)); if (bracket == 1 || bracket == 2) { result.append("{"); } if ((detail & NamedObj.CLASSNAME) != 0) { result.append(getClass().getName()); if ((detail & NamedObj.FULLNAME) != 0) { result.append(" "); } } if ((detail & NamedObj.FULLNAME) != 0) { result.append("{" + getFullName() + "}"); } if ((detail & NamedObj.CONTENTS) != 0) { if ((detail & (NamedObj.CLASSNAME | NamedObj.FULLNAME)) != 0) { result.append(" "); } result.append("directory {\n"); List