树莓派开始,玩转Linux23:多任务与同步

树莓派开始,玩转Linux23:多任务与同步

上一章提到了IPC,实际上它涉及一个关键问题:计算机的并发性。Linux系统是一个支持并发(Concunrrency)的操作系统。
并发系统可以同时执行多个任务。多个进程通过IPC的数据沟通,可以合作完成一个复杂任务。然而,并发系统并不简单,必须解决同步的问题。

1.并发与分时:

在过去很长时间里,计算机使用的都是单核CPU。每个时刻,单核的CPU只能执行一条指令。从指令的角度看,单核CPU计算机不能并发。

但单核CPU计算机可以同时运行多个任务。这种并发是通过分时(Time-Sharing)来实现的。所谓的"分时",就是把时间分配给多个服务对象。
这就好像一位妈妈同时照顾三个婴儿。她先给第一个孩子端上饭,然后给第二个孩子倒水。倒完水之后,她又跑去给第三个孩子梳头。妈妈把自己的时间分给了三个孩子。虽然每个特定的时刻,妈妈只能照顾一个孩子,但三个孩子还是能一直感受到妈妈的温暖。照顾多个进程的CPU类似于照顾三个孩子的母亲,如图所示。
在这里插入图片描述
照顾多个进程的CPU类似于照顾三个孩子的母亲

和母亲照顾多个孩子的情况类似,CPU的工作时间也可以分配给多个进程。CPU执行进程A一段时间,就换进程B继续执行。切换进程的工作由操作系统负责,操作系统会先把进程A的状态更新到进程A的描述符中,再根据进程B描述符中的记录,从进程B上次暂停的地方继续进行下去。

这样,多进程协作的目的就可以实现。即使在单核计算机上,我们也可以边浏览网页边听音乐,也就是说,单核CPU也可以让多个任务同时推进。

现代的多核CPU,可以同时执行多个指令,从基础的物理层面实现了并发。也就是说,现代的计算机看起来像是多位保姆照顾多个婴儿。即便如此,操作系统还是会用分时的方式来安排任务。原因很简单,计算机中的任务总数很容易超过CPU可以同时执行的指令总数。此外,通过分时系统,计算机的运行效率也能有效提升。我们将在讲解调度器的时候,深入这一点。

2.多线程:

第节中的并发是通过多个进程实现的。多进程加上IPC,就已经提供了丰富的多任务协作方式。如果调出其中的一个进程看,它的内部只进行一个任务,不会有并发。进程就好像一位专心写作业的小朋友,不会同时看电视。这种注意力单一、每个时刻只做一件事的工作方式,叫作单线程(Single-Threading)。我们前面见过的进程,都是单线程进程。

但程序员很多时候会在一个进程内部运行多线程(Multi-Threading)。多线程允许在一个进程中同时执行多个子任务。由于我们要同时关照多个线程的状态,进程的结构必须发生变化。

· 进程描述符需要记录每个线程的相关信息,特别是它们的状态
和进度。这一点和进程的情况类似。
· 操作系统需要把适当的计算时间分配给进程。内核调度器在分
配计算时间时,必须把各个线程考虑在内。
· 进程空间中必须有多个栈。

前两者和进程的情况类似。着重看最后一点,即进程空间中必须有多个栈。栈记录着函数调用的顺序,最下方的帧是唯一一个激活函数。既然多线程是多任务并发,那就意味着会有多个函数处于激活状态,并同时运行。比如下面的多线程程序:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在C语言中,可以用pthread的一系列函数来操作多线程。我们把某个函数放入到新线程中执行,如func1(),程序输出如下所示。
在这里插入图片描述
程序的运行流程是一个多线程的流程,如图所示。
在这里插入图片描述
多线程流程

从main()到func3()再到main()构成一个线程,而func1()和func2()构成另外两个线程。

当程序创建一个新的线程时,必须为这个线程建一个新的栈,每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务。因此,多线程的进程在内存中有多个栈。多个栈之间以一定的空白区域隔开,以备栈的增长。对于多线程来说,由于同一个进程空间中存在多个栈,任何一个空白区域被填满都会导致栈溢出的问题。

进程空间上需要调整栈的部分。一个线程与其他线程共享内存中的程序段、堆和全局数据。这些部分的组织方式也和单线程进程类似。由于多线程共享了很多内存区域,它们都可以直接读写堆上的内容,线程间的数据共享变得很简单。因此,多线程的数据交流成本要比多进程低得多。这也是程序员使用多线程的一大原因。

