Karl von Moller wrote on Wed, 7 May 2008 09:51:56 +1000:

> Thanks to all that posted.
> 
>> To the original poster:
>>
>> How much experience do you have with threads?  I'm a little confused
>> reading through your posts, I can't tell if you are familiar with
>> pthreads, and just need to figure out NSThreads, or if you have no
>> threading experience at all.
> 
> As I said earlier on, I don't have ANY experience with threading at
> all - this was my first attempt to try and understand how to implement
> a threading scheme. The Apple documentation was my first port of call
> and at least the basics of it, I got from those articles. I found a
> great deal of reading material online however I think the noise of
> hearing the same comments "Threading is hard ..." etc made this all
> the more difficult. This mailing list has in one day at least, given
> me some clear guidance on my next approach which I really appreciate.
> At some point though, to take things out of the theoretical, you have
> to put things into practice. Granted my approach may have been very
> hit and miss but it certainly accelerated my learning!

In that case, I suggest getting used to pthreads with some simple C
programs first.  This will reduce the number of things that can go wrong
to a fairly small minimum (you don't have to wonder if its you or
Cocoa).  Many of the common C standard libraries are now required to be
thread-safe (at least, if you are using C99), so things like printf()
will work out of the box.  A fairly good book to learn from is
Programming with POSIX Threads by David R. Butenhof.
(http://www.amazon.com/Programming-Threads-Addison-Wesley-Professional-Compu
ting/dp/0201633922/ref=pd_bbs_1/104-4945869-2700757?ie=UTF8&s=books&qid=1210
159650&sr=8-1)
Once you understand the basics of what threads are, then you can start
working on NSThreads (which, IIRC, wrap pthreads).

As for why threads are 'hard'; in reality, they aren't hard, as long as
you are absolutely fastidious in following the rules:

1) Don't access shared variables unless you've got possession of the
lock.  If you must own several different locks at the same time, enforce
an ordering on how you grab the locks; that is, if each thread must own
locks A, B, and C in order to get any work done, then the threads must
lock the locks in that order each time.  I'm sure you've read about
deadlock by now; the fastest way to cause it is to have one thread grab
locks A, B and then another one to grab them B, A.  Somewhere along the
line, one thread will have A and be waiting for B, while the other has B
and is waiting for A.  Enforcing an order prevents this.

2) Check your condition variables in a while loop, to make sure you
weren't woken up by accident.

3) I don't care how 'small' or 'atomic' the shared variable is, use a
lock.

4) Signals are bad in multi-threaded environments; accept that the
system may pass signals to you, but don't use signals yourself.  BTW,
the pthreads API is a little confusing here, there is a function in
there called 'pthread_cond_signal()'.  That one is safe to use, although
'pthread_cond_broadcast()' is better for beginners.  The one that can be
dangerous is 'signal()' (see 'man 3 signal').

5) WHAT PART OF 'USE A LOCK' DIDN'T YOU GET???

Sorry, that last one is for all the code I've had to debug over the
years...

Here are some of the reasons that threads are 'hard'

1) Threads introduce non-deterministic behavior into your program; just
because it didn't crash this go around, doesn't mean it won't crash the
next time, or on someone else's machine.  This non-determinism is VERY
hard to debug, because the mere act of loading the code up in a debugger
will cause the program's behavior to change, possibly masking what went
wrong.

2) Compilers are smart; they'll keep stuff in registers as long as
possible, including things that are shared between threads.  Anything
that is shared, but in a register, won't be seen between threads, so code
that should be sharing stuff won't be. That is one reason why you MUST
use a lock around all shared variable access, and around any 'sensitive'
code (code that temporarily breaks invariants).  Part of the code of the
lock includes something like OSMemoryBarrier() in
/usr/include/libkern/OSAtomic.h.  A memory barrier forces anything that
is supposed to be in memory to be written all the way out to main memory
before any further processing happens; that ensures that shared
variables are updated properly.

3) 'Small' variables like chars or ints look like they should be written
atomically to main memory, but this isn't always the case; on some
systems, the only way to write out a single character is to read in 32
or 64 bits, mask off the portions of the variable you don't want to
change, write the character into the variable, and then write the whole
32 or 64 bit chunk back into memory.  That means that without a lock,
even a char can cause problems.

