C/C++代写|操作系统代写 - SE 3313A – Operating Systems
Due: Check OWL for your lab section deadline

Lab 3: Threads and Semaphores
1. SE 3313A – Lab 2: Threads and Semaphores
The objectives for this lab are as follows:
1. Write an application that spawns and kills multiple threads
2. Work with semaphores
3. Work with UNIX shared memory objects
4. Practice working with existing code

This document contains a lot of information. This section describes the tasks for Lab 2 while
section 2 “Resources for Lab 2” provides additional information for completing this lab. I
STRONGLY recommend that you read it all the way through.

In this lab, you will create two applications, called Reader and Writer, starting from the code at
this repository:


Writer is a (minimally) user-interactive application. It repeatedly asks if the user would like to
create a new thread. As long as the user keeps answering “yes”, it then asks (for each new thread)
what the thread wait time should be. The user enters some number of seconds (let’s call it N).

The Writer then spawns a new thread with sleep time of N seconds. Each thread has the SAME
job, which is to write some information to a shared memory object once every N seconds. The
information to be written is thread Id, report Id (ie: how many times has this thread reported)
and metric of how much time has passed since the last report. Each write operation overwrites
what was previously in the shared memory object.

This process continues, with the Writer spawning new threads every time the user says “yes”. In
principle your system should accept an unlimited number of threads, but you shouldn’t need to test
it for more than three or four. Once the user says “no”, the Writer should cancel the threads, wait
for them to die, and exit.

Reader has only one job, which is to monitor the contents of the shared memory location and
report to the user. It does not need to accept user input, but rather runs for some fixed time, or until
you kill it.

Here is a picture of two terminals, one running Writer and the other running Reader.
Lab 3: Threads and Semaphores

You need to perform the two tasks: one without semaphores and one with semaphores.

RECOMMENDATION: Use provided classes, such as Thread, Semaphore, and Shared to
implement the two versions. Sections 2 “Resources for Lab 2” of this document provides
descriptions of the provided classes. Writer class (Writer.cpp) you obtain from GIT includes
a possible starting point for the use of those classes.

I. Task 1: No Semaphore Version

Write a Writer/Reader pair that does not use semaphores. In this case, you will probably want to
use sleep() or usleep() to let Reader periodically poll the shared memory.

It is important to note that this version of the software SHOULD NOT WORK correctly. The entire
point of semaphores is that things like this should not work correctly. Once you have the code
running, let it run for a while and see if you can find evidence of failure. Answer the following

Question 1: Can you find evidence of a thread report getting obliterated? That is to say, some
thread should have reported and you can’t find its report? For example, thread #3’s report #4 is
just gone? What is causing this? (In this case, I expect you will certainly see such events.)

Submit your code as per instructions in the deliverables section. With answers to question 1,
provide evidence of your program function.

II. Task 2: Semaphore Version

Re-create the system properly with semaphores. Reader must read the buffer before another Writer
overwrites it. Reader shouldn’t need to poll anymore.

Lab 3: Threads and Semaphores
In this case, please answer the following questions:

Question 2: Explain your semaphore design. How are the semaphores being used? What is their
function? How do they affect the various threads and processes?

Question 3: Do you still see any corrupt or missing reports? Could you see any corrupt or missing
reports? Explain.

Submit your code as per instructions in the deliverables section. With answers to questions,
provide some evidence of your program function.

1. ZIP file with code for task 1
• Name your submission: se3313a-username-lab2_1.zip
2. ZIP file with code for task 2
• Name your submission: se3313a-username-lab2_2.zip
3. PDF file with answers to questions 1 to 4 and discussion

If the student did not demo the solution to TA during the schedule lab time due to illness or other special
circumstances (with documentation as per the course outline), the student must contact his/her assigned
TA as soon as possible. For no demo, 50% of the available mark will be deducted.

Lab 3: Threads and Semaphores
2. Resources for Lab 2
I. Compilation

Compiling with shared memory objects, semaphores and threads is pretty complicated and requires
both compilation and link options. Accordingly, a Makefile has been provided for this assignment.
Makefiles are basically a way to simplify maintaining programs and working with command line
options. In the IDE, such as Visual Studio, all the checkboxes you specify to select build options
eventually get turned into a Makefile. Automatically generated ones tend to be very difficult to
read. Hand-crafted ones, on the other hand, are usually quite simple. A part of the Makefile
provided with your Assignment 2 looks like this:

This text creates two make “targets”: one called “Reader” and one called “Writer”. What it says,
translated into English, is as follows:

There is a thing I would like you to make, called “Reader”. It depends on three files, called
Reader.o, thread.o, and Blockable.o. If any of those three files is newer than the file called
Reader, then run g++ with the command line options as specified.

If you cloned the provided repository, you’ll have all the files you need to compile provided code
using Makefile. Then, at the command line, you type make Reader, and the compilation for Reader
happens. To compile all, you can just type make. If you want to add your own header files, you
can add them to the dependency line, to trigger automatic compilation when you change the

II. Creating and Destroying Threads

A class called Thread provided with this assignment wraps up threads functionality into an object-
oriented C++ version. Using the provided Thread class is the recommended approach, but you can
also directly use std::thread

III. 1 Provided Thread class

This class is provided in files thread.h and thread.cpp.
Thread::ThreadMain is what is called a “pure virtual function” in C++. What that means is that
you can’t create an object of type Thread. What you do instead, is create your own class, that
inherits from Thread. As long as your class implements YourClass::ThreadMain, the thread will
be spawned in your code, but it will inherit the behaviour of the base class. Of course, if you want
Lab 3: Threads and Semaphores
more than one different kind of threads running in the same application, you can make more than
one class, each of which inherits from Thread.

