热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

RabbitMQ教程(六)Remoteprocedurecall(RPC)

文章目录Remoteprocedurecall(RPC)PrerequisitesWheretogethelpClientinterface客户端接口AnoteonRPC关于RPC


文章目录

    • Remote procedure call (RPC)
        • Prerequisites
        • Where to get help
      • Client interface 客户端接口
        • A note on RPC 关于RPC的说明
      • Callback queue 回调队列
        • **Message properties 消息属性**
      • Correlation Id 相关ID
      • Summary摘要
      • Putting it all together 整合代码
      • Production [Non-]Suitability Disclaimer


Remote procedure call (RPC)

(using the Java client)


Prerequisites

  This tutorial assumes RabbitMQ is installed and running on localhost on standard port (5672). In case you use a different host, port or credentials, connections settings would require adjusting.

Where to get help

  If you’re having trouble going through this tutorial you can contact us through the mailing list.


  In the second tutorial we learned how to use Work Queues to distribute time-consuming tasks among multiple workers.
  在第二篇教程中,我们学习了如何使用工作队列在多个工作人员之间分配耗时的任务。

  But what if we need to run a function on a remote computer and wait for the result? Well, that’s a different story. This pattern is commonly known as Remote Procedure Call or RPC.
  但是如果我们需要在远程计算机上运行一个函数并等待结果呢?嗯,这是一个不同的故事。此模式通常称为远程过程调用或RPC

  In this tutorial we’re going to use RabbitMQ to build an RPC system: a client and a scalable RPC server. As we don’t have any time-consuming tasks that are worth distributing, we’re going to create a dummy RPC service that returns Fibonacci numbers.
  在本教程中,我们将使用RabbitMQ构建RPC系统:客户端和可伸缩的RPC服务器。由于我们没有任何值得分发的耗时任务,我们将创建一个返回Fibonacci数字的虚拟RPC服务。


Client interface 客户端接口

  To illustrate how an RPC service could be used we’re going to create a simple client class. It’s going to expose a method named call which sends an RPC request and blocks until the answer is received:
  为了说明如何使用RPC服务,我们将创建一个简单的客户端类。它将公开一个名为call的方法,该方法 发送一个RPC请求并阻塞,直到收到答案为止:

FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();String result = fibonacciRpc.call("4");System.out.println( "fib(4) is " + result);

A note on RPC 关于RPC的说明

  Although RPC is a pretty common pattern in computing, it’s often criticised. The problems arise when a programmer is not aware whether a function call is local or if it’s a slow RPC. Confusions like that result in an unpredictable system and adds unnecessary complexity to debugging. Instead of simplifying software, misused RPC can result in unmaintainable spaghetti code.
  尽管RPC在计算中是一种非常常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是慢的RPC时,会出现问题。这样的混淆导致系统不可预测,并增加了调试的不必要的复杂性。错误使用RPC可以导致不可维护的代码,而不是简化软件开发。

Bearing that in mind, consider the following advice:
考虑到这一点,请考虑以下建议:

  Make sure it’s obvious which function call is local and which is remote.
  确保明显哪个函数调用是本地的,哪个是远程的。
  Document your system. Make the dependencies between components clear.
  记录您的系统。使组件之间的依赖关系变得清晰。
  Handle error cases. How should the client react when the RPC server is down for a long time?
  处理错误案例。当RPC服务器长时间停机时,客户端应该如何反应?
  
  When in doubt avoid RPC. If you can, you should use an asynchronous pipeline - instead of RPC-like blocking, results are asynchronously pushed to a next computation stage.
  如有疑问,请避免使用RPC。如果可以,您应该使用异步管道 - 而不是类似RPC的阻塞,将结果异步推送到下一个计算阶段。



Callback queue 回调队列

  In general doing RPC over RabbitMQ is easy. A client sends a request message and a server replies with a response message. In order to receive a response we need to send a ‘callback’ queue address with the request. We can use the default queue (which is exclusive in the Java client). Let’s try it:
  一般来说,通过RabbitMQ进行RPC很容易。客户端发送请求消息,服务器回复响应消息。为了接收响应,我们需要发送带有请求的“回调”队列地址。我们可以使用默认队列(在Java客户端中是独占的)。我们来试试吧:

