You need Python 3.10.x or above to complete this lab.
In deadlock avoidance, before granting a resource request (even if the request is valid and the requested resources are now available), we need to check that the request will not cause the system to enter a deadlock state in the future or immediately.
The Banker’s Algorithm is a resource allocation and deadlock avoidance algorithm. It can be used to predict and compute whether or not the current request will lead to a deadlock.
- If yes, the request for the resources will be rejected,
- Otherwise, it will be granted.
There are two parts of the Banker’s Algorithm:
- Resource Allocation Algorithm
- Safety Algorithm
You will be implementing the Safety Algorithm and calling it in Resource Allocation Algorithm in this lab.
Starter Code
Install Python 3.10.x or above
If you don’t have Python 3.10.x installed already, here’s a list of commands you can enter to install:
sudo apt update && sudo apt upgrade
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
sudo apt install python3.10 -y
When done, check that it is installed, and install pip as well:
python3.10 --version
sudo apt install python3-pip
Clone
Download this repository:
git clone https://github.com/natalieagus/lab_banker
You should have the following files:
lab_banker/
|- test_files/
|- q0_answer.txt
|- q0.txt
|- q1_answer.txt
|- q1.txt
|- q2_answer.txt
|- q2.txt
|- q3_answer.txt
|- q3.txt
|- q4_answer.txt
|- q4.txt
|- q5_answer.txt
|- q5.txt
|- .gitignore
|- banker_test.py
|- banker.py
|- README.md
You will be required to modify only certain sections in banker.py
.
Leave ALL other files untouched. Also, DO NOT print
anything else in banker.py
. Only type your answers in the given space labeled in the starter code as TASK 1
and TASK 2
. Remember, DO NOT print anything else, and DO NOT import any other modules, and DO NOT modify any other instructions.
Banker’s Algorithm
Prerequisite
Suppose we have N
processes competing for M
different types of resources. We call these processes “customers” (to these available resources).
In order for the Banker’s Algorithm to work, need to know the maximum demand of process i
for resource j
instances.
System State
We also need to represent the system STATE, such as the amount of resources available per-type-per-process using the following data structures:
- available: 1 by M vector
available[i]
: the available instances of resourcei
- max: N by M matrix
max[i][j]
: maximum demand of processi
for resourcej
instances
- allocation: N by M matrix
allocation[i][j]
: current allocation of resourcej
instances for processi
- need: N by M matrix
need[i][j]
: how much more of resourcej
instances might be needed by processi
For example, the following arrays display the available
, max
, allocation
, and need
values of a current state of a system with 5 processes: P0
, P1
, P2
, P3
, and P4
, and 3 types of resources: A
, B
, C
;
Banker.py
Open Banker.py
and give it a quick read.
There are a few methods defined in Class Banker
:
set_maximum_demand
: implemented for you, this populates the corresponding row ofmax
matrixrequest_resources
: request resource algorithm implementation. Task 1 is here.release_resources
: releases resources borrowed by a customer. Assume release is valid for simplicitycheck_safe
: safety check algorithm implementation. Task 2 is here.print_state
andrun_file
: functions to load the input files and printing the states of the system:available, max, allocation, need
values.
Input Format
The inputs to banker.py
are inside test_files/
. All .txt
files named as qi.txt
are various input files.
The format are as follows:
- The first three lines contain values of
N
(number of processes),M
(number of resource type), and the contents of theavailable
vector. - For lines starting with
c
, it signifies themax
demand of a process for ALL resource types.- The format is
c,pid,rid_1 rid_2 rid_3 ...
- The format is
- For lines starting with
r
, it signifies a request by a process.- The format is:
r,pid,rid_1 rid_2 rid_3 ...
- The format is:
- For lines starting with
f
, it signifies a release of resources by a process.- The format is:
f,pid,rid_1 rid_2 rid_3 ...
- The format is:
- For lines with just
p
, we print the state of the system
The lines in the input are read from top to bottom, and are treated as incoming requests or releases by each process as time progresses.
For instance, open test_files/q0.txt
:
n,3
m,3
a,0 0 0
c,0,2 2 4
c,1,2 1 3
c,2,3 4 1
r,0,1 2 1
r,1,2 0 1
r,2,2 2 1
p
From the first two lines, we know that there are 3 distinct processes (N
) competing for 3 different types of resources (M
).
- Let’s name them as P0, P1, and P2 so that it is easier for us to address them.
- Also label each of the 3 resources as A, B, and C.
From the third line, we know that NONE of the resources are available: available = [0,0,0]
.
In the fourth to sixth line, we can initialise the max
matrix as follows:
max = [ [2,2,4]
[2,1,3]
[3,4,1] ]
This means P0
will need at most 2 instances of Resource A, 2 instances of Resource B, and 4 instances of Resource C during its lifetime of execution. Similar logic for P1 and P2.
In the seventh line r,0,1 2 1
, P0 starts requesting for 1 instance of Resource A, 2 instances of Resource B, and 1 instance of Resource C simultanously.
We will need to run the Banker’s algorithm now to determine whether this request is safe
, and to be granted.
This is already done for you inside run_file
function. Whenever line with r
is encountered, the function request_resources
is called.
- If the request is granted, we need to update
available
,allocation
andneed
. This is already done for you insiderequest_resources
function. - If the request is not granted, we simply ignore that request. The values of
available
,allocation
andneed
remains the same. This is already done for you insiderequest_resources
function.
The same is done for all lines starting with r
.
At the end, we print the state of the system with p
.
In this example, NO resources are available. So obviously we are met with the same state of available
, allocation
and need
after running the file.
You can run the file using the command:
python3.10 banker.py test_files/q0.txt
The output is as such as expected, with allocation
matrix remaining at 0.
Customer 0 requesting
[1, 2, 1]
Customer 1 requesting
[2, 0, 1]
Customer 2 requesting
[2, 2, 1]
Current state:
Available:
[0, 0, 0]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Allocation:
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
Need:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Experiment with the other 5 files as well. Try them one by one and study the result.
python3.10 banker.py test_files/q1.txt
python3.10 banker.py test_files/q2.txt
python3.10 banker.py test_files/q3.txt
python3.10 banker.py test_files/q4.txt
python3.10 banker.py test_files/q5.txt
Resource Request Algorithm
This algorithm is already implemented for you inside request_resources
function. This algorithm is called each time we encounter a resource request (line starting with r
). The function receives two parameters and returns a bool:
def request_resources(self, customer_index, request):
"""
Request resources for a customer loan.
Parameters
----------
customer_index : int
the customer's index (0-indexed)
request : list[int]
an array of the requested count for each resource
Returns
-------
True : if the requested resources can be loaned
False : otherwise
"""
The algorithm goes as follows:
-
If
request[i] <= need[customer_index][i]
for alli < M
, go to step 2. Otherwise, returnFalse
(request rejected) since the process has exceeded its maximum claim. -
If
request[i] <= available[i]
for alli < M
, go to step 3. Otherwise, returnFalse
.- Process i must wait since its requested resources are not immediately available (request rejected).
-
At this point, the requested resources are available, but we check if granting the request is safe (will not lead to deadlock).
- Create a
deepcopy
ofavailable
,allocation
, andneed
- Pass these new data structures to the
safety_check
algorithm, which will returnTrue
(safe) orFalse
(unsafe)
- Create a
-
If the outcome of Step(3) is:
True
: UPDATE all system states concerning Process i (request granted):available[i] = available[i] - request[i]
for alli<M
need[customer_index][i] = need[customer_index][i] - request[i]
for alli<M
allocation[customer_index][i] = allocation[customer_index][i] + request[i]
for alli<M
False
: This means request rejected, and process i has to try again in the future. This is because granting the request results in deadlock in the future.
Example 1
You may run: python3.10 banker.py test_files/q1.txt
and study the output.
Initially, we know that allocation
was initialised as 0 and that available=[5,5,5]
from the third line in test_files/q1.txt
. The first 3 requests are granted:
Customer 0 requesting
[1, 2, 1]
Customer 1 requesting
[2, 0, 1]
Customer 2 requesting
[2, 2, 1]
After granting the three requests above, we have the system state:
Current state:
Available:
[0, 1, 2]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Allocation:
[1, 2, 1]
[2, 0, 1]
[2, 2, 1]
Need:
[1, 0, 3]
[0, 1, 2]
[1, 2, 0]
Further request by Process 0: request=[0,1,0]
is rejected because Process 0 requests MORE than what’s been declared at maximum
.
- It declared that it needs at maximum of 2 resources B (the second type of resource)
- It already held 2 types of resources B –>
need[0][1]: 2
Customer 0 requesting
[0, 1, 0]
Current state: <--- request above is rejected, hence state remains the same.
Available:
[0, 1, 2]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Allocation:
[1, 2, 1]
[2, 0, 1]
[2, 2, 1]
Need:
[1, 0, 3]
[0, 1, 2]
[1, 2, 0]
Example 2
You may run: python3.10 banker.py test_files/q2.txt
and study the output.
This time round, we have N=5
processes and M=3
different types of resources, and available=[10,5,7]
.
After such requests from all customers,
Customer 0 requesting
[0, 1, 0]
Customer 1 requesting
[2, 0, 0]
Customer 2 requesting
[3, 0, 2]
Customer 3 requesting
[2, 1, 1]
Customer 4 requesting
[0, 0, 2]
…we should have only 3 counts of Resource A available (the first type of resource).
However here we witness Process 0 releasing 1 instance of Resource A, and hence the updated system state:
Customer 1 releasing
[1, 0, 0]
Current state:
Available:
[4, 3, 2]
Maximum:
[7, 5, 3]
[3, 2, 2]
[9, 0, 2]
[2, 2, 2]
[4, 3, 3]
Allocation:
[0, 1, 0]
[1, 0, 0]
[3, 0, 2]
[2, 1, 1]
[0, 0, 2]
Need:
[7, 4, 3]
[2, 2, 2]
[6, 0, 0]
[0, 1, 1]
[4, 3, 1]
Task 1
TASK 1:
Call check_safe
algorithm inside request_resources
in banker.py
.
Right now the variable safe
is always set to True
, hence we always grant the request as long as the resources are available and the processes do not violate their max
. For input q0, q1, q2
, we are lucky because we never had any requests that might result in deadlock.
However, if you try running the program with q3
:
python3.10 banker.py test_files/q3.txt
…you will notice that the printed output is not the same as the answer: test_files/q3_answer.txt
. We need to implement and call the check_safe
algorithm to ensure that we get the right answer.
Let’s start easy. Since check_safe
function is already defined (although not yet implemented), call check_safe
method and assign their return value to safe
:
# Task 1
# TODO: Check if the state is safe or not by calling check_safe, right now it is hardcoded to True
# 1. Perform a deepcopy of available, need, and allocation
# 2. Call the check_safe method with new data in (1)
# 3. Store the return value of (2) in variable safe
# DO NOT PRINT ANYTHING ELSE
### BEGIN ANSWER HERE ###
safe = True # Change this line
### END OF TASK 1 ###
if safe:
# If it is safe, allocate the resources to customer customer_number
for idx, req_val in enumerate(request):
self.allocation[customer_index][idx] += req_val
self.need[customer_index][idx] -= req_val
self.available[idx] -= req_val
bank_lock.release()
return True
else:
bank_lock.release()
return False
DO NOT PRINT ANYTHING ELSE. Please remove your debug messages.
The Safety Algorithm
This algorithm is also called each time we encounter a resource request (line starting with r
), but only if we managed to enter Step 3 of the Resource Request Algorithm. The function receives five parameters and returns a bool
:
def check_safe(self, customer_index, request, work, need, allocation):
"""
Checks if the request will leave the bank in a safe state.
Parameters
----------
work, need, allocation : list[int], list[list[int]], list[list[int]]
deep copy of available, need, and allocation matrices
customer_index : int
the customer's index (0-indexed)
request : list[int]
an array of the requested count for each resource
Returns
-------
True : if the request resources will leave the bank in a safe state
False : otherwise
"""
The algorithm goes as follows:
-
Create a vector
finish: list[int]
of sizeN
, initialised to beFalse
for all elements infinish
.- Then, hypothetically grant the current request by customer
customer_index
by updating:work[i] = work[i] - request[i]
for alli<M
need[customer_index][i] = need[customer_index][i] - request[i]
for alli<M
allocation[customer_index][i] = allocation[customer_index][] + request[i]
for alli<M
- This request granting is hypothetical because
work
is a copy ofavailable
(not the actualavailable
). Similar argument withneed, allocation
. In reality, we haven’t granted the request yet, we simply compute this hypothetical situation and decide whether it will besafe
orunsafe
.
- Then, hypothetically grant the current request by customer
-
Find an index
i
(which is a customer) such that:finish[i] == False
andneed[i][j] <= work[j]
for allj<M
.- The two above condition signifies that an incomplete Customer
i
can complete even after this request bycustomer_index
is granted
-
If such index
i
from Step 2 exists do the following, else go to Step 4.- Update:
work[j] = work[j] + allocation[i][j]
for allj<M
.- This signifies that a Customer
i
that can complete will free its currently allocated resources.
- This signifies that a Customer
- Update:
finish[i] = True
- Then, REPEAT step 2
You might want to store the values of
i
each time you execute this Step 3 elsewhere to backtrack a possible safe execution sequence, but that’s not required for this lab.
- Update:
-
If no such index
i
in Step 2 exists:- If
finish[i] == True
for alli<N
, then it means the system is in a safe state. ReturnTrue
. - Else, the system is not in a safe state. Return
False
.
- If
Careless Mistake
Common careless mistake: A lot of people missed the “REPEAT step 2” instruction in step 3. Step 2 and 3 must be implemented in a while
loop, as you might NOT necessarily obtain i in sequential (increasing) order.
Why is that so? Does it mean we can have a safe execution sequence e.g:
1,0,2
for a 3-process system? Yes of course! That simply meansP1
can be executed first, thenP0
, thenP2
. Think about whati
represents (just arbitrary naming of consumer processes), of course a safe execution sequence has nothing to do with their naming!
Example
Run the Banker algorithm with q3
:
python3.10 banker.py test_files/q3.txt
At first, P0 is making the request [1,0,3]
and it is granted as shown in the allocation
matrix:
Customer 0 requesting
[1, 0, 3]
Current state:
Available:
[4, 2, 1]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 1, 1]
Allocation:
[1, 0, 3]
[0, 0, 0]
[0, 0, 0]
Need:
[1, 2, 1]
[2, 1, 3]
[3, 1, 1]
You can verify whether any requests so far is granted by checking that the
allocation
matrix value adds up to the requests made.
Then, P1 requests for [1,1,1]
. This is a legal request, since if the request is granted, then P1 allocation (allocation[1]
is initially [0,0,0]
) will still be not more than its maximum: max[1]: [2,1,3]
.
If your implementation is correct, this should however lead to an unsafe state, and hence the request is rejected and the state of the system remain the same:
Customer 1 requesting
[1, 1, 1]
Current state:
Available:
[4, 2, 1]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 1, 1]
Allocation:
[1, 0, 3]
[0, 0, 0]
[0, 0, 0]
Need:
[1, 2, 1]
[2, 1, 3]
[3, 1, 1]
The same logic applies to samples in q4
and q5
as well:
- In q4, the last request by P0:
[0,2,0]
leads to an unsafe state and therefore rejected. - In q4, the request by P1:
[1,0,2]
is rejected because it leads to an unsafe state. However, after P0 release the resources it held:[1,3,4]
, a repeated request by P1 for the same set of resources[1,0,2]
is granted.
Task 2
TASK 2:
Implement the safety check algorithm inside check_safe
function in banker.py
:
def check_safe(self, customer_index, request, available, need, allocation):
"""
Checks if the request will leave the bank in a safe state.
Parameters
----------
available, need, allocation : list[list[int]]
deep copy of available, need, and allocation matrices
customer_index : int
the customer's index (0-indexed)
request : list[int]
an array of the requested count for each resource
Returns
-------
True : if the request resources will leave the bank in a safe state
False : otherwise
"""
bank_lock.acquire()
# TASK 2
# TODO: Check if the state is safe
# 1. Create finish list[int] of length self.N
# Then, hypothetically grant the current request by updating:
# 1. work[i] = work[i] - request[i] for all i<M
# 2. need[customer_index][i] = need[customer_index][i] - request[i] for all i<M
# 3. allocation[customer_index][i] = allocation[customer_index][i] + request[i] for all i<M
# 2. Find index i such that both finish[i] == False, need[i] <= work
# 3. If such index in (3) exists, update work += allocation[i], finish[i] = True
# 4. REPEAT step (3) until no such i exists
# 5. If no such i exists anymore, and finish[i] == True for all i, set safe = True
# 6. Otherwise, set safe = False
# DO NOT PRINT ANYTHING ELSE
### BEGIN ANSWER HERE ###
safe = True # Change this line according to whether the request will be safe or not
### END OF TASK 2 ###
bank_lock.release()
return safe
Again, do NOT print any other stuffs as part of your answer.
Further Notes
Rejecting Requests
If a resource request is rejected, dont panic, it’s not the end of the world. The process/consumer just need to try to request it again in the future.
How can this be implemented? Usually schedulers are programmed to tackle this kind of cases; e.g: they can be placed to a special queue and will periodically prompt for resource request until granted, or there exists some kind of event-driven solution – it’s free-for-all to implement.
Resource Release Caveat
We also assume that (for simplicity of this lab) a process will not release more than what has been allocated to them (that the value of release
is valid). We wrote this detail under release_resources
function:
def release_resources(self, customer_index, release):
"""
Releases resources borrowed by a customer. Assume release is valid for simplicity.
Parameters
----------
customer_index : int
the customer's index (0-indexed)
release : list[int]
an array of the release count for each resource
Returns
-------
None
"""
print(f"Customer {customer_index} releasing\n{release}")
bank_lock.acquire()
# Release the resources from customer customer_number
for idx, val in enumerate(release):
self.allocation[customer_index][idx] -= val
self.need[customer_index][idx] += val
self.available[idx] += val
bank_lock.release()
Synchronisation
Finally, since max, allocation, need
and available
are shared data structures among all these methods, we protect each method that modifies these values using a reentrant lock: bank_lock = threading.RLock()
.
We guard the critical sections with bank_lock.acquire()
and bank_lock.release()
to make it thread safe.