3.竞态条件:

多进程和多线程都实现了并发。并发系统实现了多任务协作,但容易产生竞态条件(Race Condition)。如果多个任务可以共享数据,特别是可以同时修改某个数据时,就很有可能发生竞态条件。

我们来看竞态条件在现实生活中的例子。如果一节火车有100张票,同时在10个售票窗口销售。每位售票员卖完一张票之后,就打电话告诉总部卖出去一张票,可售卖的票数就会减1。

用一个多线程程序来重现上述情形。程序用全局变量i存储剩余的票数。多个线程不断地卖票,也就是从i中减去1,直到剩余票数为0,因此每个都需要执行如下操作 :

在这里插入图片描述
每个线程会进行两件事。一件事是判断是否有剩余的票,即判断i是否等于0。另一件事是卖票,即从i上减去1。这两件事情之间存在一个时间窗口,其他线程可能在此时间窗口内执行卖票操作,即从i中减1。但之前的卖票线程已经执行过了判断,不知道i发生了变化,所以会继续执行卖票,以至于卖出不存在的票,让i成为负数。对于一个真实的售票系统来说,这将成为一个严重的错误。

在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清哪一个会先执行。这个时候,如果并发任务可以同时读取同一块数据,就会造成结果难以预测的情况。因此,在并发系统中,如果运行的结果依赖于不同线程执行的先后顺序,则会造成竞态条件。

4.多线程同步:

对于并发程序来说,同步(Synchronization)是指在一定的时间内只允许某一个任务访问某个资源。同步可以解决竞态条件的问题。比如,某段时间内只能有一个售票员查询票数并售出,其他售票员在此期间不能售票,就不会有竞态条件的问题。

以多线程为例,多线程同步就是在一定的时间内只允许某一个线程访问某个资源。在多线程中,我们可以通过互斥锁(Mutex)、条件变量(Condition Variable)和读写锁(Reader-Writer Lock)来同步资源,分别来看它们的功能。

1.互斥锁
互斥锁是一个特殊的变量,它有锁上和打开两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开。其他想要获得互斥锁的线程,要等到互斥锁再次打开的时候。

我们可以将互斥锁想象成为只能容纳一个人的洗手间,当某个人进入洗手间时,可以从里面将洗手间锁上。其他人只能在互斥锁外面等那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以先冲进去。上面的问题很容易使用互斥锁模拟,每个线程的程序可以改为:

在这里插入图片描述
变量mu就是互斥锁。第一个执行mutex_lock()的线程会先获得互斥锁。其他想要获得互斥锁的线程必须等待,直到第一个线程执行到mutex_unlock()释放互斥锁,才可以获得互斥锁,并继续执行线程。因此线程在进行mutex_lock()和mutex_unlock()之间的操作时,不会被其他线程影响。

每个线程必须遵守互斥锁的上述使用规则,才能保证互斥锁发挥作用。如果某个线程不尝试获得互斥锁,而是直接修改变量i,那么互斥锁就失去了保护资源的意义。互斥锁的效力在于多线程共同遵守规则,它本身并不能硬性阻止线程对i的修改。总之,互斥锁机制需要程序员自己写出完善的程序来发挥互斥锁的功能。下面介绍的其他机制也是如此。

2.条件变量
条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。举个例子,老板请了100个工人,让每个工人负责装修一个房间。当有10个房间装修完成的时候,老板就会去检查已经装修好的10个房间,然后通知这10个工人一起去喝啤酒。

我们可以并发地装修,也就是开100个线程,让每个线程对应一位工人的工作。但在多线程条件下,会有竞态条件的问题。其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式可以解决这个问题。
在这里插入图片描述
上面使用了条件变量。条件变量cond除了要和互斥锁mu配合之外,还需要和另一个全局变量num配合。这里的num表示装修好的房间数。这个全局变量用来构成所谓的"条件"。具体思路如下。我们在工人装修好房间,也就是执行num=num+1之后,去检查已经装修好的房间数是否小于10 。由于互斥锁被锁上,所以不会有其他工人在此期间装修房间,也就是改变num的值。如果该工人是前10个完成的人,那么就调用cond_wait()函数。

