同步和异步

介绍

同步

概念

在计算机领域,同步是指在执行某个操作时,程序会等待该操作完成后才继续执行后续的代码。也就是说,操作是按照顺序依次进行的,一个操作完成之后才会开始下一个操作。在同步操作过程中,程序处于阻塞状态,无法进行其他任务,直到当前操作结束。

应用场景

  • 数据一致性要求高:在一些对数据一致性要求非常严格的场景中,同步操作是必要的。例如,在银行转账系统中,必须确保一笔转账操作完全完成后,才能进行下一笔操作,以保证账户余额的准确性和数据的一致性。
  • 简单的顺序任务:对于一些简单的、不需要并发处理的顺序任务,同步操作可以使代码逻辑更加清晰易懂。比如,一个程序需要依次读取文件的不同部分并进行处理,使用同步操作可以按顺序完成这些任务。

优缺点

  • 优点
    • 逻辑简单:同步操作的代码逻辑清晰,易于理解和调试。因为操作是按顺序执行的,程序员可以很容易地预测程序的执行流程。
    • 数据一致性好:由于操作是顺序执行的,不会出现多个操作同时修改数据导致的数据不一致问题。
  • 缺点
    • 性能瓶颈:在进行 I/O 密集型任务时,同步操作会导致程序长时间阻塞,浪费 CPU 资源。例如,当程序发起一个磁盘读写请求时,CPU 会等待该请求完成,在此期间无法执行其他任务。
    • 并发处理能力差:同步操作难以处理大量的并发请求。如果有多个请求同时到达,只能依次处理,会导致响应时间变长,系统吞吐量降低。

异步

概念

异步是指在执行某个操作时,程序不会等待该操作完成,而是继续执行后续的代码。当操作完成后,会通过某种机制(如回调函数、事件通知等)通知程序。在异步操作过程中,程序不会被阻塞,可以继续处理其他任务,从而提高了系统的并发性能和资源利用率。

应用场景

  • I/O 密集型任务:对于需要大量等待外部资源的 I/O 密集型任务,异步操作可以显著提高性能。例如,在网络编程中,当客户端向服务器发送请求后,使用异步操作可以在等待服务器响应的同时处理其他任务,而不需要一直阻塞。
  • 高并发场景:在需要处理大量并发请求的场景中,异步操作可以更好地应对。例如,一个 Web 服务器需要同时处理多个客户端的请求,如果使用异步操作,服务器可以同时处理多个请求,提高响应速度和系统吞吐量。

优缺点

  • 优点
    • 提高性能:异步操作可以充分利用 CPU 资源,避免了在 I/O 操作时的阻塞等待,从而提高了系统的整体性能。
    • 高并发处理能力:异步操作可以同时处理多个任务,能够更好地应对高并发场景,提高系统的响应速度和吞吐量。
  • 缺点
    • 编程复杂度高:异步操作的代码逻辑相对复杂,需要使用回调函数、事件驱动等机制来处理操作结果,增加了编程的难度和代码的复杂度。
    • 调试困难:由于异步操作的执行顺序不是固定的,调试时很难跟踪程序的执行流程,增加了调试的难度。
    • 额外开销:异步操作会引入一些额外的开销,如数据结构和上下文管理开销、回调函数管理开销、错误处理和状态管理开销等。

对比

同步操作速度优势情况

  • 操作简单且数据量小:当操作非常简单,涉及的数据量较小,并且不需要等待外部资源(如磁盘 I/O、网络响应)时,同步操作可能更快。因为同步操作的流程较为直接,没有额外的异步调度开销。例如,在一个简单的程序中对本地内存中的小数据块进行读写操作,同步操作可以立即完成,无需进行复杂的异步任务管理。
  • 资源竞争少:在资源竞争较少的环境中,同步操作可以按照顺序依次执行,不会出现因为异步操作的并发执行而导致的资源冲突和协调开销。比如在一个单线程且资源充足的系统中,同步操作可以高效地利用资源,快速完成任务。

异步操作速度优势情况

  • I/O 密集型任务:对于需要大量等待外部资源的 I/O 密集型任务,异步操作通常更快。在同步 I/O 中,当发起一个 I/O 请求时,程序会被阻塞,直到该请求完成,这段时间内 CPU 处于空闲状态。而在异步 I/O 中,程序在发起 I/O 请求后可以继续执行其他任务,当 I/O 操作完成时会收到通知。例如,在进行大量的磁盘读写或网络数据传输时,异步操作可以充分利用 CPU 时间,提高整体效率。
  • 高并发场景:在高并发场景下,异步操作能够更好地处理多个并发请求。每个异步任务可以独立执行,不会相互阻塞,从而提高系统的吞吐量。例如,一个 Web 服务器需要同时处理大量的客户端请求,如果使用同步操作,服务器可能会因为某个请求的处理时间过长而阻塞其他请求;而使用异步操作,服务器可以同时处理多个请求,提高响应速度。

