Due: Check OWL for your lab section deadline Lab 3: Threads and Semaphores 2 1. SE 3313A – Lab 2: Threads and Semaphores Objective 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 Introduction 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: https://github.com/kgrolinger/se3313-2020-lab2 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 3 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: 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 4 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. Deliverables 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 5 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 headers. 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 6 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 7 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 of: 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 boundaries. Lab 3: Threads and Semaphores 8 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 somewhat. 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 9 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.