Thread Termination
“Killing” a thread is now frowned upon by operating system purists. The preferred approach is to
write your thread as a loop that can be terminated externally. In Thread class provided with this
lab, the Blockable family makes this flexible (see provided Writer.cpp for use).

Notice that Thread contains a Sync::Event called terminationEvent. The LAST thing
ThreadFunction does is trigger this event. As a result, you can wait on this event in your
destructor to make a blocking destructor. Thus, you can allow delete to block and make sure that
the thread has actually terminated before you try to clean up any resources it needs. You may find
this functionality useful in creating threads that terminate without causing system crashes, core
dumps, and unhandled exceptions.

III.2 Working directly with std::thread

If you really want to work directly with threads, C++-11 includes a thread object in the std
namespace. A brief review of its operation is provided here:

The constructor for std::thread looks like this:

As you can see, it is a generic class. The syntax is complicated, and includes the notion of an r-
value reference (specified by &&) and a variable parameter list. Fortunately, we don’t really have
to worry about the details. Basically, what you do is define a Function and provide its Arguments.
The Argument list can have variable length, but must match the arguments expected by the
Function, with one exception. Options for Function are:
1. A plain old function, defined at file scope (static or not). In this case, the Argument list is
the same as the arguments to the function
2. A static member function of a class. In this case, the Argument list is the same as the
arguments to the function
3. A non-static class member function. This is the exception. In this case the first Argument
MUST BE a pointer to an instance of the class. It will become the this pointer when the
thread is executed. The thread then has access to all of that instance’s member data.

Most of the time, all you want to do with a std::thread object is join() it, which blocks the parent
until the thread’s Function returns.

“Killing” a thread is now frowned upon by operating system purists. The preferred approach is to
write your thread as a loop that can be terminated externally. With std::thread, basically it means
having a Boolean variable and checking it each time through the loop.

Lab 3: Threads and Semaphores
III. Shared Memory Objects

In order to use semaphores, we need to have some kind of resource for them to protect. The classic
example is a shared memory location, or shared memory object. You can think of it as a dropbox
that different threads and/or processes can read and/or write at different times to pass messages.
This is basically creating a producer/consumer situation, except that arriving threads overwrite
what is there and the “buffer” has only one element.

Fortunately for us, POSIX supports a very powerful mechanism for creating named shared
memory objects. Unfortunately for us, this mechanism is kind of complicated. For the purposes of
this lab, a template class called template class Shared is provided. This template is
located in a file provided, called SharedObject.h.

A template allows you to write “generic” code, then instantiate specific versions of it by specifying
a type for T. In other words:

Shared sharedInt("john "); //Creates a shared integer object called “john”
Shared sharedPoint("origin "); // Creates a shared object called “origin” of some class Point

You have access to the source code for template Shared, but here’s how it works.

These are the constructor and destructor of Shared. The constructor takes two parameters. The
first one, name, is a string you provide to identify the object. The second parameter is a Boolean
variable that identifies whether the creator of the object should be its owner. The notion of
“ownership” requires some explanation. A shared memory object in UNIX is a chunk of special
memory, allocated on a special heap that is different than the one new uses. What makes it special
is that multiple processes and threads can each have their own pointers to the same chunk of
memory, as long as they know the name of the chunk, and the chunk was created to allow multiple
use. Unfortunately, these objects are not reference counted, and there is no garbage collection.
There is a function that causes the object to cease to exist (shm_unlnk), but once that is called,
nobody new can use the memory, although people who already have it can still use it.
Shared is used as follows:
• Each thread or process that wants to use shared point “origin” has to create its own version
Shared sharedPoint(“origin”);
• One (and only one) of them should identify itself as the owner. In this case, it’s probably
the parent thread of Writer, who should instead call
Shared sharedPoint(“origin”, true);
• In this way, as long as all your threads and processes use the same name for the shared
object, you can access the same piece of memory across threads, and even across process
Lab 3: Threads and Semaphores

Most of Shared is variations on this theme. Basically, Shared acts like what we call a
“smart” pointer. It contains within it a block of memory of size sizeof(T). As long as the only way
to access that memory is through pointers of type T*, all users of the same named object can
pretend that it is a T. So, the member function get() gives you a T* to work with.

However, since C++ has all kinds of operator overloading capabilities, in provided code, several
things are overloaded so that an object of type Shared has pretty much exactly the same syntax
as a T*. Suppose, for example, that you have:

Then you create:

Assuming somebody has created a shared Point called “origin”, you can now do things like:
theOrigin->x = 0; theOrigin->y=0; and all other users will see it. You can even call theOrigin-
>Move(1,1); and all other users will see that, too.

NOTE ON USAGE: Because shared objects live in, and are created in, a special shared heap, they
can’t be resized. Don’t try to create something like Shared or Shared
or anything like that.

IV. Semaphores

Just as was the case with shared memory, POSIX contains a powerful built-in mechanism to
implement named, counted semaphores. A class Semaphore is provided to simplify matters

Semaphores are named in exactly the same way as shared memory objects, and have the same flaw
(they are not reference counted either). So, the same approach is used to deal with one-owner-
many-users. Once the destructor is called, the name will cease to exist. Nobody new will be able
to access the semaphore, but current users will not be forced to give it up. Only the owner (creator)
can specify the initial state. Since this is a counted semaphore, any integer can be the initial state.

Lab 3: Threads and Semaphores

V. Time

Some of the data that needs to be stored in the shared space is time. UNIX has the following
function, which you may find useful:

time_t time(time_t *t)

time() returns the number of seconds since 12:00:00 am on New Year’s Day, 1970. While this may
strike you as a somewhat arbitrary number, it does mean that the difference between two successive
calls to time() is a measurement of how many seconds elapsed between them.