Imagine a computer playing an arcade game while it waits for a button press. Rather than leave the CPU idle, it can give another task a turn. Preemption lets the operating system make that switch without waiting for the running task to volunteer. section 6.7 section 6.8
In preemptive multitasking, a scheduler gives a ready task a short period of CPU time, called a time slice. When that slice expires, a timer interrupt lets the scheduler pause the task and run another. Rapid switching makes tasks appear to run together even when one CPU core executes only one at a time. section 6.7
An arcade-machine sketch
An arcade machine can alternate between checking for input, computing the game world, and rendering. A Python sketch uses a timer and an event to grant threads turns. It illustrates time sharing, but setting an event does not forcibly stop a thread already executing its function; the operating system supplies actual preemption. section 6.8 section 6.7
A threaded server
A socket server can start a Python thread for each client connection. The operating system schedules those threads, so one client’s wait for input need not prevent another client’s request from being handled. The programmer can write each handler as a straightforward sequence of operations. section 10.6
Preemption differs from cooperative multitasking: cooperative tasks must choose when to yield, while a preemptive scheduler can interrupt them. Neither approach makes switching free. Too many runnable tasks can waste time on context switching, and numerous system threads carry more overhead than user-level cooperative tasks. section 6.14 section 12.12
# Chapter 10/threaded_pizza_server.py
from socket import socket, create_server
from threading import Thread
BUFFER_SIZE = 1024
ADDRESS = ("127.0.0.1", 12345)
class Handler(Thread):
def __init__(self, conn: socket):
super().__init__()
self.conn = conn
def run(self) -> None:
print(f"Connected to {self.conn.getpeername()}")
try:
while True:
data = self.conn.recv(BUFFER_SIZE)
if not data:
break
try:
order = int(data.decode())
response = f"Thank you for ordering {order} pizzas!\n"
except ValueError:
response = "Wrong number of pizzas, please try again\n"
print(f"Sending message to {self.conn.getpeername()}")
self.conn.send(response.encode())
finally:
print(f"Connection with {self.conn.getpeername()} "
f"has been closed")
self.conn.close()
class Server:
def __init__(self) -> None:
try:
print(f"Starting up at: {ADDRESS}")
self.server_socket = create_server(ADDRESS)
except OSError:
self.server_socket.close()
print("\nServer stopped.")
def start(self) -> None:
print("Server listening for incoming connections")
try:
while True: #1
conn, address = self.server_socket.accept() #1
print(f"Client connection request from {address}") #1
thread = Handler(conn) #1
thread.start() #1
finally:
self.server_socket.close()
print("\nServer stopped.")
if __name__ == "__main__":
server = Server()
server.start()
The Python socket-server listing shows a separate thread handling each client while scheduling remains the operating system’s job. section 10.6