4) Threads all share the same address space, but have different stacks.
The stack is the important problem here; it is possible to have a stack
that is so deep that one thread's stack overwrites another thread's
stack (look up 'stack smashing', which is similar).  This might not seem
likely until you've run into anyone that attempts to put large arrays
onto the stack, or finds highly recursive functions to be a pleasure to
use.  Valgrind (http://valgrind.org/) is particularly nice for finding
this, but it is Linux only for right now.  There are other tricks to use
to solve this as well, but they are basically hacks (e.g. guard pages
and other canaries, etc.)

In general, I'd say threading isn't any more or less difficult than any
other programming; its just that if you're used to hacking something out
and then using the debugger to fix the problems, then it will be hard.
If you plan and design carefully, choosing what will be shared and what
will be private, then it won't be too bad.

>> to have a pool of worker threads that render the thumbnails/PDFviews
>> in the background.  It would simply be a matter of having a
>> thread-safe priority queue (so if the user clicks on a thumbnail, it
>> gets jumped to the head of the queue), and let the threads grab
>> whatever happens to be the highest priority to work on, returning the
>> results to the main thread to display later.  Alternatively, each
>> time you click on a thumbnail, it could spawn a thread, with the
>> thread returning the results to the main thread when it is done.
> 
> This idea is exactly what I was trying to achieve - The idea of the
> thumbnail creation and the PDF loading happening in a separate thread.
> However to have a thread safe priority queue to organize the "Worker
> Threads" sounds like the missing link - And with that some safe way to
> cancel Threads that have been outdated by the user before that Thread
> had time to set the PDF View! I am assuming in a priority queue this
> function could exist?

A priority queue is simply a way of organizing chunks of data, much like
a regular queue, a tree, or another data structure.  The thread safety
comes from protecting all accesses via locking; e.g., the queue's
appendData() function will internally lock a lock, do all changes that
are necessary, and then release the lock before returning from the call.
One of the simplest ways to do this is to write an ordinary queue (or
use something like stl::priority_queue<>) and then write a wrapper using
a decorator pattern that handles the locking.  This lets you use return
statements anywhere you want in the ordinary queue, while the
thread-safe wrapper catches the return value, and does the necessary
unlocking.  E.g. (all code written in my mail client, may have bugs):

#include <assert.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>

struct nonLockQueueContext
{
    uint8_t *data;
    size_t dataSize;
};

struct lockingQueueContext
{
    struct nonLockQueueContext nonLockingContext;
    pthread_mutex_t lock;
};

int nonLockingInsert(   struct nonLockQueueContext *context,
                        void *data, size_t dataSize)
{
    if (context == NULL)
        return -1;
    
    if (data == NULL)
        return -2;
        
    if (dataSize == 0)
        return -3;
    
    // Any other checks and return values you can think of
    
    // Put the data in the queue
    
    return 0;
}

int lockingInsert(  struct lockingQueueContext *context,
                    void *data, size_t dataSize)
{
    int result = 0;

    if (context == NULL)
        return -1;
        
    assert(pthread_mutex_lock(&(context->lock)) == 0);
        result = nonLockingInsert(  &(context->nonLockingContext),
                                    data, dataSize);
    assert(pthread_mutex_unlock(&(context->lock)) == 0);
    
    return result;
}

Canceling threads is another thing entirely; pthreads DOES have the
pthread_cancel() function, but if threading is a way of shooting
yourself in the foot, pthread_cancel() involves an unmarked minefield;
avoid it if you can.  (For all you pthreads experts out there; do YOU
know what ALL the cancellation points defined by the API are?  Better
yet, since NSThreads are based on pthreads, do you know when Cocoa will
drop out due to a cancellation point???)

A relatively safe way of canceling a thread is to create a cancel
variable and a lock to go with it (and I actually tested this code!)

#include <assert.h>
#include <pthread.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t myLock = PTHREAD_MUTEX_INITIALIZER;
bool dieNow = false;

void *threadFunc(void *arg)
{
    printf("About to start loop.\n");

    while (true)
    {
        assert(pthread_mutex_lock(&myLock) == 0);
            if (dieNow)
            {
                assert(pthread_mutex_unlock(&myLock) == 0);
                break;
            }
        assert(pthread_mutex_unlock(&myLock) == 0);
        
        // Whatever processing you need to do, which may require another
        // lock.  Be sure, it is short!  Otherwise, it will take a while
        // for the thread to cancel.
    }
    
    printf("Got message to quit.\n");
    
    return NULL;
}

int main(void)
{
    pthread_t ID;
    pthread_create(&ID, NULL, threadFunc, NULL);
    
    sleep(1);
    
    assert(pthread_mutex_lock(&myLock) == 0);
        dieNow = true;
    assert(pthread_mutex_unlock(&myLock) == 0);
    
    assert(pthread_join(ID, NULL) == 0);
    
    return 0;
}

I keep saying I'll write a book on all of this, but I never get around to
it...

Good luck,
Cem Karan

_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
http://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to [EMAIL PROTECTED]

Reply via email to