异步调度相比同步额外开销

1. 数据结构和上下文管理开销

  • 异步控制块(AIOCB) :在使用异步 I/O 时,通常需要为每个异步操作创建一个异步控制块(如 POSIX AIO 中的 struct aiocb)。这个结构体用于存储异步操作的相关信息,如文件描述符、缓冲区指针、偏移量等。每个异步操作都需要一个这样的结构体,当有大量异步操作时,会占用一定的内存空间。而且,操作系统需要对这些结构体进行管理和维护,增加了系统的内存管理开销。
  • 上下文切换:异步操作通常涉及多线程或多进程的使用。当异步操作完成时,可能需要进行上下文切换来执行回调函数。上下文切换需要保存和恢复寄存器、程序计数器等信息,这会消耗 CPU 时间,尤其是在频繁进行异步操作时,上下文切换的开销会更加明显。

2. 回调函数管理开销

  • 回调函数注册和调用:异步操作通常依赖回调函数来处理操作结果。在发起异步操作时,需要将回调函数注册到系统中。当操作完成时,系统会调用相应的回调函数。这个过程涉及到函数指针的管理和调用,需要一定的开销。而且,如果回调函数比较复杂,执行回调函数本身也会消耗 CPU 时间。
  • 回调函数的并发控制:在多线程环境下,多个异步操作可能同时完成并触发回调函数。为了保证回调函数的正确执行,可能需要进行并发控制,如使用锁机制。锁的使用会引入额外的开销,并且可能导致线程阻塞,降低系统性能。

3. 错误处理和状态管理开销

  • 异步操作状态跟踪:由于异步操作是非阻塞的,程序需要不断跟踪异步操作的状态。通常使用函数(如 POSIX AIO 中的 aio_error)来检查操作是否完成或是否出错。这个过程需要频繁地访问系统状态,增加了系统调用的开销。
  • 错误处理复杂性:异步操作的错误处理比同步操作更加复杂。在同步操作中,操作完成后可以立即检查返回值来判断是否出错;而在异步操作中,需要在回调函数中进行错误处理,并且可能需要处理多个异步操作的错误情况。错误处理的复杂性增加了代码的复杂度和执行开销。

4. 线程或进程创建和管理开销

  • 线程或进程创建:在实现异步操作时,可能需要创建额外的线程或进程来处理异步任务。线程或进程的创建需要分配内存、初始化堆栈等,这会消耗一定的系统资源。而且,线程或进程的创建和销毁操作本身也会带来开销。
  • 线程或进程调度:多个线程或进程之间需要进行调度,以确保它们能够合理地使用 CPU 资源。操作系统的调度算法需要考虑线程或进程的优先级、状态等因素,这会增加系统的调度开销。

 实例

这是一个在Linux系统下nve读写操作的实验,我们来看一下异步和同步程序的区别在哪

同步

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

#define BUFFER_SIZE 1024 * 1024 * 1000  // 1000MB 缓冲区大小
#define FILENAME "sync_test_file"

int main() {
    int fd;
    char *buffer;
    ssize_t bytes_written, bytes_read;
    clock_t start_time, end_time;
    double elapsed_time;
    double write_speed, read_speed;

    // 分配缓冲区
    buffer = (char *)malloc(BUFFER_SIZE);
    if (buffer == NULL) {
        perror("内存分配失败");
        return 1;
    }
    memset(buffer, 'A', BUFFER_SIZE);

    // 打开文件用于写入
    fd = open(FILENAME, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd == -1) {
        perror("打开文件用于写入失败");
        free(buffer);
        return 1;
    }

    // 记录写操作开始时间
    start_time = clock();

    // 进行写操作
    bytes_written = write(fd, buffer, BUFFER_SIZE);
    if (bytes_written == -1) {
        perror("写操作失败");
        close(fd);
        free(buffer);
        return 1;
    }

    // 记录写操作结束时间
    end_time = clock();

    // 计算写操作用时
    elapsed_time = ((double)(end_time - start_time)) / CLOCKS_PER_SEC;

    // 计算写速度(MB/s)
    write_speed = (double)bytes_written / elapsed_time / (1024 * 1024);

    // 输出写操作信息
    printf("同步写操作完成,传输了 %zd 字节,用时 %.6f 秒,速度 %.2f MB/s\n", bytes_written, elapsed_time, write_speed);

    // 关闭文件
    close(fd);

    // 打开文件用于读取
    fd = open(FILENAME, O_RDONLY);
    if (fd == -1) {
        perror("打开文件用于读取失败");
        free(buffer);
        return 1;
    }

    // 记录读操作开始时间
    start_time = clock();

    // 进行读操作
    bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read == -1) {
        perror("读操作失败");
        close(fd);
        free(buffer);
        return 1;
    }

    // 记录读操作结束时间
    end_time = clock();

    // 计算读操作用时
    elapsed_time = ((double)(end_time - start_time)) / CLOCKS_PER_SEC;

    // 计算读速度(MB/s)
    read_speed = (double)bytes_read / elapsed_time / (1024 * 1024);

    // 输出读操作信息
    printf("同步读操作完成,传输了 %zd 字节,用时 %.6f 秒,速度 %.2f MB/s\n", bytes_read, elapsed_time, read_speed);

    // 关闭文件
    close(fd);

    // 释放缓冲区
    free(buffer);

    return 0;
}

