CS343 - Concurrent and Parallel Programming
https://student.cs.uwaterloo.ca/~cs343/W23/
Notes: https://student.cs.uwaterloo.ca/~cs343/W23/notes.pdf
Apparently, we use uC++ in this course:
Concepts
Understand how multiple stacks operate with a single thread.
- You can have multiple stacks in a single thread
A2
- I actually don’t know why my program works on q1, it just magically worked when I tried
I think my Q1 is buggy? just why am I using a catch(…) in one of the cases, and catchresume in the other??
10 5 3 6 7 8 9 10
Notes
Only started taking notes seriously in the second part of the course. If I don’t take it seriously, I’m going to fail.
1. Advanced Control Flow
p.1
Multi-exit loop (mid-test loop)
This is when your loop has one or more exit locations occurring within the body, of the loop, not just top (while) or bottom (do-while).
- i.e. if you have a break statement
A loop exit NEVER needs an else clause. i.e.:
This is BAD
- Other way applies too (
if (C1) {break} else { S2}
; is BAD)
This is GOOD
- Remove S2 from the if statement
Why is it bad?
Because should be S2 logically part of the loop, not part of an IF. It’s about the scope of the statement.
- Though I feel like this is a personal preference
p.2
In theory, flag variables can achieve the same multi-exit functionality. However, very cumbersome
- Flag variables are the variable equivalent to a goto because they can be set/reset/tested at arbitrary locations in a program.
- disgusting code..
Clean version
Static muti-level exit
Static multi-level exit exits multiple control structures where exit point is known at compile time.
- This is the notion of being able to exit to various levels, not just to the level right above the current loop
p.4 Dynamic Memory Allocation Stack allocation eliminates explicit storage-management and is more efficient than heap allocation—“
Why is stack allocation faster than heap allocation?
Managing heap memory requires maintaing a data structure (like a free list) to keep track of used and free blocks.
- requires searching for a suitable block, which takes more time.
In contrast, stack memory simply follows a LIFO structure, and we know ahead of time how much memory is needed.
Some cases where heap alllocation is ABSOLUTE needed:
- When storage must outlive the block in which it is allocated (ownership change)
- When the amount of data read is unknown. (i.e C++ Vector)
- When an array of objects must be initialized via the object’s constructor and each element has a different value.
- When large local variables are allocated on a small stack.
For 3., look at this exmample
We are forced to use pointers and dynamic allocation.
HOWEVER, there is a way to achieve stack allocation in uC++ through the concept of a uArray
2. Nonlocal Transfer
One thing we’d like to achieve is non-local transfer.
- I guess isn’t multi-level exit that??
Consider this example
routine h
calls g
calls f
- cannot return from f to h, terminating g’s activation
2.3 Exception Handling
3. Coroutine
A coroutine is a routine that can also be suspended at some point and resumed from that point when control returns.
The state of a coroutine consists of:
- an execution location, starting at the beginning of the coroutine and remembered at each suspend.
- an execution state holding the data created by the code the coroutine is executing. ⇒ each coroutine has its own stack, containing its local variables and those of any routines it calls.
- an execution status—active or inactive or terminated—which changes as control resumes and suspends in a coroutine.
Difference between Routine and Coroutine?
A coroutine can suspend its execution and return to the caller without terminating, a routine cannot.
Cortouine
A coroutine is activated at the point of last suspension.
Routine
A routine always starts execution at the beginning and its local variables only persist for a single activation.
Coroutines are designed to be able to pass execution control back and forth between themselves.
- It looks something like this
I guess it looks something like preemption, and being able to resume that! So you context switch between routines and are able to get back!
A coroutine executes synchronously with other coroutines; hence, no concurrency among coroutines.
Why learn about coroutines?
Coroutines are the precursor to concurrent tasks, and introduce the complex concept of suspending and resuming on separate stacks.
coroutines.
There are 2 different approaches:
- Semi-Coroutine
- Full Coroutine
Semi-Coroutine
A semi-coroutine acts asymmetrically, like non-recursive routines, by implicitly reactivating the coroutine that previously activated it.
Full Coroutine
A full coroutine acts symmetrically, like recursive routines, by explicitly activating a member of another coroutine, which directly or indirectly reactivates the original coroutine (activation cycle).
3.1 Semi-Coroutine
Fibonacci Sequence
So the teacher’s motivation for this is first, let’s consider using global variables. But then that isn’t extendible super well to multiple instances.
- You want to encapsulate global variables into structure variables, and then pass these into each coroutine
Ahh i understand, so the state is stored.
resume()transfers to the last
suspend()`.
Example for fibonacci
Some notes:
- distinguished member
main
(coroutine main) can be suspended and resumed - no parameters or return value (supplied by public members and communication variables
The main program:
All coroutines are written like this for uC++
Warning
Warning, do not use
catch(...)
(catch any) in a coroutine, if it may be deleted before terminating, because a cleanup exception is raised to force stack unwinding (implementation issue).
p.30 Another example for reading characters
3.1.3 Correct Coroutine Usage
Danger
Eliminate computation or flag variables retaining information about execution state.
This is because the variables might change (?) No, because its bad
- In coroutine-based programming, you want to avoid such explicit logic and let the coroutine’s suspension and resumption naturally guide the flow of execution.
The purpose of coroutines is to simplify the flow, making it more linear and removing state-related logic.
- It’s a way to think about the problem
When possible, simplest coroutine construction is to write a direct (stand-alone) program
Convert to coroutine by:
- putting processing code into coroutine main,
- converting reads if program is consuming or writes if program is producing to suspend,
- Fibonacci consumes nothing and produces (generates) Fibonacci numbers ⇒ convert writes (cout) to suspends.
- Formatter consumes characters and only indirectly produces output (as side-effect) ⇒ convert reads (cin) to suspends.
- use interface members and communication variables to to transfer data in/out of coroutine
This approach is impossible for advanced coroutine problems.
3.2 EHM
Exception is the same as a class-object with respect to creation and destruction.
The tree traversal stuff
3.5 Raising
• There are two raising mechanisms: throwing and resuming.
_Throw [ exception-type ] ;
_Resume [ exception-type ] [ _At uBaseCoroutine-id ] ;
• If _Throw
has no exception-type, it is a rethrow.
• If _Resume
has no exception-type, it is a reresume.
Resumption allows faulting execution greatest flexibility: it can process the exception as a resumption or rethrow the exception for termination.
3.6 Handler
uC++ has 2 kinds of handlers:
- Termination
- Resumption
For termination, uC++ uses try and catch, same as C++.
For Resumption, µC++ extends the try block to include resumption handlers.
• Resumption handler is denoted by a _CatchResume
clause after try
body:
• Any number of resumption handlers can be associated with a try block.
Tip
- All
_CatchResume
handlers must precede any catch handlers, solely to make reading the two sets of clauses easier.
- Like
catch(...)
(catch-any),_CatchResume(...)
must appear at the end of the list of the resumption handlers.
p.38 Resumption handler cannot perform a break, continue, goto, or return.
This example program makes things make more sense for how _CatchResume
works
output
before throw
catch
after try1
before resume
_CatchResume
after resume
after try2
There is a defaultResume
(like if there is no resumption handler, the default behavior is to try and find a handler to catch it (generic catch)).
The raise dictates set of handlers examined during propagation:
â—¦ terminating propagation (_Throw
) only examines termination handlers (catch
),
â—¦ resuming propagation (_Resume
) only examines resumption handlers (_CatchResume
).
I'm confused about the interactions?
Like not sure if this is totally true. LIke i can
_Throw
, and have it handled by a catchResume??
- Like in my A2, I threw using
_Throw
- Initially, I tried to handle it with a CatchResume, however that didn’t work. Or also trying with
Resume_
, but that didn’t work too. I still am not 100% sure about the mechanism#gap-in-knowledge
_CatchResume(Sentinel & e) {
// Node without value
_Throw Sentinel();
}
3.7 Nonlocal Exceptions
For nonlocal exceptions, you do something more advanced?
That’s where you do _Resume Exception() at C
.
- That syntax allows you to trigger resumption somewhere else
For nonlocal resumption, _Resume
is a proxy for actual raise in the faulting coroutine ⇒
non-local resumption becomes local resumption.
While source delivers nonlocal exception immediately, propagation only occurs when faulting becomes active.
- i.e. you must resume back to the coroutine for it to react properly (through calling their
resume()
in some way)
- Multiple nonlocal exceptions are queued and delivered in FIFO order depending on the current enabled exceptions.
I'm confused on non-local exceptions and resumption
So I understand that when you call
_Resume
, that is essentially triggering an exception. After the exception is handled, we will resume here.I also remember that
_CatchResume
only handles_Resume
_Catch
only handles_Throw
So after a
_Resume
is handled, the control will come back right?
- And what happens the next time that coroutine is resumed, because we just jumped back? Does it just continue down?
- And in these cases, is using catchresume correct? don’t you just want to throw at? No, because you don’t want termination
I guess you can ask, resume()
vs. _Resume
?
- it makes sense resume() goes with suspend()
- But do you need that for
_Resume
Now, if there are
p.49
If a coroutine throws an exception but does not handle it, the system will raise a special exception called uBaseCoroutine::UnhandledException
.
When an unhandled exception occurs:
- The exception is propagated to the last resumer of the coroutine, not its original starter.
- The coroutine terminates after this unhandled exception is raised
Take a look at this example
- There are 2 ways to handle an unhandled exception:
-
What it does: The CatchResume handler catches the unhandled exception and allows the program to continue execution from the point of resumption—that is, after the coroutine has been resumed.
3.10 Full Coroutines
- Semi-coroutine activates the member routine that activated it.
- Full coroutine has a resume cycle; semi-coroutine does not form a resume cycle
I still don’t really understand the difference in full coroutine.
Ahh I think i get it
It's about the resume()
When a semi-coroutine calls a
resume()
, it will resume itself inmain()
the last point of suspension (lastsuspend()
).However, for a full-coroutine, we can have the case where we call
resume()
and have another coroutine resumed.
3.11 Coroutine Languages
Coroutine implementations have two forms:
- stackless: use the caller’s stack and a fixed-sized local-state
- stackful: separate stack and a fixed-sized (class) local-state