云计算百科
云计算领域专业知识百科平台

并发相关概念介绍:同步、异步、并发、并行、死锁、临界区等

欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。

并发编程是现代软件开发的基石,也是区分初级和高级程序员的重要标尺。

在现代软件开发中,并发编程是提高系统性能、利用多核处理器能力的关键技术。

无论是Web服务器、数据库系统还是分布式计算,都离不开并发编程。但并发编程也是一把双刃剑,在提升性能的同时,也带来了复杂性问题。

今天,我们将深入探讨并发编程中的相关概念,来构建并发相关的理论基础。

1. 同步(Synchronous)和异步(Asynchronous):理解调用方式的本质区别

同步和异步通常用来形容一次方法调用。

同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。这就好比你去商场买空调,向售货员下单后,在商店里等着,直到商家把空调配送安装完成,你们一起回家。这个过程是顺序执行的,你必须等待前一个步骤完成后才能进行下一个步骤。

异步方法调用则更像消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。异步方法通常会在另外一个线程中"真实"地执行。这就像网上购物:你完成在线支付后,购物过程对你来说就结束了,你可以去做其他事情,等快递送货上门即可。

图片

在编程中,同步调用像是普通函数调用,而异步调用通常通过回调、Future/Promise等机制实现。.NET中的async/await关键字和Java中的Future/Callable接口都是异步编程的典型实现。

2. 并发(Concurrency)和并行(Parallelism):细微差别背后的重大区别

并发和并行是两个最易混淆的概念,但它们有本质区别。

并发指的是在一段时间内,多个任务交替执行,从宏观上看像是"同时"执行,但微观上还是分时交替执行。这好比大家排队在一个咖啡机上接咖啡:虽然只有一个咖啡机,但通过交替使用,多个人都能接到咖啡。

图片

并行是真正意义上的"同时执行",需要多个执行单元。如同有两台咖啡机,两个人可以同时接咖啡,互不干扰。

图片

并发关乎程序结构设计,是一种能力;而并行关乎程序执行,是一种状态。

在单核处理器系统中,只能实现并发;而在多核系统中,既可以并发也可以并行。并发编程的价值在于,它既能提高资源利用率,又能让程序响应更快,还能更自然地建模某些问题域。

3. 临界区:共享资源的保护屏障

临界区表示一种公共资源或共享数据,可以被多个线程使用,但每次只能由一个线程使用。

当临界区资源被占用时,其他线程必须等待。比如办公室里只有一台打印机,如果张三正在使用它打印文件,李四就必须等待张三完成后才能使用。

临界区的典型特点是:

  • 互斥访问:一次只能有一个线程进入临界区

  • 有限等待:任何线程等待进入临界区的时间必须是有限的

  • 无优先权限制:不能假设线程调度的顺序和速度

实现临界区保护的技术包括锁、信号量、互斥量等。在Java中,可以使用synchronized关键字;在C++中,可以使用std::mutex;在C#中,可以使用lock关键字。

4. 阻塞(Blocking)和非阻塞(Non-Blocking):线程的等待策略

阻塞和非阻塞形容线程间的相互影响。

阻塞指一个线程占用了临界区资源,导致其他需要该资源的线程必须等待,这些等待的线程会被挂起。如果占用资源的线程长时间不释放资源,其他线程将一直无法工作。

非阻塞强调没有线程会妨碍其他线程执行,所有线程都会尝试不断向前执行。非阻塞算法通常使用原子操作(如CAS)实现,避免了线程挂起和唤醒的开销。

阻塞操作简单直观,但可能导致性能问题和死锁;非阻塞操作性能更好,但实现复杂,需要处理竞争条件和重试逻辑。

5. 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock):并发编程的三大陷阱

5.1 死锁

死锁指两个或多个线程互相持有对方所需的资源,导致所有线程都无法继续执行。就像两辆车在十字路口相遇,每辆车都被另一辆车挡住了去路,谁也不愿后退,形成僵局。

图片

死锁发生的四个必要条件:

  • 互斥条件:资源一次只能被一个线程使用

  • 请求与保持条件:线程在等待其他资源时不释放已占有的资源

  • 不剥夺条件:资源只能由占用它的线程主动释放

  • 循环等待条件:多个线程形成头尾相接的资源等待环

5.2 饥饿

饥饿指某个或某些线程因无法获得所需资源而一直无法执行。常见于线程优先级设置不合理或某些线程长时间占用资源的情况。

与死锁不同,饥饿在理论上是有可能解决的,比如当高优先级线程完成任务后,低优先级线程可能有机会执行。

5.3 活锁

活锁指线程不断重复相同的操作,但就是无法取得进展。就像两个人在走廊相遇,同时向一侧让路,结果又挡住了对方,于是又同时向另一侧让路,如此反复。

活锁与死锁的不同在于:活锁中的线程并未阻塞,而是在不断活动,但活动没有产生进展。

6. 死锁代码示例与避免策略

以下是一个简单的Java死锁示例:

// 死锁示例代码
public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1…");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for resource 2…");
                synchronized (resource2) {
                    System.out.println("Thread 1: Acquired resource 2!");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2…");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for resource 1…");
                synchronized (resource1) {
                    System.out.println("Thread 2: Acquired resource 1!");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

避免死锁的实用策略:

  • 按固定顺序获取资源:所有线程都按相同顺序请求资源

  • 使用超时机制:为锁请求设置超时时间,超时后释放已占有的资源

  • 避免嵌套锁:尽量减少同时持有多个锁的情况

  • 使用高级并发工具:如Java并发包中的并发集合和同步器

7. 结语

并发编程是现代软件开发中不可或缺的技能。通过理解同步/异步、并发/并行、临界区、阻塞/非阻塞以及死锁等相关概念,我们能够设计出更高效、更健壮的并发系统。

并发编程的学习曲线可能较为陡峭,但掌握其核心概念后,你会发现它并没有想象中那么可怕。重要的是理解各种概念的本质区别,并在实际开发中遵循最佳实践。

希望本文能帮助你建立对并发编程核心概念的清晰理解,为后续深入学习打下坚实基础。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 并发相关概念介绍:同步、异步、并发、并行、死锁、临界区等
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!