【万字长文】操作系统如何解决并发问题?

本文深入探讨了操作系统如何处理并发问题,重点关注了互斥、睡眠与唤醒、信号量、互斥量和管程等同步机制。通过实例分析了并发导致的竞争条件,并介绍了多种避免此类问题的方法,如屏蔽中断、锁变量、严格轮换法、Peterson算法等。文章还提到了条件变量和信号量在多线程环境中的应用,以及Pthread中的互斥量和条件变量如何协同工作,帮助程序员理解和解决并发控制中的挑战。
摘要由CSDN通过智能技术生成

在之前的文章中我们讲到过引起多线程bug的三大源头问题(可见性,原子性,有序性问题),java的内存模型(Java Memory Model)可以解决可见性和有序性问题,但是原子性问题如何解决呢?

public class Counter {
   

  volatile int count;

  public void add10K() {
   
    for(int i = 0; i < 10000; i++) {
   
        count++;
    }
  }

  public int get() {
   
      return count;
  }

  public static void main(String[] args) throws InterruptedException {
   

    Counter counter = new Counter();

    Thread t1 = new Thread(() -> counter.add10K());

    Thread t2 = new Thread(() -> counter.add10K());

    t1.start();
    t2.start();

    t1.join();
    t2.join();

    System.out.println(counter.count);
  }

}

上面的代码我们的期望结果是20000,可以运行结果确实小于20000的。是因为count这个共享变量同时被两个线程在修改,而count++这条语句在cpu中实际对应三条指令

  1. 读取count的值
  2. count + 1
  3. 将count+1的值重新复制给count

而线程切换就可能发生在执行完任意一个指令的时候,比如 现在count的值为100, 线程1读取到了之后加1完成,但是还没有将新的值赋值给count,此时让出cpu,然后线程2执行,线程2读取到的值也为100,执行后count++之后,才让出cpu,此时线程1获取到执行资格,将count设置为101。

类似上面的情况,两个或多个线程读写某些共享数据,而最后的结果取决于线程运行的精准时序,称为竞争条件。
那么如何避免竞争条件呢?实际上凡是涉及共享内存、共享文件以及共享任何资源的情况都会引发与前面类似的错误,要避免这种错误,关键是要找出某种途径来阻止多个线程同时读写共享的数据。

换言之,我们需要的是互斥,即以某种手段确保当一个进程在使用另一个共享变量或文件时,其他进程不能做同样的操作。

上面例子的症结在于,线程1对共享变量的使用未结束之前线程2就使用它了。

线程的存在的问题,进程也同样存在。所以我们接下来看看操作系统是如何处理的。

一个进程的一部分时间做内部计算或另外一些不会引发竞争条件的操作。在某些时候进程可能需要访问共享内存或文件,或执行另外一些会导致竞争的操作。

我们把对共享内存进行访问的程序片段称作临界区域或临界区。如果我们能够使得两个进程不同时处于临界区中,就能避免竞争。

为了保证使用共享数据的并发进程能够正确和高效地进行写作,一个好的解决方案需要满足以下4个条件。

  1. 任何两个进程不能同时处于其临界区
  2. 不应对CPU的速度和数量做任何假设
  3. 临界区以外运行的进程不得阻塞其他进程
  4. 不得使进程无限期等待进入临界区

在这里插入图片描述

比较期望的进程行为如上图所示。进程A在T1时刻进入临界区。稍后,在T2时刻进程B试图进入临界区,但是失败了,因为另一个进程已经在临界区内,而一个时刻只允许一个进程在临界区内。所以B被暂时挂起知道T3时刻A离开临界区为止,从而允许B立即进入。最后,在B离开(在时刻T4),回到了在临界区中没有进程的原始状态。

互斥

为了保证同一时刻只能有一个进程进入临界区,我们需要让进程在临界区保持互斥,这样当一个进程在临界区更新共享内存时,其他进程将不会进入临界区,以免带来不可预见的问题。

接下来会讨论下关于互斥的几种方案。

屏蔽中断

