diff --git a/homework/doc/arc.md b/homework/doc/arc.md index dc1abdc627..6fc6e104a8 100644 --- a/homework/doc/arc.md +++ b/homework/doc/arc.md @@ -12,8 +12,8 @@ The skeleton code is a heavily modified version of `Arc` from the standard libra We don't recommend reading the original source code before finishing this homework because that version is more complex. -## ***2023 fall semester notice: Use `SeqCst`*** -Due to lack of time, we cannot cover the weak memory semantics. +## ***2024 spring semester notice: Use `SeqCst`*** +We won't cover the weak memory semantics in this semester. So you may ignore the instructions on `Ordering` stuff below and use `Ordering::SeqCst` for `ordering: Ordering` parameters for `std::sync::atomic` functions. diff --git a/homework/doc/hash_table.md b/homework/doc/hash_table.md index 623e634d2a..9cb5f60fe9 100644 --- a/homework/doc/hash_table.md +++ b/homework/doc/hash_table.md @@ -10,7 +10,7 @@ This homework is in 2 parts: optimize the implementation by relaxing the ordering on the atomic accesses. We recommend working on this part after finishing the [Arc homework](./arc.md). -## ***2023 fall semester notice: Part 2 is cancelled*** +## ***2024 spring semester notice: Part 2 is cancelled*** We won't cover the weak memory semantics in this semester. ## Part 1: Split-ordered list in sequentially consistent memory model diff --git a/homework/doc/hazard_pointer.md b/homework/doc/hazard_pointer.md index 5ed2713a9c..f9ac453285 100644 --- a/homework/doc/hazard_pointer.md +++ b/homework/doc/hazard_pointer.md @@ -15,7 +15,7 @@ This homework is in 2 parts: optimize the implementation by relaxing the ordering. We recommend working on this part after finishing the [Arc homework](./arc.md). -## ***2023 fall semester notice: Part 2 is cancelled*** +## ***2024 spring semester notice: Part 2 is cancelled*** We won't cover the weak memory semantics in this semester. To ensure that the grader works properly, you must use `Ordering:SeqCst` for all operations. diff --git a/homework/doc/list_set.md b/homework/doc/list_set.md index 3fc88cf784..8ed4b1d6bd 100644 --- a/homework/doc/list_set.md +++ b/homework/doc/list_set.md @@ -1,55 +1,30 @@ # Concurrent set based on Lock-coupling linked list -**Implement concurrent set data structures with sorted singly linked list using (optimistic) fine-grained lock-coupling.** +**Implement concurrent set data structures with sorted singly linked list using fine-grained lock-coupling.** Suppose you want a set data structure that supports concurrent operations. The simplest possible approach would be taking a non-concurrent set implementation and protecting it with a global lock. However, this is not a great idea if the set is accessed frequently because a thread's operation blocks all the other threads' operations. -In this homework, you will write two implementations of the set data structure based on singly linked list protected by fine-grained locks. +In this homework, you will write an implementation of the set data structure based on singly linked list protected by fine-grained locks. * The nodes in the list are sorted by their value, so that one can efficiently check if a value is in the set. * Each node has its own lock that protects its `next` field. When traversing the list, the locks are acquired and released in the hand-over-hand manner. This allows multiple operations run more concurrently. -You will implement two variants. -* In `list_set/fine_grained.rs`, the lock is the usual `Mutex`. -* In `list_set/optimistic_fine_grained.rs`, the lock is a `SeqLock`. - This allows read operations to run optimistically without actually locking. - Therefore, read operations are more efficient in read-most scenario, and - they do not block other operations. - However, more care must be taken to ensure correctness. - * You need to validate read operations and handle the failure. - * Do not use `ReadGuard::restart()`. - Using this correctly requires some extra synchronization - (to be covered in lock-free list lecture), - which makes `SeqLock` somewhat pointless. - The tests assume that `ReadGuard::restart()` is not used. - * Since each node can be read and modified to concurrently, - you should use atomic operations to avoid data races. - Specifically, you will use `crossbeam_epoch`'s `Atomic<T>` type - (instead of `std::sync::AtomicPtr<T>`, due to the next issue). - For `Ordering`, use `SeqCst` everywhere. - (In the later part of this course, you will learn that `Relaxed` is sufficient. - But don't use `Relaxed` in this homework, because that would break `cargo_tsan`.) - * Since a node can be removed while another thread is reading, - reclamation of the node should be deferred. - You can handle this semi-automatically with `crossbeam_epoch`. - -Fill in the `todo!()`s in `list_set/{fine_grained,optimistic_fine_grained}.rs` (about 40 + 80 lines of code). +Fill in the `todo!()`s in `list_set/fine_grained.rs` (about 40 lines of code). As in the [Linked List homework](./linked_list.md), you will need to use some unsafe operations. ## Testing -Tests are defined in `tests/list_set/{fine_grained,optimistic_fine_grained}.rs`. +Tests are defined in `tests/list_set/fine_grained.rs`. Some of them use the common set test functions defined in `src/test/adt/set.rs`. -## Grading (100 points) +## Grading (45 points) Run ``` ./scripts/grade-list_set.sh ``` -For each module `fine_grained` and `optimistic_fine_grained`, -the grader runs the tests +The grader runs the tests with `cargo`, `cargo_asan`, and `cargo_tsan` in the following order. 1. `stress_sequential` (5 points) 1. `stress_concurrent` (10 points) @@ -58,12 +33,6 @@ with `cargo`, `cargo_asan`, and `cargo_tsan` in the following order. For the above tests, if a test fails in a module, then the later tests in the same module will not be run. -For `optimistic_fine_grained`, the grader additionally runs the following tests -(10 points if all of them passes, otherwise 0). -* `read_no_block` -* `iter_invalidate_end` -* `iter_invalidate_deleted` - ## Submission ```sh cd cs431/homework @@ -72,3 +41,44 @@ ls ./target/hw-list_set.zip ``` Submit `hw-list_set.zip` to gg. + +## Advanced (optional) +**Note**: This is an *optional* homework, meaning that it will not be graded and not be asked in the exam. + +Consider a variant of the homework that uses `SeqLock` instead of `Mutex`. +This allows read operations to run optimistically without actually locking. +Therefore, read operations are more efficient in read-most scenario, and +they do not block other operations. +However, more care must be taken to ensure correctness. + * You need to validate read operations and handle the failure. + * Do not use `ReadGuard::restart()`. + Using this correctly requires some extra synchronization + (to be covered in lock-free list lecture), + which makes `SeqLock` somewhat pointless. + The tests assume that `ReadGuard::restart()` is not used. + * Since each node can be read and modified to concurrently, + you should use atomic operations to avoid data races. + Specifically, you will use `crossbeam_epoch`'s `Atomic<T>` type + (instead of `std::sync::AtomicPtr<T>`, due to the next issue). + For `Ordering`, use `SeqCst` everywhere. + (In the later part of this course, you will learn that `Relaxed` is sufficient. + But don't use `Relaxed` in this homework, because that would break `cargo_tsan`.) + * Since a node can be removed while another thread is reading, + reclamation of the node should be deferred. + You can handle this semi-automatically with `crossbeam_epoch`. + +**Instruction**: Fill in the `todo!()`s in `list_set/optimistic_fine_grained.rs` (about 80 lines of code). + +**Testing**: Tests are defined in `tests/list_set/optimistic_fine_grained.rs`. + +**Self grading**: +Run +``` +./scripts/grade-optimistic_list_set.sh +``` + +Unlike the main homework, the grader additionally runs the following tests +(10 points if all of them passes, otherwise 0). +* `read_no_block` +* `iter_invalidate_end` +* `iter_invalidate_deleted` diff --git a/homework/scripts/grade-list_set.sh b/homework/scripts/grade-list_set.sh index 0ff29142c1..a07df24f97 100755 --- a/homework/scripts/grade-list_set.sh +++ b/homework/scripts/grade-list_set.sh @@ -34,8 +34,6 @@ RUNNER_TIMEOUTS=( ) # the index of the last failed test fine_grained_fail=${#COMMON_TESTS[@]} -optimistic_fine_grained_fail=${#COMMON_TESTS[@]} -others_failed=false for r in "${!RUNNERS[@]}"; do RUNNER=${RUNNERS[r]} @@ -53,35 +51,10 @@ for r in "${!RUNNERS[@]}"; do fi done fi - if [ $t -lt $optimistic_fine_grained_fail ]; then - echo "Testing optimistic_fine_grained $TEST_NAME with $RUNNER, timeout $TIMEOUT..." - TESTS=("--test list_set -- --exact optimistic_fine_grained::$TEST_NAME") - for ((i = 0; i < REPS; i++)); do - if [ $(run_tests) -ne 0 ]; then - optimistic_fine_grained_fail=$t - break - fi - done - fi done - - if [ "$others_failed" == false ]; then - echo "Running additional tests for optimistic_fine_grained with $RUNNER, timeout $TIMEOUT..." - TESTS=( - "--test list_set -- --exact optimistic_fine_grained::read_no_block" - "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_end" - "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_deleted" - ) - if [ $(run_tests) -ne 0 ]; then - others_failed=true - fi - fi done SCORES=( 0 5 15 30 45 ) -SCORE=$(( SCORES[fine_grained_fail] + SCORES[optimistic_fine_grained_fail] )) -if [ "$others_failed" == false ]; then - SCORE=$(( SCORE + 10 )) -fi +SCORE=$(( SCORES[fine_grained_fail] )) -echo "Score: $SCORE / 100" +echo "Score: $SCORE / 45" diff --git a/homework/scripts/grade-optimistic_list_set.sh b/homework/scripts/grade-optimistic_list_set.sh new file mode 100755 index 0000000000..7f58aef126 --- /dev/null +++ b/homework/scripts/grade-optimistic_list_set.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# set -e +set -uo pipefail +IFS=$'\n\t' + +# Imports library. +BASEDIR=$(dirname "$0") +source $BASEDIR/grade-utils.sh + +run_linters || exit 1 + +export RUST_TEST_THREADS=1 + + +REPS=3 +COMMON_TESTS=( + "stress_sequential" + "stress_concurrent" + "log_concurrent" + "iter_consistent" +) +RUNNERS=( + "cargo --release" + "cargo_asan" + "cargo_asan --release" + "cargo_tsan --release" +) +# timeout for each RUNNER +RUNNER_TIMEOUTS=( + 30s + 180s + 180s + 180s +) +# the index of the last failed test +optimistic_fine_grained_fail=${#COMMON_TESTS[@]} +others_failed=false + +for r in "${!RUNNERS[@]}"; do + RUNNER=${RUNNERS[r]} + TIMEOUT=${RUNNER_TIMEOUTS[r]} + for t in "${!COMMON_TESTS[@]}"; do + TEST_NAME=${COMMON_TESTS[t]} + # run only if no test has failed yet + if [ $t -lt $optimistic_fine_grained_fail ]; then + echo "Testing optimistic_fine_grained $TEST_NAME with $RUNNER, timeout $TIMEOUT..." + TESTS=("--test list_set -- --exact optimistic_fine_grained::$TEST_NAME") + for ((i = 0; i < REPS; i++)); do + if [ $(run_tests) -ne 0 ]; then + optimistic_fine_grained_fail=$t + break + fi + done + fi + done + + if [ "$others_failed" == false ]; then + echo "Running additional tests for optimistic_fine_grained with $RUNNER, timeout $TIMEOUT..." + TESTS=( + "--test list_set -- --exact optimistic_fine_grained::read_no_block" + "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_end" + "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_deleted" + ) + if [ $(run_tests) -ne 0 ]; then + others_failed=true + fi + fi +done + +SCORES=( 0 5 15 30 45 ) +SCORE=$(( SCORES[optimistic_fine_grained_fail] )) +if [ "$others_failed" == false ]; then + SCORE=$(( SCORE + 10 )) +fi + +echo "Score: $SCORE / 55" diff --git a/homework/src/hash_table/growable_array.rs b/homework/src/hash_table/growable_array.rs index de04310130..473dd4e3e9 100644 --- a/homework/src/hash_table/growable_array.rs +++ b/homework/src/hash_table/growable_array.rs @@ -2,7 +2,6 @@ use core::fmt::Debug; use core::mem::{self, ManuallyDrop}; -use core::ops::{Deref, DerefMut}; use core::sync::atomic::Ordering::*; use crossbeam_epoch::{Atomic, Guard, Owned, Shared}; diff --git a/homework/src/hazard_pointer/hazard.rs b/homework/src/hazard_pointer/hazard.rs index 4f6df1354b..aea350220d 100644 --- a/homework/src/hazard_pointer/hazard.rs +++ b/homework/src/hazard_pointer/hazard.rs @@ -146,6 +146,12 @@ impl HazardBag { } } +impl Default for HazardBag { + fn default() -> Self { + Self::new() + } +} + impl Drop for HazardBag { /// Frees all slots. fn drop(&mut self) { diff --git a/homework/src/linked_list.rs b/homework/src/linked_list.rs index 39adbaff64..b6b9dd7a1a 100644 --- a/homework/src/linked_list.rs +++ b/homework/src/linked_list.rs @@ -1,6 +1,5 @@ use std::cmp::Ordering; use std::fmt; -use std::iter::FromIterator; use std::marker::PhantomData; use std::mem; use std::ptr; diff --git a/homework/tests/growable_array.rs b/homework/tests/growable_array.rs index e1f36c8161..2c01c5a9c9 100644 --- a/homework/tests/growable_array.rs +++ b/homework/tests/growable_array.rs @@ -79,12 +79,12 @@ mod stack { use crossbeam_epoch::{Atomic, Guard, Owned, Shared}; #[derive(Debug)] - pub(crate) struct Stack<T> { + pub(super) struct Stack<T> { head: Atomic<Node<T>>, } impl<T> Stack<T> { - pub(crate) fn new() -> Self { + pub(super) fn new() -> Self { Self { head: Atomic::null(), } @@ -92,20 +92,20 @@ mod stack { } #[derive(Debug)] - pub(crate) struct Node<T> { + pub(super) struct Node<T> { data: T, next: UnsafeCell<*const Node<T>>, } impl<T> Node<T> { - pub(crate) fn new(data: T) -> Self { + pub(super) fn new(data: T) -> Self { Self { data, next: UnsafeCell::new(ptr::null()), } } - pub(crate) fn into_inner(self) -> T { + pub(super) fn into_inner(self) -> T { self.data } } @@ -130,7 +130,7 @@ mod stack { /// /// - A single `n` should only be pushed into the stack once. /// - After the push, `n` should not be used again. - pub(crate) unsafe fn push_node<'g>(&self, n: Shared<'g, Node<T>>, guard: &'g Guard) { + pub(super) unsafe fn push_node<'g>(&self, n: Shared<'g, Node<T>>, guard: &'g Guard) { let mut head = self.head.load(Relaxed, guard); loop { unsafe { *n.deref().next.get() = head.as_raw() };