tags:

views:

322

answers:

8

I have some old code that uses qsort to sort an MFC CArray of structures but am seeing the occasional crash that may be down to multiple threads calling qsort at the same time. The code I am using looks something like this:

struct Foo
{
  CString str;
  time_t t;

  Foo(LPCTSTR lpsz, time_t ti) : str(lpsz), t(ti)
  {
  }
};

class Sorter()
{
public:
    static void DoSort();
    static int __cdecl SortProc(const void* elem1, const void* elem2);
};

...

void Sorter::DoSort()
{
  CArray<Foo*, Foo*> data;
  for (int i = 0; i < 100; i++)
  {
    Foo* foo = new Foo("some string", 12345678);
    data.Add(foo);
  }

  qsort(data.GetData(), data.GetCount(), sizeof(Foo*), SortProc);
  ...
}

int __cdecl SortProc(const void* elem1, const void* elem2)
{
  Foo* foo1 = (Foo*)elem1;
  Foo* foo2 = (Foo*)elem2;
  // 0xC0000005: Access violation reading location blah here
  return (int)(foo1->t - foo2->t);
}

...

Sorter::DoSort();

I am about to refactor this horrible code to use std::sort instead but wondered if the above is actually unsafe?

EDIT: Sorter::DoSort is actually a static function but uses no static variables itself.

EDIT2: The SortProc function has been changed to match the real code.

+1  A: 

C++ doesn't really make any guarantees about thread safety. About the most you can say is that either multiple readers OR a single writer to a data structure will be OK. Any combination of readers and writers, and you need to serialise the access somehow.

anon
A: 

Right now, your code is thread-safe, but useless, as the DoSort-method only uses local variables, and doesn't even return anything. If the data you are sorting is a member of Sorter, then it is not safe to call the function from multiple threads. In gerenal, read up on reentrancy, this may give you an idea of what you need to look out for.

Space_C0wb0y
There is more code after the call to qsort but the crash is always in the SortProc itself.
Rob
+1  A: 

Since you tagged your question with MFC tag I suppose you should select Multi-threaded Runtime Library in Project Settings.

Kirill V. Lyadvinsky
It is using the MT version.
Rob
A: 

what make it thread safe is, whether your object are thread safe, for example to make qsort thread-safe you must ensure that anything that write or read to or from and to the object are thread safe.

uray
+4  A: 

Your SortProc isn't returning correct results, and this likely leads to memory corruption by something assuming that the data is, well, sorted after you get done sorting it. You could even be leading qsort into corruption as it tries to sort, but that of course varies by implementation.

The comparison function for qsort must return negative if the first object is less than the second, zero if they are equal, and positive otherwise. Your current code only ever returns 0 or 1, and returns 1 when you should be returning negative.

int __cdecl Sorter::SortProc(const void* ap, const void* bp) {
  Foo const& a = *(Foo const*)ap;
  Foo const& b = *(Foo const*)bp;
  if (a.t == b.t) return 0;
  return (a.t < b.t) ? -1 : 1;
}
Roger Pate
Why the downvote?
Roger Pate
Actually, the code I posted was wrong and I will update it.
Rob
A: 

As a word of warning, you may find std::sort is not as fast as qsort. If you do find that try std::stable_sort.

I once wrote a BWT compressor based on the code presented my Mark Nelson in Dr Dobbs and when I turned it into classes I found that regular sort was a lot slower. stable_sort fixed the speed problems.

graham.reeds
This is backwards, or you just got stuck with a poor implementation. Stable sorts have one additional requirement that non-stable sorts don't have, and if std::stable_sort really was faster, then std::sort could be re-implemented to just call it. Generally std::sort beats qsort in speed because it can aggressively inline, and for the same reason can result in larger (generated-)code size.
Roger Pate
graham.reeds
Why are you using VC6? It came with an STL implementation from Dinkumware that actually had legal issues (someone was suing either MSFT or Dinkumware; I don't recall which) that prevented MSFT updating it (even bugs had to be fixed manually in the headers by each user). What happened when you tried a compiler that has the benefit of the last *dozen years* of C++ optimization experience? (VC6 was released before the C++ standard in 1998.) I'll agree that C++ optimization is hard, but you can bias any benchmark.
Roger Pate
Even given all that, it's possible you ran into a particular use case that's the opposite of the general behavior (again, "you just got stuck with a poor implementation" tuned for other situations than yours). Access patterns, expected/best/worst case, branch prediction, and how you run the tests all matter. You even called your own benchmarking "informal".
Roger Pate
I used VC6 because that was what the company was using at the time. Bizarrely I am still using VC6 today (different company though) for maintenance of legacy applications that still need to run but still have few bugs left. As for trying a different compiler, I didn't get the chance as to remove every single error regarding CString would of taken weeks. The BWT was part of a very large, very old system.
graham.reeds
And who's not to say the implementation was bad - it many cases std::sort probably is sufficient. However it can go quadratic - which in my case maybe it was.
graham.reeds
+1  A: 

The pthreads man page lists the standard functions which are not required to be thread-safe. qsort is not among them, so it is required to be thread-safe in POSIX.

http://www.kernel.org/doc/man-pages/online/pages/man7/pthreads.7.html

I can't find the equivalent list for Windows, though, so this isn't really an answer to your question. I'd be a bit surprised if it was different.

Be aware what "thread-safe" means in this context, though. It means you can call the same function concurrently on different arrays -- it doesn't mean that concurrent access to the same data via qsort is safe (it isn't).

Steve Jessop
+4  A: 

Your problem doesn't necessarily have anything to do with thread saftey.

The sort callback function takes in pointers to each item, not the item itself. Since you are sorting Foo* what you actually want to do is access the parameters as Foo**, like this:

int __cdecl SortProc(const void* elem1, const void* elem2)
{
  Foo* foo1 = *(Foo**)elem1;
  Foo* foo2 = *(Foo**)elem2;
  if(foo1->t < foo2->t) return -1;
  else if (foo1->t > foo2->t) return 1;
  else return 0;
}
SoapBox
But most of the time the code posted works - surely it would crash each time if this correct?
Rob
@Rob: No, only when the memory access causes an access violation, as you found out. Even when it appears to work, you're not really accessing what you think you're accessing, and you'll get unpredictable results. Your (incorrect) casts mean your code has undefined behavior, which isn't guaranteed to crash.
Roger Pate
Yep, this is the cause of the issue I think. Superb!
Rob