Questions related to Project 5

New questions will be put at the top.

    Added 11/19:

  • Scheduling advice:
  • This is just an advice. Do the scheduling in thread_join(). What is scheduling? Scheduling is anything that involves with manipulating the ready queue Clarified to be more specific: Scheduling is anything that involves deciding which thread to run next (i.e. manipulating the head of the ready queue). So far, we have received some problems. When a thread calls thread_yield, some of you schedule the next thread to run within the thread_yield function (i.e. modifying the ready queue, and swapping context to the next thread). Hence, the scheduling is done within the context of the currently running thread. As a result, the logic is becoming too complex. A better implementation is to do the scheduling in the main context in thread_join. So if you need to schedule a new thread to run, always swap back to the main thread (your thread library), and do the scheduling there. Do not scatter around your scheduling code.
  • Test 5 has been fixed.
  • Please upload the new test5.c
  • I got a seg-fault ...
  • There are many reasons you can get a seg-fault. For this project, some students got a seg-fault because of wrong initialization of the stacks. Each thread must has its own exclusive stack, which is allocated in the heap. Second, make sure you allocate your stack properly, i.e. if you use malloc to setup your stack, make sure that whatever size X you pass to sizeof(X), X is 256 KB. Third, when you set the context stack size "uc_stack.ss_size = X", also make sure that X is 256 KB. A common mistake is to write it like: "uc_stack.ss_size = sizeof ptr". Although ptr points to 256 KB area in memory, ptr is a pointer, hence sizeof ptr will give you 4 B rather than 256 KB.
  • One join per thread-id:
  • To simplify the project a little bit, you can assume we only do one join per thread id. There is no such thing as "reusing" old threads. In other words, since we only support 100 threads, you program can only create 100 threads in its life time. Thus said, we will modify test5.c soon.

    Added 11/14:

  • More hints on condition variables:
  • First of all, this project is not about semaphore. Don't get confused between wait(semaphore) and wait(condition_variable)!

    Each lock holds a true/false value, and has a queue (if you want to put more stuffs in a lock that is fine).

    A lock is also passed to wait(lock,cv) because usually an application calls wait(lock,cv) when a lock is acquired.

    application() {
    	 lock(L1);
    	 while (something) {
    		 wait(L1, cv);
    	 }
    	 // do something ..
    	 unlock(L1);
    }
    

    But since wait() on a condition variable always put the calling thread to sleep, wait() must release the lock (e.g. L1) first. Otherwise other thread cannot acquire L1 until the waiting thread is signaled by someone. For example, if a thread T1 calls wait(L1, CV), you simply set L1->value to false (which also implies you should wake a thread from the head of the L1->queue, if any, and put it to the ready queue), and then put T1 at the tail of CV->queue.

    Say another thread calls signal(CV), and so far only T1 is in CV->queue. Then, signal(CV) will check if T1 can be put to the ready queue or not. That depends on whether L1 is locked or not .

    If L1 is locked, T1 should be put to L1->queue. In other words, although T1 has been signaled, another thread is still holding L1. Hence T1 cannot continue; in order to continue properly T1 must acquire L1 which has been released in wait().

    If L1 is not locked, then set L1->value to true, and put T1 to the ready queue.

    Revision: Say another thread calls signal(CV), and so far only T1 is in CV->queue. Then, signal(CV) will put T1 to the ready queue. Once T1 is picked to run by the scheduler, T1 will continue executing inside the wait() function (Remember that T1 was put to sleep inside the wait() function). Before returning from the wait() function to the application, T1 must try to acquire L1 again inside the wait() function.

    Think about this for a while. Basically, we accept any implementation as long as it follows this rule: wait(lock,cv) will release lock and put the calling thread to sleep, and when wait() returns to the caller (because someone has signaled on the cv), the calling thread should already acquire the lock again.

  • Will thread_join() only be called from the main method or can threads create other threads and thus wait for a join on them?
  • We made the problem simpler. thread_join can only be called from the main thread . This should simplify the scheduling logic (i.e. whenever a thread blocks (due to yield/lock/wait), the control is always given back to the main thread).
  • More explanation on thread_join:
  • The pseudo-code for thread_join looks something like this:
    thread_join(tid) { schedule: next_thread = getNextThreadToRunFromReadyQueue(); // swap, main_thread relinquish CPU to // next_thread swapcontext(main_thread, next_thread); // the execution will come back at this point // if main_thread gets the control back. // This means that the next_thread is either // blocked or has finished. How do we know? // There must be some kind of flag. if (next thread is blocked) { // if next_thread was yielding, // put the next_thread to the back of the FIFO queue // if it was blocked because of lock/wait, then // the thread should already be in the lock/wait queue goto schedule; } else { // next thread has finished if (next_thread's tid == tid) { // if this thread is what the main thread is waiting // for, then return saying that the thread has finished } else { // do nothing, and schedule again goto schedule; } } }
    Note that the code above is neither absolute nor complete ; it's just describing the logic. You can implement the logic in anyway you want.
  • I am wondering if I have created a bunch of threads, and call join on one of the threads, most likely, that thread won't be the head of the ready queue. Should I just go and execute it anyway, or should I execute all the threads ahead of this one in the ready queue, and then execute this thread?
  • As the ready queue is FIFO, so you should not execute that thread directly. The thread_join can return after the particular thread finishes.
  • (1) i am wondering if a lock has a "blocked" queue associated with it at all? or rather, when a thread tries lock() and cannot walk thru, it will be simply put to the end of the ready queue? i think conditional variable should have such a blocked queue associated with it in theory so that when a thread finishes, it could call signal() to invoke all the waiting threads. but how the queue attached to the conditional variable sync with the "ready queue"? say, when a thread finishes and does signal() to swap to the other thread, should it swap to the head of the ready queue or to the head of conditional variable queue. if swap to the conditional variable queue, when one thread finishes, should we delete its corresponding entry in the ready queue. (2) i guess my question in general is what ready queue really is? it only contains the runnable threads(excluding all the finished threads) or rather it is a thread pool that contains all the created threads no matter their statuses.
  • Each lock should have a queue within it. blocked thread should not be in the ready queue. In signal(), the woken thread should be put to the end of the ready queue. Ready queue should only have runnable threads. You can read the "Scheduling order" section in the spec. Also if a thread has just been woken up (it is set to ready/runnable) by a signal or an unlock, put this thread at the end of the ready queue.

    Added 11/12:

  • If a thread with tid 3 has finished running, but the main thread never calls thread_join(3), can I reuse thread-3?
  • No, you should not reuse a tid that has been used but not joined. Hence, you need a flag for each thread to determine that. In fact, you can maintain as many flags/states you need (e.g. if a thread is yielding, running, runnable, etc).
  • If a thread calls thread_yield(), where should I put that thread in the ready queue (the tail or at the head after the next scheduled thread)?
  • For simplicity, if a thread calls thread_yield(), put the thread at the tail of the ready queue.
  • When I create a thread, I simply put a thread into the ready queue. If the ready queue is empty, should I run it?
  • As stated "When a thread calls thread_create, the caller does not yield the CPU. The newly created thread is put on the ready queue but is not executed right away." In other words, when you create a thread, it means the caller is using the CPU right now. you should not run the thread because the caller is still using the CPU. When the caller (e.g. the main thread) calls thread_join, then your scheduler can run the thread.
  • I am somewhat puzzled by what we should implement in project 5. in general, should we just implement those interfaces that will look like the pthread library? say, thread_lock ->pthread_mutex_lock, thread_unlock->pthread-mutex_unlock, thread_wait->pthread_cond_wait, thread_signal->pthread_cond_signal.
  • Yes, implement those interfaces, which also implies that you should implement the lock_t and cond_t structures (e.g. their internal queues, etc.). You should not use pthread_mutex_t or pthread_cond_t.
  • Also, I was wondering how the FIRST created thread gets the CPU yielded to it? Should we simply check if we are the ONLY thread in the ready queue, and if so, get the CPU?
  • when your main thread calls thread_join(tid), you should call your function that performs the scheduling. Then the first thread gets to execute.

    Added 11/10:

  • Is there a maximum number of threads that we should support or should we support unlimited threads?
  • You should support up to 100 active threads.
  • What should we do in a situation where we're supposed to replace a currently executing context with one from our ready queue (ie, a call to thread_wait, or thread_join), but our ready queue is empty? Clearly this is deadlock, because it implies that all other active threads are blocking in some queue associated with a lock or condition variable, or something along those lines. Currently my library terminates the application, as that seems like the only sensible thing to do, since execution cannot continue if all threads are waiting on another thread, but I wasn't sure.
  • In that case, please just call exit(1).
  • I would like to know weather thread_yield should consider the main thread as a separate thread? Should we put the main thread on to the runnable queue as well?
  • we will not call thread_yield from the main thread. the main thread will just create threads and relinquishes the cpu when it calls thread_join.
  • I would like to know if one thread locked a lock, then another thread tries to unlock that thread, what should we do? throw an error?
  • this is just a strange program. your implementation should not have to worry about it (e.g., no extra code added to guard against this type of odd behavior)
  • I would like to know what should happen when we get a call of thread_signal before any thread_wait. Should we treat that as an error or should we handle it?
  • it is not an error, per se. the signal should just succeed (but without any effect, if no one is waiting)
  • Regarding this project, when should the program finally terminate - When all threads have completed execution, or when the main() routine returns?
  • Depends on your test code. If you create a main function that test your library, but you never call thread_join, then your program will terminate before the threads execute (which is not the fault of your library).
  • I am not sure where this lock_t *lock and cond_t *cond are from. Are they from Pthread? or from the library of what we have to implement?
  • you have to implement them. a sample program could be as simple as: create a few threads have them each update a shared variable in a loop have them call lock and unlock around the shared variable access.
  • Should we return null when we run out of memory?
  • Return -1