CAPÍTULO VI: Envejecimiento, idea de salud y cuerpo medicado
2. EL IDEAL DE LO SALUDABLE Y EL CUERPO MEDICADO EN LA VEJEZ
if( data_->refs == Unshareable || --data_->refs < 1 )
{
bDelete = true;
}
data_->m.Unlock(); //---
if( bDelete )
{
delete data_;
}
}
For the
String
copy constructor, note that we can assume the otherString
's data buffer won't be modified or moved during this operation, because it's the responsibility of the caller to serialize accessto visible objects. We must still, however, serialize access to the reference count itself, as we did above:
String::String( const String& other )
{
bool bSharedIt = false;
other.data_->m.Lock(); //---
if( other.data_->refs != Unshareable )
{
bSharedIt = true;
data_ = other.data_;
++data_->refs;
}
other.data_->m.Unlock(); //---
if( !bSharedIt )
{
data_ = new StringBuf( *other.data_ );
}
}
So making the
String
copy constructor safe wasn't very hard at all. This brings us toAboutToModify()
, which turns out to be very similar. But notice that this sample code actually acquires the lock during the entire deep copy operation—really, the lock is strictly needed only when looking at therefs
value and again when updating therefs
value at the end. But let's go ahead and lock the whole operation instead of getting slightly better concurrency by releasing the lock during the deep copy and then reacquiring it just to updaterefs
:void String::AboutToModify(
size_t n,
bool markUnshareable /* = false */
)
{
data_->m.Lock(); //---
if( data_->refs > 1 && data_->refs != Unshareable )
{
StringBuf* newdata = new StringBuf( *data_, n );
--data_->refs; // now all the real work is
data_->m.Unlock(); //---
data_ = newdata; // done, so take ownership
}
else
{
data_->m.Unlock(); //---
data_->Reserve( n );
}
data_->refs = markUnshareable ? Unshareable : 1;
}
None of the other functions need to be changed.
Append()
andoperator[]()
don't need locks because onceAboutToModify()
completes, we're guaranteed that we're not using a shared representation.Length()
doesn't need a lock because by definition, we're okay if ourStringBuf
is not shared (there's no one else to change the used count on us), and we're okay if it is shared (the other thread would take its own copy before doing any work and hence still wouldn't modify our used count on us):void String::Append( char c )
{
AboutToModify( data_->used+1 );
data_->buf[used++">data_->used++] = c;
}
size_t String::Length() const
{
return data_->used;
}
char& String::operator[]( size_t n )
{
AboutToModify( data_->len, true );
return data_->buf[n];
}
const char String::operator[]( size_t n ) const
{
return data_->buf[n];
}
}
Again, note the interesting thing in all of this: The only locking we needed to do involved the
refs
count itself.With that observation and the above general-purpose solution under our belts, let's look back to the (a) part of the question:
a) assuming that there are atomic operations to get, set, and compare integer values; and
Some operating systems provide these kinds of functions.
Note: These functions are usually significantly more efficient than general-purpose synchronization primitives such as mutexes. It is, however, a fallacy to say that we can use atomic integer operations "instead of locks" because locking is still required—the locking is just generally less expensive than other alternatives, but it's not free by a long shot, as we will see.
Here is a thread-safe implementation of
String
that assumes we have three functions: anthat safely return the new value. We'll do essentially the same thing we did above, but use only atomic integer operations to serialize access to the
refs
count:namespace Optimized
{
String::String() : data_(new StringBuf) { }
String::~String()
{
if( IntAtomicGet( data_->refs ) == Unshareable ||
IntAtomicDecrement( data_->refs ) < 1 )
{
delete data_;
}
}
String::String( const String& other )
{
if( IntAtomicGet( other.data_->refs ) != Unshareable )
{
data_ = other.data_;
IntAtomicIncrement( data_->refs );
}
else
{
data_ = new StringBuf( *other.data_ );
}
}
void String::AboutToModify(
size_t n,
bool markUnshareable /* = false */
)
{
int refs = IntAtomicGet( data_->refs );
if( refs > 1 && refs != Unshareable )
{
StringBuf* newdata = new StringBuf( *data_, n );
if( IntAtomicDecrement( data_->refs ) < 1 )
{ // just in case two threads
delete newdata; // are trying this at once
}
else
{ // now all the real work is
data_ = newdata; // done, so take ownership
}
}
else
{
data_->Reserve( n );
}
data_->refs = markUnshareable ? Unshareable : 1;
}
void String::Append( char c )
{
AboutToModify( data_->used+1 );
data_->buf[used++">data_->used++] = c;
}
size_t String::Length() const
{
return data_->used;
}
char& String::operator[]( size_t n )
{
AboutToModify( data_->len, true );
return data_->buf[n];
}
const char String::operator[]( size_t n ) const
{
return data_->buf[n];
}
}
3. What are the effects on performance? Discuss.
Without atomic integer operations, copy-on-write typically incurs a significant performance penalty. Even with atomic integer operations, COW can make common
String
operations take nearly 50% longer, even in single-threaded programs.In general, copy-on-write is often a bad idea in multithread-ready code. That's because the calling code can no longer know whether two distinct
String
objects actually share the samerepresentation under the covers, so