• No se han encontrado resultados

EL IDEAL DE LO SALUDABLE Y EL CUERPO MEDICADO EN LA VEJEZ

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 other

String

's data buffer won't be modified or moved during this operation, because it's the responsibility of the caller to serialize access

to 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 to

AboutToModify()

, 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 the

refs

value and again when updating the

refs

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 update

refs

:

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()

and

operator[]()

don't need locks because once

AboutToModify()

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 our

StringBuf

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: an

that 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 same

representation under the covers, so

String

must incur overhead to do enough serialization so that calling code can take its normal share of responsibility for thread safety. Depending on the availability of more-efficient options such as atomic integer operations, the impact on performance ranges from moderate to profound.