异步

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

#define BUFFER_SIZE 1024 * 1024 * 1000  // 1000MB 缓冲区大小
#define FILENAME "sync_test_file"

int main() {
    int fd;
    char *buffer;
    ssize_t bytes_written, bytes_read;
    clock_t start_time, end_time;
    double elapsed_time;
    double write_speed, read_speed;

    // 分配缓冲区
    buffer = (char *)malloc(BUFFER_SIZE);
    if (buffer == NULL) {
        perror("内存分配失败");
        return 1;
    }
    memset(buffer, 'A', BUFFER_SIZE);

    // 打开文件用于写入
    fd = open(FILENAME, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd == -1) {
        perror("打开文件用于写入失败");
        free(buffer);
        return 1;
    }

    // 记录写操作开始时间
    start_time = clock();

    // 进行写操作
    bytes_written = write(fd, buffer, BUFFER_SIZE);
    if (bytes_written == -1) {
        perror("写操作失败");
        close(fd);
        free(buffer);
        return 1;
    }

    // 记录写操作结束时间
    end_time = clock();

    // 计算写操作用时
    elapsed_time = ((double)(end_time - start_time)) / CLOCKS_PER_SEC;

    // 计算写速度(MB/s)
    write_speed = (double)bytes_written / elapsed_time / (1024 * 1024);

    // 输出写操作信息
    printf("同步写操作完成,传输了 %zd 字节,用时 %.6f 秒,速度 %.2f MB/s\n", bytes_written, elapsed_time, write_speed);

    // 关闭文件
    close(fd);

    // 打开文件用于读取
    fd = open(FILENAME, O_RDONLY);
    if (fd == -1) {
        perror("打开文件用于读取失败");
        free(buffer);
        return 1;
    }

    // 记录读操作开始时间
    start_time = clock();

    // 进行读操作
    bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read == -1) {
        perror("读操作失败");
        close(fd);
        free(buffer);
        return 1;
    }

    // 记录读操作结束时间
    end_time = clock();

    // 计算读操作用时
    elapsed_time = ((double)(end_time - start_time)) / CLOCKS_PER_SEC;

    // 计算读速度(MB/s)
    read_speed = (double)bytes_read / elapsed_time / (1024 * 1024);

    // 输出读操作信息
    printf("同步读操作完成,传输了 %zd 字节,用时 %.6f 秒,速度 %.2f MB/s\n", bytes_read, elapsed_time, read_speed);

    // 关闭文件
    close(fd);

    // 释放缓冲区
    free(buffer);

    return 0;
}

结果对比

我们编译两个程序然后运行看一下结果

我们可以看出在大数据传输情况下,异步操作好像更快一些。

程序区别分析

同步和异步操作在文件读写中的主要区别在于程序在进行 I/O 操作时的行为模式,下面来详细分析这两个程序的差异以及区分同步和异步的关键部分。

整体区别概述
同步操作:在同步文件读写中,当程序发起一个 I/O 请求(如 `read` 或 `write`)时,程序会被阻塞,直到该请求完成。也就是说,在 I/O 操作进行期间,程序无法执行其他任务,只能等待操作结束。
异步操作:而异步文件读写允许程序在发起 I/O 请求后继续执行其他任务,无需等待 I/O 操作完成。当 I/O 操作完成时,程序会通过某种机制(如回调函数)得到通知。

具体代码差异及关键区分部分1. 读写操作的发起和执行方式
同步程序

// 同步写操作
start_time = clock();
bytes_written = write(fd, buffer, BUFFER_SIZE);
end_time = clock();

// 同步读操作
start_time = clock();
bytes_read = read(fd, buffer, BUFFER_SIZE);
end_time = clock();

ps:write函数结构体

ssize_t write(int fd, const void *buf, size_t count);

fd :这是一个整数类型的文件描述符

buf :这是一个指向要写入数据的缓冲区的指针。

count :这是一个 size_t 类型的参数,表示要写入的字节数。


