Re: A simple unit test framework
James Kanze wrote:
On May 7, 10:42 pm, Gianni Mariani <gi3nos...@mariani.ws> wrote:
....
For some meanings of thread safe.
For the only meaning that makes sense.
Are stl objects thread safe ? i.e. can I call string::assign from
multiple threads simultaneously ? I know of no STL implementation that
allows that. All recent implementations that I have used though do
allow different stl objects to be called by multiple threads
simultanteously.
... Do you know of another
meaning? I don't.
Chris Thomasson on comp.pragramming.threads is proposing calling it
"strong thread safe" and "normal thread safe". Ya, don't start me.
... Thread safe code is code that specifies a
set of guarantees for use in a multithreaded environment, and
conforms to those guarantees. G++ definitly does that (although
there is some question concerning what the actual guarantees are
for std::string).
None of the stl classes as far as I know support simultaneous
modification from multiple threads.
So? Is there any reason for them to?
Yes and no, that's the point. Sometimes it would be nice, but most of
the time no.
Formally, of course, if by STL, you mean the standard library,
doesn't support threading at all. But all of the actual
implementations I know of today do, as an extension.
....
It's a non-const call so the implementation is free to do
whatever it wants to global.
I don't modify anything, so the implementation is not free to do
whatever it wants.
Well, you call a non const method (begin()) and it does some
modification for you, whether you like it or not in this implementation.
It's kind of a given for COW when you pull a pointer. Otherwise,
every iterator gets really complicated. IIRC, the standard wanted to
specifically allow COW implementations of basic_string, maybe this is
where the rubber meets the road on this requirement.
BTW - I added this code below to your GCC bug.
(http://gcc.gnu.org/bugzilla/show_bug.cgi?id=21334) It turns out you
don't need to have multiple threads to expose the bug. See this:
#include <string>
#include <cassert>
std::string str( 1<<20, 'a' );
void foo()
{
std::string str2( str );
const std::string & rstr2 = str2;
// str2.begin(); // uncomment line and all works well
std::string::const_iterator bc = rstr2.begin();
std::string::iterator bnc = str2.begin();
std::string::const_iterator ec = rstr2.end();
std::string::iterator enc = str2.end();
std::string r1( bc, ec );
std::string r2( bnc, enc );
assert( r1 == r2 );
assert( r1 == str );
}
int main()
{
foo();
}
I'm not a C++ lawyer so I'm not sure if the above code is malformed but
I would advocate that it does not for purely selfish reasons. Since no
threads are involved here, I'd say that it's a standard
interpretation/clarification thing.
In my initial implementation of the test I had taken a const reference
to the string (as a default thing I do without thinking) before I called
begin. Then I proceeded to tweak the parameters to trigger the problem,
I has one test run for ten minutes and alas, NO failures. So, I looked
again and I made it non-const and viola immediate failure.
I don't think I would call this one a bug.
It depends on whether G++ wants to support Posix guarantees or
not. As I said, it's not clear. (The next version of the C++
will probably statute one way or the other. I suspect in favor
of requiring this to work, but that suspicion is based on my
knowing the people involved, and not on any specific discussions
or vote.) At any rate, either std::string is in error, or the
code above is in error.
Yup.
(An interesting point if the code above is in error: you
definitely cannot write a test to detect the error using VC++ or
Sun CC, because the code works with those compilers. Obviously,
testing can't reveal dependencies on the compilers you're
currently using.)
? I'm not sure what the point is here ? You can't detect a problem
that's not there ? An astonishing revelation ?
....
Having the computer do the hard work is far better than for me doing the
hard work. My B.S. meter just pegged again.
The problem is that the computer doesn't do the hard work for
you. It does the easy part, saying that there is an error
somewhere. Afterwards, you have the hard part: finding where.
Oh, that's the easy part, give me an evidence trail and I'll find the
culprit. Give me a 100 million lines of code and I'll fall asleep in 20
minutes.
In a code review, this is automatic.
The human is too fallible. Yes, a humans can pick the obvious things
pretty quickly. Things like "are there loops in your resource
acquisition" can at times be hard to tell by reading the code and some
times resource acquisition might be non-obvious, i.e. not just mutexes.
These things can be easily found in a MC stress test.
....
I couldn't get this to compile on my machine; I'm missing some
of the necessary headers.
You will need the latest Austria C++ alpha -
http://netcabletv.org/public_releases/ - warning large download - needs
cygwin on win32 to compile. Some of the code in this alpha is not pretty.
... I'll have to download them first. If
I've understood it correctly, the basic idea does seem
interesting. I think it still depends somewhat on the
scheduling algorithm being used by the system, but I'll try to
find time to evaluate it thoroughly.
... (It obviously depends
somewhat on the scheduling algorithm, because the code is thread
safe if non-preemptive threading is used. But that's not the
default on most modern platforms.)
Most OS's on machines with multiple processors will do the same kind of
thing and yes, it does depend on pre-emptive multitasking but all modern
OS's (NT,W2K,XP,OSX,Solaris etc) are.
Machines with multiple cores or hyperthreads are pretty much the norm.
[...]
// Should this be const or not - I think it should
// James Kanze possibly believes otherwise.
Just a nit: in real code, if I were accessing through a
reference, I would use a const reference unless I planned on
modification (in which case, a lock is required). My problem is
when the code accesses the object directly, without an
intervening reference. (I presume, here, however, that the
reference is an artifact of the test suite, and is used to
simulate accessing the object directly.)
Well yes, in theory it is an artifact, however my first cut at the code
had me taking a const reference as a default thing.
Of course, the question here isn't how the user should write
code, but whether the specific construction should be guaranteed
to work or not. Posix bases its guarantees on whether
modification takes place, not on const-ness, which is the basis
of my argument.
[...]
enum { s_count = 43 }; // prime number
Just curious: why is it important that s_count be prime?
Because of this:
( i * pnum ) % g_test->s_count;
I want every thread to be able to access every element. If it's not
prime (relatively prime) then some threads won't access all the strings.
static const unsigned s_length = 3; // tweak for maximum effect
std::string m_strings[ s_count ];
virtual void test2( int l_val, std::string & o_tval )
{
t_local_type & l_str = m_strings[ l_val ];
t_iter_type beg = l_str.begin();
// at::OSTraitsBase::SchedulerYield();
o_tval.assign( beg, l_str.end() );
Just curious: is there some reason behind using o_tval.assign,
and not simply:
std::string localCopy( l_str.begin(), l_str.end() ) ;
A hunch - mabe a false one. I'm trying to keep the loop from doing too
many other things that allocate memory and produce contention - avoid
the threads from synchronizing if possible. Keeping a pre-constructed
string in the thread's stack might avoid (maybe not) a memory
allocation. In this case, it probably will.
followed by the AT_TCAssert?
TCAssert is "Test Case" Assert. It's an assert for the test case, it
normally throws on failure (aborting the rest of the test) but it can
also abort and dump core (which the Makefiles do when running automated
tests).
[...]
class HardTask
: public String_TaskBase< 2 >
{
public:
static at::AtomicCount s_task_counter;
static int s_xcount;
// each test thread calls this with a unique thread number
virtual void TestWork( int l_thrnum )
{
unsigned pnum = g_primes[ l_thrnum % at::CountElements( g_primes ) ];
unsigned count = l_thrnum;
unsigned done = 0;
std::string l_teststr;
for ( int i = 0; i < m_iters; ++i )
{
int choice = ( i * pnum ) % g_test->s_count;
switch ( l_thrnum & 1 ? i % 8 : (7 - (i % 8) ) )
{
case 0 : case 2 :
g_test->test1( choice, l_teststr );
break;
case 1 : case 3 :
g_test->test2( choice, l_teststr );
break;
case 4 :
g_test->test3( choice, l_teststr );
break;
case 5 :
g_test->test4( choice, l_teststr );
break;
}
// after every 1<<5 iterations - rebuild the test object
// with brand new strings
if ( true && !( i % (1<<5) ) )
{
// stuff is done here
at::Lock<at::ConditionalMutex> l_lock( m_mutex );
int l_num = ++ s_xcount;
if ( m_thr_count == l_num )
{
s_xcount = 0;
g_test = new STDStringTest();
l_lock.PostAll();
}
else
{
l_lock.Wait();
}
}
I'm not sure I understand this. How is it synchronized with the
previous loop? If some of the threads are still in the previous
loop when a thread gets here, then it is undefined behavior.
Even using const_iterators.
It's a standard barrier, all the threads except one drop into the
condition variable Wait. The last thread resets a few things and pulls
all the threads out of the wait. Bad code - I really need to implement
a standard barrier class and fix it a little bit. Threads can drop out
of lock.Wait() spuriously according to pthread_cond_wait but I have
never witnessed it in a unit test.
I've saved a copy of your code locally, and will try to find
time to download your test suite in order to evaluate it on my
machines. If you really can trigger threading errors with any
degree of reliability, I'm very interested.
Well, writing test cases that really push the interface still needs to
be done, and no it's not a design test (As in TDD). It has however
uncovered some really contorted bugs that I would never have imagined
way before code release. For example, in the Austria C++ timer code, a
very contorted shutdown sequence could happen that needed a "take the
lock but give up if X happens".
This is the comment:
* On shutdown, the client thread locks *m_mutex, sets the
* m_shutdown flag, and waits for this thread to die (while
* still holding *m_mutex). If we try to lock *m_mutex
* normally here, it will be a deadlock. So we spin until
* either we get the lock, or m_shutdown is set.
TryLock< Mutex > l_trylock( *m_mutex, m_shutdown );
This one was found using the timer MC test. This is the kind of bug
that usually gets through the human only process. I'm pretty confident
that the Austria C++ timer code will handle almost any situation because
it's pushed very hard on every test run with random new requests every
time it is run. Sure, there is a more regular test as well and it found
bugs that the MC test didn't.