练习
本页包含几个练习,练习并行编程,了解Java和Erlang,并为作业做好准备。我们还提供了一些使用语言Promela和相关工具Spin的练习,这些练习可用于模拟和正式验证程序模型是否满足某些并发相关属性。这些练习都不是强制性的。
Java练习
可以找到Java练习的解决方案在这里
练习1:共用计数器
在这个问题中,我们考虑以下Java程序:
班计数器实施 可运行{
私有的 整数计数器=0;
私有的 最终的 整数轮=100000;
公众的 空隙 运行() {
//尝试{
对于(整数我=0; i<轮数;i++){
计数器++;
}
//}catch(中断异常e){
//System.out.println(“中断!”);
// }
}
公众的 静止的 空隙 主要的(字符串[]参数){
尝试{
计数器c=新的 计数器();
//创建两个运行run()方法的线程。
螺纹t1时间=新的 螺纹(c),“螺纹1”);
螺纹t2时间=新的 螺纹(c),“螺纹2”);
第1页。开始(); 第2章。开始();
//等待线程完成。
第1页。参加(); 第2章。参加();
//打印计数器
系统.外面的.打印ln(c.)。柜台);
}抓住(中断异常e){
系统.外面的.打印ln(“被打断了!”);
}
}
}
main方法创建一个计数器
对象并在其上运行两个线程,都运行其运行()
方法更新共享计数器。两个线程更新计数器100000次后,它们完成并打印其值。预计产量为200000。编译并运行程序。
您得到的结果可能取决于硬件、操作系统和Java环境的版本。如果你在多核机器上运行它,很可能至少有一些运行会产生与200000不同的结果。
原因是递增计数器实际上不是一个基本操作,而是两个。代码计数器++
编译成如下内容:
因此,当一个线程将要向共享计数器变量写入新值时,另一个线程可能会同时多次(甚至多次)更新它。Java标准并不保证线程以任何方式同步运行。事实上,它们通常是完全不同步的,并以非常令人惊讶的顺序执行。每当在不同线程中执行代码的特定顺序导致程序出错时,我们将其称为比赛条件在这种情况下,这是两个线程读取和更新同一计数器之间的竞争条件。
如何解决这个问题?一种可能的方法是限制两个线程的操作顺序。特别是,我们不希望一个线程在另一个线程读取计数器但尚未更新时读取并更改计数器。
简而言之,我们要确保一次最多有一个线程在执行读取和更新计数器的代码。此属性称为相互排斥实现这一目标的一种方法是使用已同步
Java中的关键字。更换管路计数器++
使用此代码:
我们添加了一个已同步
块,确保没有其他已同步
来自同一对象的块(这
)同时执行。在我们的例子中,这意味着另一个线程,它试图进入同一个线程上的同步块计数器
对象,则必须等待第一个线程离开块。
重新编译并运行该示例,现在打印的值应始终为200000。我们没有更改程序执行的实际操作,但通过控制操作的执行顺序,我们确保了结果是正确的。
现在是练习的主要部分。你的工作是找出原始的、未同步的程序报告的计数器的最小值。你还必须证明这一点。但是,您将能够在战略时刻暂停线程,以获得获得结果的良好机会,而不是大量运行程序并希望获得不太可能的结果。
从获取原始程序(没有同步块)开始,并替换计数器++
具有
现在,您将向循环中添加许多这样的行:
如果(id==??&&i==??)螺纹.睡觉(??);
每一行都会将循环的特定迭代中的一个线程延迟特定的毫秒数。为了能够告诉您处于哪个线程中,您必须将此代码添加到运行()
方法:
整数身份证件;
字符串姓名=螺纹.当前线程().获取名称();
id=名称。等于(“螺纹1”) ?1:2;
注意,在普通程序中,您应该使用自己的数据(例如整数)作为标识符来标识线程。线程.getName()
有利于调试。还有,因为线程.sleep()
是可中断的,它可能会抛出中断异常
,我们必须在这里抓住。此异常用于安全线程取消,我们将在本练习中不讨论此主题。相反,我们将只捕获它并忽略它。为此,您需要取消注释运行()
方法。
班计数器实施 可运行{
私有的 整数计数器=0;
私有的 最终的 整数轮=100000;
公众的 空隙 运行() {
尝试{
整数身份证件;
字符串姓名=螺纹.当前线程().获取名称();
id=名称。等于(“螺纹1”) ?1:2;
对于(整数我=0; i<轮数;i++){
//在这里耽搁?
整数tmp=计数器;
//也许在这里?
计数器=tmp+1;
//还是在这里?
}
}抓住(中断异常e){
系统.外面的.打印ln(“被打断了!”);
}
}
公众的 静止的 空隙 主要的(字符串[]参数){
尝试{
计数器c=新的 计数器();
//创建两个运行run()方法的线程。
螺纹t1时间=新的 螺纹(c),“螺纹1”);
螺纹t2时间=新的 螺纹(c),“螺纹2”);
第1页。开始(); 第2章。开始();
//等待线程完成。
第1页。参加(); 第2章。参加();
//打印计数器
系统.外面的.打印ln(c.)。柜台);
}抓住(中断异常e){
系统.外面的.打印ln(“被打断了!”);
}
}
}
找出程序可以报告的最少数字,并通过插入延迟来证明这一点。
以下是我们希望从上述练习中得出的结论。首先,我们演示了如果由多个线程在没有适当同步的情况下并发执行,即使是简单的数据操作也会导致损坏的结果。数据损坏是有错误的并发代码的常见问题。其次,如果并发操作的顺序非常特定(而且不太可能),有时并发程序可能会产生非常意外的结果。我们能够通过限制不同线程使用同步执行的操作的不同可能顺序来解决并发问题。
练习2:分幅显示
在本例中,我们将考虑在机场中常见的一种显示,其中给出了即将到来的航班的信息:
我们将研究这种显示在Java中的编程。这将使我们能够研究并发线程干扰共享数据结构使用的简单情况。
首先下载文件显示Java.zip到合适的工作目录。然后减压:
不幸的是,我们无法访问真正的10平方米硬件显示器,所以这里有一个完整的Java实现,其中显示是使用JavaSwing库可视化的。然而,我们应该将其视为以以下方式模拟硬件设备的编程。
使用文件中的以下简单API访问硬件硬件显示.java
:
公众的 接口硬件显示器{
公众的 整数 获取行();
公众的 整数 获取Cols();
公众的 空隙 写(整数行,整数科尔,烧焦c) ;
}
前两个方法分别返回显示中的行数和列数(可能设备的大小不同)。第三个用于在指定的行和列中写入一个字符。按照Java的约定,我们从0开始计算行和列。
文件JDisplay.java
包含此API的一个使用Swing的实现。本练习不需要查看此文件它包含了许多特定于Java GUI库的底层细节,而这些对于本课程来说并不是必需的。相反,请查看Main1.java中的一个简单主程序,该程序在显示屏的不同位置写入a和B,然后等待三秒钟,最后删除a。之后,程序只需等待进一步的输入,您必须使用Ctrl-C将其杀死。要查看此操作,请执行以下操作:
javac语言*.java文件
java语言主要1
您可能会注意到方便的功能打盹
供将来使用。
然而,很明显,界面硬件显示器
对于实际编程显示器来说太低了。因此,制造商还提供高级显示.java
,使用更方便:
公众的 接口高电平显示器{
公众的 空隙 清楚的();
公众的 空隙 addRow(添加行)(字符串str);
公众的 空隙 删除浏览(整数行);
}
方法clear从显示中删除所有文本。addRow(str)
添加字符串字符串
作为新的最后一行,位于最后一个现有行之后。最后,删除行(r)
删除行第页
,将下面的所有行向上移动一个插槽。此外,目的是在显示器完全使用时也可以添加行;在删除上面足够多的行之前,该行在屏幕上不可见。最后,最后一个可见的更改通过闪烁几次来突出显示。当然,所有这些都应该有更完整的文档记录,最好使用java文档
,但我们故意省略了这一点,以免使代码混乱。
显示器制造商还在文件中提供了此接口的实现JDisplay2.java语言
。您应该学习并理解本课程,但首先您可能想运行一个简单的演示:
我们没有时间考虑使用此API为机场管理部门设计一个完整的程序,但请注意,可以想象,显示器将通过多线程程序访问,几个机场官员同时更新显示器。测试类J显示2
在这种情况下,您现在的任务是编写一个简单的多线程程序。在文件中找到骨架主要3.java
,它显示了创建显示的简单主程序的结构d日
并启动两个线程,其中一个执行静态过程添加过程(d)
,另一个正在执行删除程序(d)
。您必须完成这两个程序的主体。填充添加进程
有一个序列addRow(添加行)
命令,中间穿插着适当的小睡。同样,填充删除proc
呼叫deleteRow(0)
。为了进行不太枯燥的模拟,小睡的顺序应该是秒(或秒的分数),而不是像在真实机场那样的分钟。
如果你这样做并运行你的程序,你可能会看到一些未被关注的行为。你应该确保你了解这些问题是如何发生的。事实上,这个班级J显示2
不是线程安全的; 当并发线程访问其方法时,它不能保证正确的行为。
现在,我们将用两种不同的方法解决此问题:
- 机场IT部门,开发应用程序
电源3
,无权访问J显示2
,但仅限于接口和对象文件。因此,他们必须在不修改的情况下解决问题J显示2
一种方法是确定电源3
并使用信号灯保护它们。做这个!您应该使用的实例java.util.concurrent。信号灯
为此目的。(java.util.concurrent(java.utilconcurrent)
是Java 1.5中的一个新包。)请注意获取()
可能会抛出中断的异常
,因此您必须使用尝试
/抓住
结构。再次测试程序,看它是否正常运行。我们说“似乎”,因为您现在应该对测试并发程序的困难有了初步的感觉。
- IT部门还向显示器制造商投诉,要求线程安全实现
J显示2
。满足这个要求。很简单:只需添加修改器已同步
到每个方法的标头。我们将在稍后的课程中对此进行解释,但目前您只需检查您的首字母电源3
(没有信号量)和线程安全J显示2
给出了正确的程序。作为最后一句话,我们只需要注意,是否使类线程安全是一个重要但不平凡的设计决策。对于多线程使用来说,这是必要的,但它确实意味着性能下降。非线程安全的大型库的一个例子是GUI的Swing库。Collections框架中使用的一种可能性是提供非同步的基本实现和同步的包装器类。您可能还想考虑对原始程序的另一个修复:IT部门本可以为生成一个简单的同步包装类,而不是使用信号量JDisplay2.java语言
.
障碍物同步练习
在这些练习中,您将练习创建线程并使用信号量同步它们的行为。我们将从一个打开一个窗口的程序开始,在这个窗口中,两个“球”开始移动,在墙上反弹。
下载程序到您的工作目录并解压缩文件。您将找到三个类:
-
球.java
此类的一个实例是一个球,它位于BallWorld中。该类是的子类螺纹
它的运行是一个无限循环,在这个循环中,球不断地移动,更新它的位置,发出命令,迫使世界重新绘制,并短暂休眠(30ms)。在给定图形上下文的情况下,球还具有绘制自身的能力。此外,在创造时(即在其构造器中),球通过调用世界的addBall(添加球)
方法。
-
BallWorld.java足球世界.java
例如,一个世界可能包含几个球,存储在数组列表
.世界是面板
,的摆动
用作绘图表面的类。因此,世界可以画自己,它在paintComponent(油漆组件)
方法是让所有包含的球画出它们自己。详细信息摆动
绘画对于练习来说不那么重要,但严肃的Java程序员必须了解更多,特别是关于BallWorld.addBall球
.
-
球.java
该类包含main方法,该方法创建一个世界和几个球,并将世界添加到摆动
窗口。
请注意,本例中的(并发)控制流相当微妙。球的移动和重新绘制由每个球独立启动,因为每个球都在运行单独的控制线程。因此,可能执行不同的球doMove()
和world.repaint()
同时。呼叫world.repaint()
正如Swing文档中所述,并发是可以的。但是打电话重新打印()
也会触发调用图纸()
所有球的注册方法。这也是可以的,因为两者都图纸()
和doMove()
方法是同步的,这意味着不会对给定对象并发调用它们。
如您所见,这个非常简单的程序已经具有复杂的并发性。对于简单的程序来说,这样的设计是可以接受的,但对于更大的程序,其并发行为必须以结构化的方式设计,否则将非常复杂。
现在回到练习。编译并运行程序:
看看这些课,确保你理解了它们。
练习3.1:击球
您的第一个任务是向程序中添加另一个线程,该线程会在任意时间杀死球(即在其间有短暂的随机延迟)。但这些球应该按随机顺序被击杀。杀死一个球意味着它的run方法必须终止。这应该通过使循环正常终止来实现,而不是通过调用弃用的方法来停止线程。此外,必须将球从世界上移除(以类似于添加球
). 完成此操作后,运行时系统中的垃圾收集器将最终回收为ball对象分配的空间。
为了解决这个问题,你可以利用球试图获取的信号量来“获得死亡许可”。信号量最初为零,然后在main中启动的线程中释放多次。注意,使用tryAcquire方法获取信号量非常有用。
实现这一点并测试您的程序。确保您了解设计如何使杀戮顺序不可预测。如果您愿意,可以进一步更改程序的行为,以便在一段时间(随机)后死球重生。
练习3.2:冻结球
现在返回到程序的原始版本(创建一个新目录并再次下载程序)。
现在必须修改程序以实现以下行为:当一个弹跳球在其一次移动后发现自己位于世界的对角线区域(即x非常接近y)时,它将“冻结”,即停止移动。注意,一个球可能会在一次移动中跳过对角线区域;这不会导致它冻结。当所有球在对角线冻结时,它们都会醒来并继续弹跳,直到再次在对角线上冻结。这种弹跳/冻结将永远持续下去。
您应该认识到这是一种可以使用N+1信号量实现的屏障同步:一个公共屏障信号量,当球线程到达同步点时释放,以及一组“continue”信号量,按线程索引,线程获取这些信号量以继续超越屏障。
还需要一个特殊的屏障同步过程,该过程重复获取N次屏障信号量,然后释放所有连续信号量。
练习3.3:再次冻结
包裹java.util.concurrent(java.utilconcurrent)
包括该类篱栅
,这为实现屏障同步提供了更方便的手段。使用这个类而不是信号量重写上一个练习中的程序。
练习4:你自己的自行车障碍
现在做一个更难的练习:实现你自己的课堂主体篱栅
它提供了与同名Java类类似的功能。但我们对以下规格的更简单版本感到满意:
公众的 班 篱栅{
公众的 篱栅(整数各方);
公众的 空隙 等待();
}
参数当事人
是在允许所有线程继续之前,需要到达屏障的线程数。编写整个类,然后再次使用它来解决冻结球的问题。
提示:我们不能直接使用array-of-semaphores方法,因为这需要等待一个参数i来指示要阻塞的信号量的索引。相反,可以尝试使用一个所有进程都阻塞的信号量和一个整数变量来计算到达屏障的进程数。但是这个整数变量是共享的,所以我们需要使用第二个互斥信号来保护对它的更新。我们不能使用Java同步锁定代替互斥信号量。为什么?
请注意,然而,尝试简单地完成这个想法,您将得到一个错误的解决方案,它无法阻止快速进程“窃取”释放
从较慢的过程中。您可以通过对不同的屏障周期使用不同的信号量来解决此问题。
要测试实现,请修改弹跳球程序以使用新类。
练习5:单车道桥梁
河流上的桥梁只有一条车道,但汽车从两个方向进入。因此,需要某种同步来防止汽车碰撞。为了说明这个问题,我们为您提供了一个故障解决方案,其中没有执行同步。您的任务是为汽车添加同步功能。为了好玩,我们包含了一个编译过的图形界面。
不安全的解决方案可能是下载为示例3-java.zip
。编译并执行它。GUI应该是不言自明的;使用这两个按钮,您可以分别添加从右侧或左侧进入的汽车。你会看到汽车在桥上相撞。
像往常一样,您不需要理解图形代码就可以解决问题。你需要知道的是,从左向右行驶的汽车调用该方法controller.enterLeft()
当他们接近桥(请求许可)并调用方法时控制器.leaveRight()
当他们离开的时候。另一方向的汽车呼叫enterRight(输入右侧)
和离开左边
而不是。这里,控制器是类的一个实例交通管制员
,负责同步。如您所见,提供的类具有所有四个方法的空实现。您的任务是将该类更改为一个监视器,用于协调汽车,使其不会发生碰撞。
我们建议您在类中使用简单的监视机制对象
而不是更复杂的工具java.util.concurrent(java.utilconcurrent)
用于本练习。
这个练习有很多可能的解决方案——你能找到多少种汽车同步的方法?这两种方式有什么不同?你的实施公平吗?还是会出现饥饿?
Erlang练习
可以找到Erlang练习的解决方案在这里.
一般语法
-
机具倒档
反向([1,2,3])=[3,2,1]
-
实现一个在列表中查找并返回元素的函数
发现(4,[1,2,3,4,5,6])={发现,4}
查找(10,[1,2,3,4,5,6])=未找到
-
实现一个从列表中删除第一个出现的元素的函数
删除(4,[1,2,3,4,5,6])=[1,2,2,3,5,4]
删除(10,[1,2,3,4,5,6])=[1,2,4,5,1,6]
-
使用append实现扁平化(扁平化以列表形式存在:扁平化/1)
压扁([[1,2],[3,4,5],[6]])=[1,2,3,4,1,6]
-
实现一个函数,使列表中的每个数字平方。首先使用尾部递归,然后使用列表理解,然后使用列表:map/2
正方形([1,2,3])=[1,4,9]
-
实现过滤器:给定一个谓词(布尔函数)和一个列表,返回仅包含谓词所包含元素的子列表。
过滤器(fun(X)->X rem 2=:=0结束,[1,2,3,4,5,6])=[2,4,6]
简单消息传递
- 编写一个服务器,它接收数字,并打印出每个请求的当前平均值。下面是一个示例,说明如果您将代码放入模块中,它应该如何工作
foo公司
:
> 皮德 = 产卵(foo公司, 平均服务器, [0]).
电流 平均的 是 0
> 皮德 ! 10.
电流 平均的 是 5
> 皮德 ! 10.
电流 平均的 是 7.5
> 皮德 ! 10.
电流 平均的 是 8.75
- 更新服务器以记录和显示所有输入号码。与上面不同,它不应该从任何初始值开始。其行为应如下:
> 皮德 = 产卵(foo公司, 平均服务器, []).
> 皮德 ! 5.
这个 平均的 属于 [5] 是 5
> Pid公司 ! 10.
这个 平均的 属于 [5,10] 是 7.5
> 皮德 ! 20.
这个 平均的 属于 [5,10,20] 是 11.666666666666666
> 皮德 ! 50.
这个 平均的 属于 [5,10,20,50] 是 21.25
- 实现一个维护简单FIFO队列的服务器。您应该编写助手函数来为用户创建一个API,该API隐藏了队列的实现方式:
new_queue() %向队列服务器返回进程ID
推动(皮德, 价值) %将值添加到队列末尾
流行音乐(皮德) %从队列前面弹出第一项。如果队列为空,则应阻止@
下面是这些正在使用的函数的一个示例,同样假设您已将代码放入模块中foo公司
:
> 皮德 = foo:new_queue()。
> foo:推送(皮德, “你好”).
> foo:推送(皮德, {元组}).
> foo:弹出(皮德).
“你好”
> foo:推送(皮德, 333).
> foo:弹出(皮德).
{元组}
> foo:弹出(皮德).
333
> 产卵(乐趣() -> X(X) = foo:弹出(皮德), io:写(“我终于得到了:~p~n”, [X(X)]) 结束).
% ... 暂停。。。
> foo:推送([4,4]).
我 最后 得到了: [4,4]
二郎语中的符号
(此练习来自2015-10考试)
在本课程中,我们看到了用于并发编程的不同类型的原语:信号量、监视器、消息传递等。实际上,我们提到这些原语具有同等的表达能力,即,可以使用信号量做什么,可以使用监视器做什么,也可以使用信号灯做什么,等等。在这个练习中,我们将展示消息传递能够对信号量进行编码。
第一季度:实现以下Erlang模块。
-模块(扫描电镜).
-出口([创建SEM/1, 获得/1, 释放/1]).
创建SEM(初始值) -> ...
获得(信号量) -> ...
释放(信号灯) -> ...
例如,Erlang的进程可以以下列方式使用此模块:
Mutex公司 = 创建SEM(0),
获得(Mutex公司),
%%临界截面
释放(Mutex公司),
%%程序的其余部分
.
获取或释放信号量不应延迟获取或释放另一个信号量,每个信号量都关心自己的事情。
第2季度:编写一些代码来测试您的实现。它应该产生一系列进程,每个进程都使用信号量来控制对某些共享资源的访问。用它来说服自己,两个进程不能同时获得语义。
电梯
(此练习来自考试2016-03)
您正在为应用程序开发网络模块。模块的主要操作是发送(H,消息)
,它接受句柄和消息,并通过网络发送它们。这是实现此操作的模块的代码。
-模块(网络).
-出口([开始/0, 发送/2]).
启动() -> 产卵(乐趣 () -> 环 () 结束).
请求(皮德, 数据) ->
裁判 = make_ ref(),
皮德!{请求, 自我(), 裁判, 数据},
接收
{结果, 裁判, 结果} -> 结果
结束.
发送(皮德, 消息) ->
请求(皮德, {发送, 消息}).
循环() ->
接收
{请求, 皮德, 裁判, {发送, 消息}} ->
net_io:传输([消息]),
皮德 ! {结果, 裁判, 好 啊},
循环()
结束.
功能启动()
启动服务的新实例并返回其句柄发送(H,消息)
使用低级net_io:transmiss()
函数和返回好 啊
. Thenet_io:transmiss()
函数获取要立即发送的消息列表。为了简单起见,我们假设net_io:transmiss()
不使用任何其他参数来指定终点。
第一季度:由于一次发送一条消息的开销很大,您的团队决定呼叫发送()
不应该一次发送每条消息,而是等到累计十条消息后,再通过一次调用将所有消息发送给net_io:transmiss()
。更改网络模块以提供所描述的行为。
第2季度:消息缓冲有其自身的缺点。在前一个任务中实现的方案中,如果发送更多消息的请求到达得晚得多,则单个消息可能会延迟任意时间。为了缓解这种情况,应修改网络模块,使缓冲区中的消息保持时间不超过100毫秒。因此,每当缓冲区中包含100毫秒之前的消息时,所有消息都应发送出去,而不必等待剩余消息填充缓冲区。通过修改网络模块实现上述行为。