阅读22:队列和消息传递
http://web.mit.edu/6.031/www/sp17/classes/22-queues/#implementing_message_passing_with_queues
学校作业吨吨吨~
目标
在阅读了注释并检查了这个类的代码之后,您应该能够使用消息传递(使用同步队列)而不是共享内存来进行线程之间的通信。
两个模型的并发性
在我们介绍并发性,我们看到 并发编程的两种模型 : 共享内存 和 消息传递 。
-
在 共享内存 模型,并发模块之间的交互是通过阅读和写作共享可变对象在内存中。 创建多个线程在一个Java进程是我们的主要例子,共享内存并发性。
-
在 消息传递 并发模块交互模型,通过发送彼此不变的信息沟通渠道。 通信信道可以连接不同的计算机通过网络,在我们的一些最初的例子:上网、即时通讯等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rv9zFE2o-1621439570665)(http://web.mit.edu/6.031/www/sp17/classes/22-queues/figures/network.png)]
消息传递模型有几个优势共享内存模型,归结为更安全的bug。 在消息传递中,并发模块交互 显式地 通过通信通道,通过传递消息,而不是 隐式地 通过共享数据的变异。 共享内存的隐式交互可以很容易导致 无意中做的 互动,共享和操作数据的部分程序,不知道他们并发和不正确的线程安全合作策略。 消息传递也只股票不可变对象(消息)模块之间,而共享内存 需要 共享可变对象,我们已经看到 错误的来源
我们将讨论在这个阅读如何实现消息传递在一个过程。 我们将使用 阻塞队列 (现有的线程安全的类型)来实现一个进程中的多个线程之间的消息传递。
在接下来的并发读我们将看到如何实现消息传递客户机/服务器进程之间通过网络。
消息通过线程传递
我们看到在 锁和同步 一个线程块试图获得一个锁 直到锁已经发布的当前所有者。 阻塞意味着一个线程等待(没有做进一步的工作),直到一个事件发生。 我们可以使用这个术语来描述方法和方法调用:如果一个方法 阻塞方法 ,然后调用该方法可以阻止,等待某些事件发生之前,它返回给调用者。
我们可以使用一个队列阻塞操作线程间消息传递,缓冲在客户机/服务器网络通信频道消息传递将以同样的方式工作。 Java提供了 BlockingQueue与阻塞队列操作的接口。
在一个普通的 队列
add(e)
添加元素e
到的队列。remove()
删除并返回元素的队列,如果队列是空的或者抛出一个异常。
一个BlockingQueue扩展这个接口:
另外支持操作等待队列成为非空时检索一个元素,并等待空间可用在队列中存储一个元素。
put(e)
直到它可以添加元素e
队列的队列take()
直到它可以删除并返回元素的队列
当你使用 BlockingQueue
线程间消息传递,确保使用 put ()
和 take()
操作,不使用 。add ()
和 remove ()
我们将实现 生产者和消费者的设计模式 线程间消息传递。 生产者线程和消费者线程共享一个同步队列。 生产者把数据或请求在队列,消费者消除和处理它们。 一个或多个生产者和一个或多个消费者可能都从同一队列添加和删除条目。 这个队列必须安全的并发性。
Java提供了两种实现的 BlockingQueue
:
ArrayBlockingQueue
是一个固定大小的线程队列,使用一个数组表示。put
ting队列上的一个新项目将阻止如果队列已满。LinkedBlockingQueue
使用链表表示可生长的队列。 如果没有指定最大容量,队列不会填满,所以put
止。
像其他Java集合类,这些同步队列可以容纳任意类型的对象。 我们必须选择或设计一个类型的消息队列:我们将选择一个 不可变类型 因为我们在使用消息传递的目标是避免共享内存的问题。 生产者和消费者只会交流只能通过发送和接收消息,并将没有机会通过变异一个别名(mis)通信消息对象。
正如我们设计的操作在一个线程安全的ADT避免竞态条件,使客户执行所需的原子操作,我们将设计设计与相同的消息对象的要求。
银行账户的例子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FubBOzmk-1621439570669)(http://web.mit.edu/6.031/www/sp17/classes/22-queues/figures/message-passing-bank-account.png)]
我们的消息传递是第一个例子 银行账户的例子
每个现金机器,每个帐户是自己的模块,以及模块之间的交互是通过发送消息。 传入的消息到达队列。
我们设计了消息 get-balance
和 withdraw
说,每一个提款机撤出,以防止透支前检查帐户余额:
get-balance
if balance >= 1 then withdraw 1
但仍然是有可能的,交错的信息来自两个提款机,所以他们都是傻到以为他们可以安全地收回最后一美元的帐户只有1美元。
我们需要选择一个更好的原子操作: withdraw-if-sufficient-funds
将会是一个更好的操作不仅仅是 withdraw
.
用队列实现消息传递
这里有一个消息传递整数平方的模块:
/** Squares integers. */
public class Squarer {
private final BlockingQueue<Integer> in;
private final BlockingQueue<SquareResult> out;
// Rep invariant: in, out != null
/** Make a new squarer.
* @param requests queue to receive requests from
* @param replies queue to send replies to */
public Squarer(BlockingQueue<Integer> requests,
BlockingQueue<SquareResult> replies) {
this.in = requests;
this.out = replies;
}
/** Start handling squaring requests. */
public void start() {
new Thread(new Runnable() {
public void run() {
while (true) {
// TODO: we may want a way to stop the thread
try {
// block until a request arrives
int x = in.take();
// compute the answer and send it back
int y = x * x;
out.put(new SquareResult(x, y));
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
}).start();
}
}
传入的消息SquareResult
是整数; 平方电路知道它的工作就是平方这些数字,所以不需要任何进一步的细节。
传出的消息实例 SquareResult
:SquareResult
:
/** An immutable squaring result message. */
public class SquareResult {
private final int input;
private final int output;
/** Make a new result message.
* @param input input number
* @param output square of input */
public SquareResult(int input, int output) {
this.input = input;
this.output = output;
}
@Override public String toString() {
return input + "^2 = " + output;
}
}
我们可能会增加额外的观察员 SquareResult
所以客户可以检索输入数量和输出结果。
最后,这里有一个使用the squarer的主要方法:
public static void main(String[] args) {
BlockingQueue<Integer> requests = new LinkedBlockingQueue<>();
BlockingQueue<SquareResult> replies = new LinkedBlockingQueue<>();
Squarer squarer = new Squarer(requests, replies);
squarer.start();
try {
// make a request
requests.put(42);
// ... maybe do something concurrently ...
// read the reply
System.out.println(replies.take());
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
停止
如果我们要关闭 Squarer
所以它不再是等待新输入吗? 一个策略是 毒丸计划 :一个特殊的消息在队列中,信号的消费者信息结束工作。
关闭平方电路,因为它的输入消息仅仅是整数,我们必须选择一个神奇的毒药整数(每个人都知道0 = 0的平方是吗? 当然,他们永远不会要求0的平方? 不要用神奇的数字 )或使用null ( 不使用空 )。 相反,我们可能会改变的元素类型的请求队列的ADT:
SquareRequest = IntegerRequest + StopRequest
与操作:
input : SquareRequest → int
shouldStop : SquareRequest → boolean
当我们想要阻止平方电路,我们排队 SquareRequest
在哪里 shouldStop
返回 真正的
。
例如,在 Squarer.start ()
:
public void run() {
while (true) {
try {
// block until a request arrives
SquareRequest req = in.take();
// see if we should stop
if (▶▶A◀◀) { ▶▶B◀◀; }
// compute the answer and send it back
int x = ▶▶C◀◀;
int y = x * x;
out.put(new SquareResult(x, y));
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
还可以 中断 一个线程通过调用它 中断() 方法。 如果等待的线程被阻塞,阻塞的方法将抛出 打断例外 (这就是为什么我们必须try - catch异常几乎任何时候我们调用一个阻塞方法)。 如果线程没有阻塞, 打断了 标志将被设置。 线程必须处理打断例外年代和检查中断标志是否应该停止工作。 例如:
public void run() {
// handle requests until we are interrupted
while ( ! Thread.interrupted()) {
try {
// block until a request arrives
int x = in.take();
// compute the answer and send it back
int y = x * x;
out.put(new SquareResult(x, y));
} catch (InterruptedException ie) {
// stop
break;
}
}
}
-
线程安全与消息传递参数
线程安全参数与消息传递可能依赖于:
- 现有的线程安全的数据类型 同步队列。 这个队列是绝对绝对共享和可变,所以我们必须确保它是安全的并发性。
- 不变性 的消息或数据可能是多个线程同时访问。
- 监禁 数据生产者/消费者线程。 局部变量使用一个生产者或消费者对其他线程不可见,它只使用消息队列中相互沟通。
- 监禁 可变的消息或数据发送队列,但只有一个线程访问。 这个论点必须仔细铰接和实现。 假设一个线程有一些可变的数据发送给另一个线程。 如果第一个线程滴所有引用的数据就像烫手的山芋让他们送货到队列的其他线程,那么只有一个线程可以访问这些数据,从而排除并发访问。
相比,同步消息传递可以并发系统中每个模块更容易保持自己的线程安全的不变量。 我们不需要理由对多个线程访问共享数据如果模块之间传输数据,而不是使用一个线程安全的通信通道。
总结
-
而不是同步锁,消息传递系统同步共享通信信道,如流或一个队列。
** 可变的消息或数据发送队列,但只有一个线程访问。 这个论点必须仔细铰接和实现。 假设一个线程有一些可变的数据发送给另一个线程。 如果第一个线程滴所有引用的数据就像烫手的山芋让他们送货到队列的其他线程,那么只有一个线程可以访问这些数据,从而排除并发访问。相比,同步消息传递可以并发系统中每个模块更容易保持自己的线程安全的不变量。 我们不需要理由对多个线程访问共享数据如果模块之间传输数据,而不是使用一个线程安全的通信通道。
总结
- 而不是同步锁,消息传递系统同步共享通信信道,如流或一个队列。
- 线程与阻塞队列是一个有用的模式,消息传递在一个过程。