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
|