在同步程序中,`write` 和 `read` 函数是阻塞调用。当调用 `write` 函数时,程序会暂停执行,直到数据全部写入文件;调用 `read` 函数时,程序会等待直到从文件中读取到指定数量的数据。在这个过程中,CPU 处于等待状态,无法处理其他任务。

异步程序

// 异步写操作
write_start_time = clock();
if (aio_write(&aiocb) == -1) {
    perror("发起异步写操作失败");
    free(buffer);
    close(fd);
    return 1;
}

// 异步读操作
read_start_time = clock();
if (aio_read(&aiocb) == -1) {
    perror("发起异步读操作失败");
    free(buffer);
    close(fd);
    return 1;
}


在异步程序中,`aio_write` 和 `aio_read` 函数是非阻塞调用。当调用这些函数时,程序会立即返回,不会等待 I/O 操作完成。程序可以继续执行后续的代码,而 I/O 操作会在后台进行。2. 操作完成的通知和等待机制
同步程序
同步程序不需要额外的通知机制,因为 `write` 和 `read` 函数返回时,操作已经完成。程序通过检查返回值来判断操作是否成功。

if (bytes_written == -1) {
    perror("写操作失败");
    close(fd);
    free(buffer);
    return 1;
}

if (bytes_read == -1) {
    perror("读操作失败");
    close(fd);
    free(buffer);
    return 1;
}

异步程序
异步程序使用 `aio_error` 函数来检查 I/O 操作的状态,并通过循环等待操作完成。当操作完成时,会调用回调函数 `aio_complete_handler` 进行处理。

// 等待写操作完成
int max_attempts = 10000;
int attempts = 0;
while (aio_error(&aiocb) == EINPROGRESS && attempts < max_attempts) {
    usleep(1000);
    attempts++;
}

// 等待读操作完成
attempts = 0;
while (aio_error(&aiocb) == EINPROGRESS && attempts < max_attempts) {
    usleep(1000);
    attempts++;
}

同时,在初始化 `aiocb` 结构体时,设置了回调函数:

aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
aiocb.aio_sigevent.sigev_notify_function = aio_complete_handler;


当 I/O 操作完成时,会启动一个新线程调用 `aio_complete_handler` 函数,程序可以在这个函数中处理操作结果。

总结
区分同步和异步的关键在于 I/O 操作的发起和执行方式以及操作完成的通知和等待机制。同步操作是阻塞的,程序需要等待操作完成;而异步操作是非阻塞的,程序可以继续执行其他任务,通过回调或轮询来处理操作结果。 
 

在Java中,同步异步是并发编程的重要概念,理解它们有助于编写高效的并发应用程序[^2]。 ### 概念 - **同步**:同步是指程序在执行一个任务时,必须等待该任务完成后才能继续执行下一步操作。这种方式确保了对共享资源的协调访问以及操作的顺序执行[^1]。 - **异步**:异步是一种通讯方式,对设备需求简单。计算机多线程的异步处理与同步处理相对,在执行任务时不必等待该任务完成,而是继续执行下一步操作。异步双方不需要共同的时钟,接收方不知道发送方什么时候发送,所以在发送的信息中要有提示接收方开始接收的信息,如开始位,同时在结束时有停止位。并且异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程[^3][^4]。 ### 区别 同步处理时程序需要等待任务完成才能继续后续操作;而异步处理时程序不必等待任务完成,可继续执行下一步操作。异步处理可以提高程序的效率,因为程序能同时处理多个任务,在等待时间较长的任务执行期间可执行其他任务,充分利用计算机资源,提高程序的运行效率;而同步处理更注重操作的顺序性对共享资源的协调访问[^1][^3]。 ### 应用场景 - **同步**:适用于需要协调共享资源访问、保证顺序执行的场景。例如多个线程同时访问同一个文件,为避免数据混乱,需要使用同步机制保证同一时间只有一个线程可以访问该文件[^1]。 - **异步**:适用于可以并行执行、不依赖彼此结果的任务。例如在Web应用中,当用户发起一个文件下载请求时,服务器可以异步处理该请求,在处理下载任务的同时继续响应其他用户的请求,提高服务器的处理效率[^1][^3]。 以下是简单的Java同步异步代码示例: ```java // 同步示例 class SynchronousExample { public static void main(String[] args) { System.out.println("开始同步任务"); synchronousTask(); System.out.println("同步任务完成,继续后续操作"); } public static void synchronousTask() { try { // 模拟耗时操作 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("同步任务执行完毕"); } } // 异步示例 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class AsynchronousExample { public static void main(String[] args) { System.out.println("开始异步任务"); ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(() -> { try { // 模拟耗时操作 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("异步任务执行完毕"); }); System.out.println("不等待异步任务,继续后续操作"); executor.shutdown(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值