Blocking vs Non-blocking

Migo·2024년 10월 22일

CS

목록 보기
2/7

Sync - Async

In programming, discussions about synchronization typically involve at least two interacting objects.

Sync means, in that context, two objects are tightly intertwined that one task(A) is dependent on the other(B).

If A can execute its own task without a constraint, A and B is async.

Blocking and Non-blocking

When function A invokes function B, if the operating system pauses the thread or process running A during the call to B, this indicates a blocking operation. Otherwise, the operation is non-blocking.

Likewise, blocking call means to pause thread/process - that doesn't mean that every function call pauses thread execution as follows:

fn some_work(a:i32,b:i32) -> i32{
	a+b
}

fn main(){
	let c = some_work(1,1);
}

The thread running main doesn't get blocked just because it invokes some_work.

But then, in what situation a thread/process get blocked by operating system?

The core issue of blocking : I/O

Most blocking is related to I/O because CPU processing is much faster than that of other devices, such as disks, keyboards, and networks. Therefore, when a program, thread, or process encounters an I/O task, it yields CPU time back to the operating system, which then assigns it to a different thread, as follows:

From the CPU's perspective, it never wastes time; it continues working on a different thread, polling jobs from the waiting queue. However, as programmers, we care about how quickly a program handles its tasks. In this case, frequent blocking can slow things down. Is there a way to handle I/O tasks without pausing the program? Yes, with non-blocking I/O calls.

Non-blocking asynchronous I/O

Take network data transfer as an example. If the data receiving function recv is non-blocking, it means that instead of yielding control, the OS returns from recv immediately. The invoking thread continues its work, while data reception is handled by the kernel:

But how can your program know when the data has been received? Three mechanisms are commonly suggested:

  • Use a notification system that triggers an interrupt.
  • Pass a callback function to the recv function (if recv supports callbacks).
  • Pass a result-checking function alongside the recv call.

Let's take a look at Rust example for non-blocking asynchoronous call:

use tokio::{net::TcpListener, io::AsyncReadExt};

async fn handler(data: &[u8]) {
    // process network data
    // ...
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;
        
        // Spawn a new task for each connection
        tokio::spawn(async move {
            let mut buf = vec![0; 1024];
            
            // Non-blocking read using async/await
            match socket.read(&mut buf).await {
                Ok(n) if n > 0 => {
                    handler(&buf[..n]).await;
                }
                _ => {
                    // Handle error or connection closed
                }
            }
        });
    }
}
profile
Dude with existential crisis

0개의 댓글