单处理器系统中比较简单的做法就是在进入临界区之后立即屏蔽所有中断,并在要离开之前在打开中断。屏蔽中断后,时钟中断也被屏蔽,由于cpu只有发生时钟中断或其他中断时才会进行进程切换,这样在屏蔽中断后CPU不会被切换到其他进程,检查和修改共享内存的时候就不用担心其他进程介入。

中断信号导致CPU停止当前正在做的工作并且开始做其他的事情

可是如果把屏蔽中断的能力交给进程,如果某个进程屏蔽之后不再打开中断怎么办?
整个系统可能会因此终止。多核CPU可能要好点,可能只有其中一个cpu收到影响,其他cpu仍然能正常工作。

锁变量

锁变量实际是一种软件解决方案,设想存在一个共享锁变量,初始值为0,当进程想要进入临界区的时候,会执行下面代码类似的判断


if( lock == 0) {
  lock = 1;

  // 临界区
  critical_region();
} else {
   wait lock == 0;
}

看上去很好,可是会出现和文章开头演示代码同样的问题。进程A读取锁变量lock的值发现为0,而在恰好设置为1之前,另一个进程被调度运行,将锁变量设置为1。
当第一个进程再次运行时,同样也将该锁设置为1,则此时同时有两个进程进入临界区中。

同样的即使在改变值之前再检查一次也是无济于事,如果第二个进程恰好在第一个进程完成第二次检查之后修改了锁变量的值,则同样会发生竞争条件。

严格轮换法

这种方法设计到两个线程执行不同的方法

线程1:


while(true) {
   
  
  while(turn != 0) {
   
    // 空等
  }
  // 临界区
  critical_region();
  
  turn = 1;
  // 非临界区
  noncritical_region();
}


线程2:

while
MySQL多数据源是指在一个应用程序中同时使用多个不同的MySQL数据库来存储和管理数据的技术。它可以帮助开发人员更灵活地处理各种数据库操作,提高程序的性能和可扩展性。下面是一个完整的MySQL多数据源教程。 一、设置数据库连接信息 1. 在应用程序的配置件中,创建多个数据库连接的配置项。例如,可以为每个数据源创建一个配置项,分别命名为db1、db2等。 2. 在配置项中,设置每个数据源的连接信息,包括数据库地址、用户名、密码等。 二、创建数据源管理器 1. 创建一个数据源管理器类,用于管理多个数据源。该类需要实现数据源的动态切换和获取。 2. 使用Java的线程安全的数据结构,如ConcurrentHashMap来存储数据源信息。将配置件中的数据库连接信息加载到数据结构中。 3. 实现方法来切换不同的数据源,通过传入数据源的名称来切换到对应的数据库。 三、实现数据源切换 1. 在应用程序中,根据业务需求选择需要使用的数据源。可以通过调用数据源管理器的方法来切换数据源。 2. 在DAO层的代码中,根据当前使用的数据源名称,选择对应的数据源进行数据库操作。 四、使用多数据源进行数据库操作 1. 在DAO层的代码中,区分不同的数据源,并将数据库操作的代码包装在对应的数据源中。 2. 在业务层的代码中,调用DAO层的方法来进行数据库操作。不同的数据源会自动切换。 五、处理事务 1. 如果需要在一个事务中操作多个数据源,可以使用分布式事务的方式来处理。 2. 可以使用开源的分布式事务框架,如Atomikos、Bitronix等来实现多数据源的事务管理。 六、监控和维护 1. 使用监控工具来监控多个数据源的使用情况,包括连接数、查询次数等。 2. 定期对数据库进行维护,包括索引优化、数据清理等工作,以保证数据库的性能和稳定性。 通过以上步骤,我们可以实现MySQL多数据源的配置和使用。使用多数据源可以更好地管理和处理不同的数据库操作,在提高程序性能和可扩展性的同时,也提供了更灵活的数据操作方式。同时,需要注意合理选择和配置数据源,以及监控和维护数据库,以保证系统的运行效率和数据的安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值