cond_wait()做两件事情,一个是释放互斥锁mu,从而让别的工人可以建房;另一个是等待条件变量cond的通知。这样,符合条件的线程就开始等待。当"第10个房间已经修好"的通知到达时,condwait()会再次锁上mu。线程的恢复运行,执行下一句代表喝啤酒的printf(“drink beer”)。从这里开始,直到mutex_unlock(),就构成了另一个互斥锁结构。

前面10个调用cond_wait()的线程如何得到通知呢?我们注意到elseif,即修建好第11个房间的人负责调用cond_broadcast()。这个函数会给所有调用cond_wait()的线程发通知,以便让前面10个等待的线程恢复运行。

条件变量特别适用于多个线程共同等待某个条件发生的情况。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

3.读写锁
读写锁与互斥锁非常相似,但它对读写做出了区分。如果一个共享资源只有读取而没有写入操作,那么多个任务可以同时读取,而不用担心竞态条件的发生。一旦有一个进程开始写入,那么其他想要读取和写入的进程必须等待该进程完成写入,才能继续操作。因此,读写锁中包含了两把锁,即读锁(R)和写锁(W)。

应用程序应该用R锁来控制读取操作。如果一个线程获得R锁,读写锁允许其他线程继续获得R锁,而不必等待该线程释放R锁。也就是说,多个进程可以同时读取同一资源。W锁用来控制写入操作,同一时间只能有一个线程获得W锁。不过,在获得W锁之前,线程必须等到所有持有共享读取锁的线程释放掉各自的R锁,以免自己的写入操作干扰到其他线程的读取。

我们这一部分介绍了一些常见的多线程同步方法。多进程同步的方法也类似,这里不再赘述。我们看到,多任务同步的实施有赖于程序员的编程付出,但在多核计算机和多主机集群流行的大背景下,多任务程序又是提高资源利用率的关键手段,因此多任务同步就变得极为重要。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,树莓开始Linux PDF 是一本介绍如何在树莓上使用Linux操作系统的电子书。它包含了从安装Linux到使用命令行和图形界面进行操作的详细指南,还介绍了如何使用树莓进行编程和网络连接等方面的内容。这本书对于想要深入了解树莓Linux的人来说是非常有用的。 ### 回答2: 树莓是一款小型的单板计算机,拥有良好的扩展性和低功耗特性,因此备受开发者和爱好者的喜爱。而Linux作为开源操作系统,已经成为开发者们最重要的操作系统之一。 树莓的硬件特性使得它非常适合运行Linux操作系统。而且,官方提供的Raspbian等系统均是基于Linux的,因此我们可以轻松地将树莓Linux系统进行结合。 《树莓开始Linux》是一本面向初学者的指南,其框架比较清晰,从基础的Linux命令开始介绍,然后逐渐深入到网络配置、安全设置等方面。通过阅读本书,我们可以学习到如何将树莓Linux结合起来,还能够掌握Linux的基础操作。 具体来讲,本书讲解了如何安装和配置树莓,包括如何选择和下载Linux操作系统,如何安装和配置SSH(Secure Shell)、VNC(Virtual Network Computing)等,使用户可以通过远程控制来管理设备。同时,本书还涉及了Linux系统的基础操作,包括文件和目录操作、文本编辑、用户和权限管理等等。 总之,学习《树莓开始Linux》会让我们对树莓Linux系统有更深入的了解,能够更好的利用树莓进行开发和应用,也将极大地提升我们的技能和竞争力。 ### 回答3: 树莓开始Linux PDF是一本比较值得推荐的书籍,它适合初学者学习树莓Linux系统。树莓作为一种新型的单板计算机,在硬件性能和软件应用方面有着许多亮点,它可以用来实现很多最初想象不到的应用。 在这本书中,作者详细介绍了在树莓上使用Linux系统的方法和相关技巧,主要包括系统安装、文件系统、网络设置、硬件接口、Python编程等方面。书中的内容既涉及到树莓硬件的使用,也关注到了Linux系统的全面运用,这也充分体现了树莓的多功能性和灵活性。 该书对于初学者学习树莓Linux系统的方法非常友好,它不仅提供了基本知识的讲解,而且还有很多例程和实践,这样可以帮助初学者快速入手和上手。特别是对于以前没有接触过树莓Linux系统的读者,该书对于他们理解树莓Linux系统的应用具有很好的借鉴价值。 总之,树莓开始Linux PDF是一本好书,它的出现可以方便初学者对树莓Linux系统的理解和使用,同时可以丰富各行各业对于树莓的应用和推广,这也有助于树莓在未来的应用市场上得到更加广泛地应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值