The problem with returning reference objects is that it defeats the entire attempt to lock access to it in the first place. You can't use a basic lock() command to control access to a resource outside the scope of the object, and that means that the traditional getter/setter designs don't work.
Something that MAY work is an object that contains lockable resources, and allows lambdas or delegates to be passed in that will make use of the resource. The object will lock the resource, run the delegate, then unlock when the delegate completes. This basically puts control over running the code into the hands of the locking object, but would allow more complex operations than Interlocked has available.
Another possible method is to expose getters and setters, but implement your own access control by using a "checkout" model; when a thread is allowed to "get" a value, keep a reference to the current thread in a locked internal resource. Until that thread calls the setter, aborts, etc., all other threads attempting to access the getter are kept in a Yield loop. Once the resource is checked back in, the next thread can get it.
public class Library
{
private Book controlledBook
private Thread checkoutThread;
public Book CheckOutTheBook()
{
while(Thread.CurrentThread != checkoutThread && checkoutThread.IsAlive)
thread.CurrentThread.Yield();
lock(this)
{
checkoutThread = Thread.CurrentThread;
return controlledBook;
}
}
public void CheckInTheBook(Book theBook)
{
if(Thread.CurrentThread != checkoutThread)
throw new InvalidOperationException("This thread does not have the resource checked out.");
lock(this)
{
checkoutThread = null;
controlledBook = theBook;
}
}
}
Now, be aware that this still requires some cooperation among users of the object. Particularly, this logic is rather naive with regards to the setter; it is impossible to check in a book without having checked it out. This rule may not be apparent to consumers, and improper use could cause an unhandled exception. Also, all users must know to check the object back in if they will stop using it before they terminate, even though basic C# knowledge would dictate that if you get a reference type, changes you make are reflected everywhere. However, this can be used as a basic "one at a time" access control to a non-thread-safe resource.