# Some Concurrency Problems ## Simple parallel map Implement a parallel version of Python's built-in map function using the concurrent.futures.ThreadPoolExecutor class. It should take a function and an iterable as arguments and apply the function to each element in the iterable concurrently. For instance, you can imagine a `par_map(time.sleep, [1,2,3,4])` which executes in ~4 seconds ## Caching Write a simple caching system that stores the results of expensive operations using Python's threading.Lock class. Implement a function that performs an expensive operation (e.g., a slow API call) and caches the result for future use. Use a lock to prevent race conditions when multiple coroutines access the cache concurrently. ## Web Crawler Create a web crawler that finds all internal links on a website. Then make it concurrent. Here is a helper function: ```python import requests from bs4 import BeautifulSoup from typing import List from urllib.parse import urljoin def get_links(url:str) -> List[str]: """Get all the links on a page.""" page = requests.get(url) bs = BeautifulSoup(page.content, features='lxml') links = [link.get("href") for link in bs.findAll('a')] absolute_urls = [urljoin(url, link) for link in links] return absolute_urls # This is the website we'll crawl website_to_crawl = "https://andyljones.com" ``` There are a total of ~90 internal links. What interesting problem does this introduce when you try to make it concurrent? ## Rate-limited API Given a list of API endpoints, the caller should send requests concurrently while respecting a given rate limit (e.g., 5 requests per second). Note you can also use `time.sleep` to model the api. Here are some tests. We assume you have `rate_limited(f, xs, rate_per_second)` ```python= import datetime import time def f(x): time.sleep(x) return x a = datetime.datetime.now() rs = list(rate_limited(f, range(5), 3)) b = datetime.datetime.now() print(f'Should be 5: {(b-a).total_seconds()}') a = datetime.datetime.now() rs = list(rate_limited(f, [0]*5, 3)) b = datetime.datetime.now() print(f'Should be 1: {(b-a).total_seconds()}') a = datetime.datetime.now() rs = list(rate_limited(f, [0]*5, 8)) b = datetime.datetime.now() print(f'Should be 0: {(b-a).total_seconds()}') ```