601.229 (S21): Assignment 6: Multithreaded network calculator

Due dates:

Assignment type: Pair (you may work with one partner, or do the assignment individually)

Overview

In this assignment you will make your calcServer program from Assignment 5 multithreaded, so it can handle connections from multiple clients simultaneously.

Get started by making a copy of your code for Assignment 5 in a new directory. You will modify both calcServer.c and either calc.cpp or calc.c (depending on whether you used C++ or C to implement the calculator functionality.)

Goals of the assignment

The goals of the assignment are:

Grading rubric

Your grade will be determined as follows:

Milestone 1:

Note that at most 24 late hours may be used for Milestone 1.

Milestone 2:

There is also an extra credit option: if you can make your calcServer program shut down cleanly after receiving a shutdown command from a client, exiting only after all client connections have finished, you will receive an extra 2 points. Please note that we will only consider your submission for the extra credit if it implements the base functionality correctly.

Note that at most 48 late hours may be used for Milestone 2.

Milestone 1 tasks

For Milestone 1, your code must compile, and there must be a substantial start on handling client connections using threads. We will expect to see a reasonable thread start function, and a new thread using this start function should be started for each accepted connection. We will also expect to see a struct data type encapsulating the data needed for a client connection. A dynamically-allocated instance of this type should be passed to the thread start function as its argument. At a minimum, this object should contain the client socket file descriptor and a pointer to the shared struct Calc object.

Your code doesn’t need to be fully working, but it must compile and have the elements described above.

Milestone 2 tasks

Your main tasks for Milestone 2 are (1) to use multiple threads to handle client connections (fully working), and (2) to use synchronization to protect shared data so that expression evaluations are atomic.

Using threads for client connections

In general, it makes sense for server applications to handle connections from multiple clients simultaneously. Threads are a useful mechanism for handling multiple client connections because they allow the code which communicates with each client to execute concurrently.

In your main server loop, create a thread for each accepted client connection. Use pthread_create to create the client threads. You can let the client threads be detached (i.e., by having them call pthread_detach with their own thread id.) You do not need to place any upper limit on the number of threads that can be active simultaneously (although in practice that’s a good idea.)

You can test that your server can handle multiple client sessions simultaneously by running 2 (or more) telnet sessions connecting to the server.

Note that as with the server from Assignment 5, all connections should share a common struct Calc instance. This means that a variable set by one client is visible to other clients, and in fact, can be considered a simple form of communication between clients.

The following screen capture shows two instances of telnet connecting to the same calcServer (using GNU Screen as a split-screen terminal):

Using synchronization to protect shared data

Any time two thread access shared data, such that one or both threads might modify the shared data, synchronization is typically necessary to ensure the integrity of the shared data. In addition, synchronization is sometimes necessary to ensure that the desired semantics of accesses to shared data is assured.

Add synchronization to your struct Calc data type so that it is guaranteed that updates to calculator variables are atomic. For example, if multiple clients execute the update a = a + 1 some arbitrary number of times, then assuming that the initial value of a was 0, the final value of a should be exactly equal to the number of times a = a + 1 was executed.

Another way of describing the synchronization requirements is that for any variable update of the form lhs = rhs, where lhs is the variable being updated, and rhs is an expression computing the value to assign to lhs, you must guarantee that any variable or variables accessed in rhs will not be modified while the execution of the overall assignment is in progress.

From a practical standpoint, you should add either a mutex or semaphore (mutex_t or sem_t) field to your struct Calc data type, and then add critical section(s) where needed to ensure that the synchronization requirements are met. Very important: the critical section(s) should be in the struct Calc functions (in calc.c or calc.cpp), and not in calcServer.c.

Important requirement: In your README file, briefly describe how you made the calculator instance’s shared data safe to access from multiple threads. Indicate what kind of synchronization object you used, and how you determined which regions of code were critical sections.

Clean shutdown (extra credit!)

You can ignore this section if you’re not planning to try the extra credit, although what’s described here is useful stuff to think about if you’re interested in systems and network programming.

One difficulty in implementing a multithreaded server is how to allow it to shut down cleanly.

For calcServer, the problem is that when one client sends a shutdown command, there could be other threads still running, and shutting down the server would interrupt these connections.

For up to 2 points extra credit, you can implement the shutdown command such that the server will exit

In addition, once a shutdown command has been received, calcServer should not accept any further client connections.

Shutting down cleanly is fairly challenging. Here are some rough ideas that might be useful:

The reason that calls to accept need to be nonblocking is because if the server is stuck waiting for an incoming client connection, it might not be aware that one of its currently-connected clients has requested a shutdown. By using a timed wait, the server can “wake up” periodically in order to check the global shutdown variable.

Testing

Here are some automated tests you can try.

Download the following files into the directory containing your calcServer executable:

You can download the above files from a terminal by running the following commands:

curl -O https://jhucsf.github.io/spring2021/assign/assign06/test_server_concurrent1.sh
curl -O https://jhucsf.github.io/spring2021/assign/assign06/test_server_concurrent2.sh
curl -O https://jhucsf.github.io/spring2021/assign/assign06/test_server_concurrent_stress.sh
curl -O https://jhucsf.github.io/spring2021/assign/assign06/test_input.txt
curl -O https://jhucsf.github.io/spring2021/assign/assign06/conc_test_input1.txt
curl -O https://jhucsf.github.io/spring2021/assign/assign06/conc_test_input2.txt

Make the scripts executable:

chmod a+x test_server_concurrent1.sh
chmod a+x test_server_concurrent2.sh
chmod a+x test_server_concurrent_stress.sh

First test: run the following commands:

./test_server_concurrent1.sh 30000 test_input.txt actual1.txt
cat actual1.txt

The output of the cat command should be:

2
3
5

This test tests that a long-running client does not prevent the server from handling an additional client connection.

Second test: run the following commands:

./test_server_concurrent2.sh 30000 conc_test_input1.txt actual1.txt conc_test_input2.txt actual2.txt
cat actual1.txt
cat actual2.txt

The output of the first cat command should be:

1
42

The output of the second cat command should be:

40
54

This test tests that two client sessions can interact with each other through commands accessing a shared variable.

Third test: run the following commands:

./test_server_concurrent_stress.sh 30000
cat final_count.txt

The file final_count.txt must contain the value 400000. Any value less than 400000 means that expression evaluation is not atomic, so the thread synchronization does not meet the requirements.

Deliverables

You should not need to make any changes to your Makefile.

To submit your work, run the command

make solution.zip

As you did with Assignment 5, make sure that your solution.zip contains a README file (in addition to the other required files.)

Upload solution.zip to Gradescope as Assignment 6.