callbackQueueName = channel.queueDeclare().getQueue();BasicProperties props = new BasicProperties.Builder().replyTo(callbackQueueName).build();channel.basicPublish("", "rpc_queue", props, message.getBytes());// ... then code to read a response message from the callback_queue ...

Message properties 消息属性

  The AMQP 0-9-1 protocol predefines a set of 14 properties that go with a message. Most of the properties are rarely used, with the exception of the following:
  AMQP 0-9-1协议预定义了一组带有消息的14个属性。大多数属性很少使用,但以下情况除外:
  
  deliveryMode: Marks a message as persistent (with a value of 2) or transient (any other value). You may remember this property from the second tutorial.
  deliveryMode:将消息标记为持久性(值为2)或瞬态(任何其他值)。您可能会记住第二个教程中的这个属性。
  
  contentType: Used to describe the mime-type of the encoding. For example for the often used JSON encoding it is a good practice to set this property to: application/json.
  contentType:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为:application / json是一种很好的做法。
  
  replyTo: Commonly used to name a callback queue.
  replyTo:通常用于命名回调队列。
  
  correlationId: Useful to correlate RPC responses with requests.
  correlationId:用于将RPC响应与请求相关联。


We need this new import:我们需要这个新的导入:

import com.rabbitmq.client.AMQP.BasicProperties;

Correlation Id 相关ID

  In the method presented above we suggest creating a callback queue for every RPC request. That’s pretty inefficient, but fortunately there is a better way - let’s create a single callback queue per client.
  在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。这是非常低效的,但幸运的是有更好的方法 - 让我们为每个客户端创建一个回调队列。

  That raises a new issue, having received a response in that queue it’s not clear to which request the response belongs. That’s when the correlationId property is used. We’re going to set it to a unique value for every request. Later, when we receive a message in the callback queue we’ll look at this property, and based on that we’ll be able to match a response with a request. If we see an unknown correlationId value, we may safely discard the message - it doesn’t belong to our requests.
  这引发了一个新问题,在该队列中收到响应后,不清楚响应属于哪个请求。那是在使用correlationId属性的时候 。我们将为每个请求将其设置为唯一值。稍后,当我们在回调队列中收到一条消息时,我们将查看此属性,并根据该属性,我们将能够将响应与请求进行匹配。如果我们看到未知的 correlationId值,我们可以安全地丢弃该消息 - 它不属于我们的请求。

  You may ask, why should we ignore unknown messages in the callback queue, rather than failing with an error? It’s due to a possibility of a race condition on the server side. Although unlikely, it is possible that the RPC server will die just after sending us the answer, but before sending an acknowledgment message for the request. If that happens, the restarted RPC server will process the request again. That’s why on the client we must handle the duplicate responses gracefully, and the RPC should ideally be idempotent.
  您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是失败并出现错误?这是由于服务器端可能存在竞争条件。虽然不太可能,但RPC服务器可能会在向我们发送答案之后,但在发送请求的确认消息之前死亡。如果发生这种情况,重新启动的RPC服务器将再次处理请求。这就是为什么在客户端上我们必须优雅地处理重复的响应,理想情况下RPC应该是幂等的。


Summary摘要

在这里插入图片描述
  Our RPC will work like this 我们的RPC将这样工作:

  For an RPC request, the Client sends a message with two properties: replyTo, which is set to a anonymous exclusive queue created just for the request, and correlationId, which is set to a unique value for every request.
  对于RPC请求,客户端发送带有两个属性的消息: replyTo,它被设置为仅为请求创建的匿名独占队列;以及correlationId,设置为每个请求的唯一值。
  The request is sent to an rpc_queue queue.
  请求被发送到rpc_queue队列。

  The RPC worker (aka: server) is waiting for requests on that queue. When a request appears, it does the job and sends a message with the result back to the Client, using the queue from the replyTo field.
  RPC worker(aka:server)正在等待该队列上的请求。当出现请求时,它会执行该作业,并使用来自replyTo字段的队列将带有结果的消息发送回客户端。

  The client waits for data on the reply queue. When a message appears, it checks the correlationId property. If it matches the value from the request it returns the response to the application.
  客户端等待回复队列上的数据。出现消息时,它会检查correlationId属性。如果它与请求中的值匹配,则返回对应用程序的响应。


Putting it all together 整合代码

The Fibonacci task 斐波纳契任务:

private static int fib(int n) {if (n == 0) return 0;if (n == 1) return 1;return fib(n-1) + fib(n-2);}

  We declare our fibonacci function. It assumes only valid positive integer input. (Don’t expect this one to work for big numbers, and it’s probably the slowest recursive implementation possible).
  我们声明我们的斐波那契函数。它假定只有有效的正整数输入。(不要指望这个适用于大数字,它可能是最慢的递归实现)。

  The code for our RPC server can be found here: RPCServer.java.
  我们的RPC服务器的代码可以在这里找到:RPCServer.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope;import java.io.IOException;
import java.util.concurrent.TimeoutException;public class RPCServer {private static final String RPC_QUEUE_NAME = "rpc_queue";private static int fib(int n) {if (n ==0) return 0;if (n == 1) return 1;return fib(n-1) + fib(n-2);}public static void main(String[] argv) {ConnectionFactory factory = new ConnectionFactory();factory.setHost("localhost");Connection connection = null;try {connection = factory.newConnection();final Channel channel = connection.createChannel();channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);channel.queuePurge(RPC_QUEUE_NAME);channel.basicQos(1);System.out.println(" [x] Awaiting RPC requests");Consumer consumer = new DefaultConsumer(channel) {@Overridepublic void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder().correlationId(properties.getCorrelationId()).build();String response = "";try {String message = new String(body,"UTF-8");int n = Integer.parseInt(message);System.out.println(" [.] fib(" + message + ")");response += fib(n);}catch (RuntimeException e){System.out.println(" [.] " + e.toString());}finally {channel.basicPublish( "", properties.getReplyTo(), replyProps, response.getBytes("UTF-8"));channel.basicAck(envelope.getDeliveryTag(), false);// RabbitMq consumer worker thread notifies the RPC server owner thread synchronized(this) {this.notify();}}}};channel.basicConsume(RPC_QUEUE_NAME, false, consumer);// Wait and be prepared to consume the message from RPC client.while (true) {synchronized(consumer) {try {consumer.wait();} catch (InterruptedException e) {e.printStackTrace(); }}}} catch (IOException | TimeoutException e) {e.printStackTrace();}finally {if (connection != null)try {connection.close();} catch (IOException _ignore) {}}}
}

  The server code is rather straightforward:服务器代码非常简单:

  As usual we start by establishing the connection, channel and declaring the queue.
  像往常一样,我们首先建立连接,通道和声明队列。

  We might want to run more than one server process. In order to spread the load equally over multiple servers we need to set the prefetchCount setting in channel.basicQos.
  我们可能希望运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要在channel.basicQos中设置 prefetchCount设置。

  We use basicConsume to access the queue, where we provide a callback in the form of an object (DefaultConsumer) that will do the work and send the response back.
  我们使用basicConsume来访问队列,我们​​以对象(DefaultConsumer)的形式提供回调,它将完成工作并发回响应。

  The code for our RPC client can be found here: RPCClient.java.
  我们的RPC客户端的代码可以在这里找到:RPCClient.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope;import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;public class RPCClient {private Connection connection;private Channel channel;private String requestQueueName &#61; "rpc_queue";public RPCClient() throws IOException, TimeoutException {ConnectionFactory factory &#61; new ConnectionFactory();factory.setHost("localhost");connection &#61; factory.newConnection();channel &#61; connection.createChannel();}public String call(String message) throws IOException, InterruptedException {final String corrId &#61; UUID.randomUUID().toString();String replyQueueName &#61; channel.queueDeclare().getQueue();AMQP.BasicProperties props &#61; new AMQP.BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName).build();channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));final BlockingQueue<String> response &#61; new ArrayBlockingQueue<String>(1);String ctag &#61; channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {&#64;Overridepublic void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {if (properties.getCorrelationId().equals(corrId)) {response.offer(new String(body, "UTF-8"));}}});String result &#61; response.take();channel.basicCancel(ctag);return result;}public void close() throws IOException {connection.close();}public static void main(String[] argv) {RPCClient fibonacciRpc &#61; null;String response &#61; null;try {fibonacciRpc &#61; new RPCClient();for (int i &#61; 0; i < 32; i&#43;&#43;) {String i_str &#61; Integer.toString(i);System.out.println(" [x] Requesting fib(" &#43; i_str &#43; ")");response &#61; fibonacciRpc.call(i_str);System.out.println(" [.] Got &#39;" &#43; response &#43; "&#39;");}}catch (IOException | TimeoutException | InterruptedException e) {e.printStackTrace();}finally {if (fibonacciRpc!&#61; null) {try {fibonacciRpc.close();}catch (IOException _ignore) {}}}}
}

  The client code is slightly more involved:客户端代码稍微复杂一些&#xff1a;

  We establish a connection and channel.
  我们建立了一个连接和渠道。

  Our call method makes the actual RPC request.
  我们的调用方法生成实际的RPC请求。

  Here, we first generate a unique correlationId number and save it - our implementation of handleDelivery in RpcConsumer will use this value to catch the appropriate response.
  在这里&#xff0c;我们首先生成一个唯一的correlationId 数并保存它 - 我们 在RpcConsumer中的handleDelivery实现将使用该值来捕获适当的响应。

  Then, we create a dedicated exclusive queue for the reply and subscribe to it.
  然后&#xff0c;我们为回复创建一个专用的独占队列并订阅它。

  Next, we publish the request message, with two properties: replyTo and correlationId.
  接下来&#xff0c;我们发布请求消息&#xff0c;其中包含两个属性&#xff1a; replyTo和correlationId。

  At this point we can sit back and wait until the proper response arrives.
  在这一点上&#xff0c;我们可以坐下来等待正确的响应到来。

  Since our consumer delivery handling is happening in a separate thread, we’re going to need something to suspend the main thread before the response arrives. Usage of BlockingQueue is one possible solutions to do so.
Here we are creating ArrayBlockingQueue with capacity set to 1 as we need to wait for only one response.
  由于我们的消费者交付处理是在一个单独的线程中进行的&#xff0c;因此我们需要在响应到来之前暂停主线程。使用BlockingQueue是一种可能的解决方案。这里我们创建了ArrayBlockingQueue &#xff0c;容量设置为1&#xff0c;因为我们只需要等待一个响应。

  The handleDelivery method is doing a very simple job, for every consumed response message it checks if the correlationId is the one we’re looking for. If so, it puts the response to BlockingQueue.
  该handleDelivery方法是做一个很简单的工作&#xff0c;对每一位消费响应消息它会检查的correlationID 是我们要找的人。如果是这样&#xff0c;它会将响应置于BlockingQueue。

  At the same time main thread is waiting for response to take it from BlockingQueue.
  同时主线程正在等待响应从BlockingQueue获取它。

  Finally we return the response back to the user.
  最后&#xff0c;我们将响应返回给用户。

Making the Client request 发出客户请求&#xff1a;

RPCClient fibonacciRpc &#61; new RPCClient();System.out.println(" [x] Requesting fib(30)");String response &#61; fibonacciRpc.call("30");System.out.println(" [.] Got &#39;" &#43; response &#43; "&#39;");fibonacciRpc.close();

  Now is a good time to take a look at our full example source code (which includes basic exception handling) for RPCClient.java and RPCServer.java.
  现在是查看RPCClient.java和RPCServer.java的完整示例源代码&#xff08;包括基本异常处理&#xff09;的 好时机。

Compile and set up the classpath as usual (see tutorial one):
像往常一样编译和设置类路径&#xff08;参见教程一&#xff09;&#xff1a;

javac -cp $CP RPCClient.java RPCServer.java

Our RPC service is now ready. We can start the server:
我们的RPC服务现已准备就绪。我们可以启动服务器&#xff1a;

java -cp $CP RPCServer# &#61;> [x] Awaiting RPC requests

To request a fibonacci number run the client:
要请求斐波纳契数&#xff0c;请运行客户端&#xff1a;

java -cp $CP RPCClient# &#61;> [x] Requesting fib(30)

  The design presented here is not the only possible implementation of a RPC service, but it has some important advantages:
  此处介绍的设计并不是RPC服务的唯一可能实现&#xff0c;但它具有一些重要优势&#xff1a;

  If the RPC server is too slow, you can scale up by just running another one. Try running a second RPCServer in a new console.
  如果RPC服务器太慢&#xff0c;您可以通过运行另一个服务器来扩展。尝试在新控制台中运行第二个RPCServer。

  On the client side, the RPC requires sending and receiving only one message. No synchronous calls like queueDeclare are required. As a result the RPC client needs only one network round trip for a single RPC request.
  在客户端&#xff0c;RPC只需要发送和接收一条消息。不需要像queueDeclare这样的同步调用 。因此&#xff0c;对于单个RPC请求&#xff0c;RPC客户端只需要一次网络往返。

  Our code is still pretty simplistic and doesn’t try to solve more complex (but important) problems, like:
  我们的代码仍然相当简单&#xff0c;并不试图解决更复杂&#xff08;但重要&#xff09;的问题&#xff0c;例如&#xff1a;

How should the client react if there are no servers running?
如果没有运行服务器&#xff0c;客户应该如何反应&#xff1f;

Should a client have some kind of timeout for the RPC?
客户端是否应该为RPC设置某种超时&#xff1f;

If the server malfunctions and raises an exception, should it be forwarded to the client?
如果服务器出现故障并引发异常&#xff0c;是否应将其转发给客户端&#xff1f;

Protecting against invalid incoming messages (eg checking bounds, type) before processing.
在处理之前防止无效的传入消息&#xff08;例如检查边界&#xff0c;键入&#xff09;。

  If you want to experiment, you may find the management UI useful for viewing the queues.
  如果您想进行实验&#xff0c;您可能会发现管理UI对于查看队列非常有用。


Production [Non-]Suitability Disclaimer

  Please keep in mind that this and other tutorials are, well, tutorials. They demonstrate one new concept at a time and may intentionally oversimplify some things and leave out others. For example topics such as connection management, error handling, connection recovery, concurrency and metric collection are largely omitted for the sake of brevity. Such simplified code should not be considered production ready.

  Please take a look at the rest of the documentation before going live with your app. We particularly recommend the following guides: Publisher Confirms and Consumer Acknowledgements, Production Checklist and Monitoring.

Getting Help and Providing Feedback

  If you have questions about the contents of this tutorial or any other topic related to RabbitMQ, don’t hesitate to ask them on the RabbitMQ mailing list.

Help Us Improve the Docs ❤️

  If you’d like to contribute an improvement to the site, its source is available on GitHub. Simply fork the repository and submit a pull request. Thank you!


推荐阅读
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • RingBuffer,或者说CircularBuffer,是一个长度固定的缓冲区,当从一端插入元素超过指定的最大长度时,缓冲区另一端的元素 ... [详细]
  • rabbitmq杂谈
    rabbitmq中的consumerTag和deliveryTag分别是干啥的,有什么用?同一个会话,consumerTag是固定的可以做此会话的名字,deliveryTag每次接 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • Centos下安装memcached+memcached教程
    本文介绍了在Centos下安装memcached和使用memcached的教程,详细解释了memcached的工作原理,包括缓存数据和对象、减少数据库读取次数、提高网站速度等。同时,还对memcached的快速和高效率进行了解释,与传统的文件型数据库相比,memcached作为一个内存型数据库,具有更高的读取速度。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • AFNetwork框架(零)使用NSURLSession进行网络请求
    本文介绍了AFNetwork框架中使用NSURLSession进行网络请求的方法,包括NSURLSession的配置、请求的创建和执行等步骤。同时还介绍了NSURLSessionDelegate和NSURLSessionConfiguration的相关内容。通过本文可以了解到AFNetwork框架中使用NSURLSession进行网络请求的基本流程和注意事项。 ... [详细]
  • C++ STL复习(13)容器适配器
    STL提供了3种容器适配器,分别为stack栈适配器、queue队列适配器以及priority_queue优先权队列适配器。不同场景下,由于不同的序列式 ... [详细]
  • 给出一群女孩的重量和颜值和她们的朋友关系现在有一个舞台ab是朋友bc是朋友ac就是朋友给出最大承重可以邀请这些女孩来玩对于每一个朋友团体全邀请or邀请一个or不邀请问能邀请的女孩的 ... [详细]
  • java线程池的实现原理源码分析
    这篇文章主要介绍“java线程池的实现原理源码分析”,在日常操作中,相信很多人在java线程池的实现原理源码分析问题上存在疑惑,小编查阅了各式资 ... [详细]
  • QuestionThereareatotalofncoursesyouhavetotake,labeledfrom0ton-1.Somecoursesmayhaveprerequi ... [详细]
  • 一、死锁现象与递归锁进程也是有死锁的所谓死锁:是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作 ... [详细]
author-avatar
mobiledu2502869373
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有