多线程 GAE Servlet 来处理并发用户

9
我想让我的GAE servlet支持多线程,这样同一个实例上的同一个servlet就可以同时处理来自不同用户的最多10个并发请求(在前端实例上,我认为最大线程数是10),在它们之间进行时间片轮转。
public class MyServlet implements HttpServlet {
    private Executor executor;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        if(executor == null) {
            ThreadFactory threadFactory = ThreadManager.currentRequestFactory();
            executor = Executors.newCachedThreadPoolthreadFactory);
        }

        MyResult result = executor.submit(new MyTask(request));

        writeResponseAndReturn(response, result);
    }
}

基本上,当GAE启动时,第一次收到对此servlet的请求时,会创建一个Executor并保存。然后每个新的servlet请求都使用该executor来生成新线程。显然,MyTask中的所有内容都必须是线程安全的。

我关心的是,这段代码是否真正实现了我所希望的功能。也就是说,这段代码是否创建了一个非阻塞的servlet,可以同时处理多个用户的多个请求?如果没有,为什么,我需要做什么来解决它?另外,在一般情况下,有没有其他的问题可能会被GAE专家发现?谢谢。


+1 - 我非常喜欢你思考问题和解决问题的方式。但是(不幸的是),GAE并不是这样工作的,currentRequestThreadFactory()方法也不完全符合人们根据其名称所期望的方式。我在下面发布了一个答案,希望能澄清该方法名称中的歧义。(它是current-Request-Thread-Factory,而不是current-RequestThread-Factory);) - Markus A.
只是出于好奇:您有什么特定的目标吗?我问这个问题,因为不久之前,我也在探究完全相同的问题。我希望能够使用GAE实现某种形式的长轮询推送通知,但事实证明,有许多其他附加问题将阻碍您完成此类操作。在允许您调整其请求调度和处理方式方面,GAE确实非常受限制。而且其中一些限制有点晦涩...但是,当然,这就是它能够如此快速和可扩展的原因... - Markus A.
3个回答

6
我认为你的代码可能无法运行。 doGet方法在由servlet容器管理的线程中运行。当请求到达时,会占用一个servlet线程,并且直到doGet方法返回才会释放。在你的代码中,executor.submit将返回一个Future对象。要获取实际结果,您需要在Future对象上调用get方法,它会阻塞直到MyTask完成其任务。只有在此之后,doGet方法才会返回,新的请求才能开始。
我不熟悉GAE,但根据他们的文档,您可以将Servlet声明为线程安全的,然后容器将并行地将多个请求分派给每个Web服务器。
<!-- in appengine-web.xml -->
<threadsafe>true</threadsafe>

5

你实际上提出了两个问题,我来回答一下:

1. 如何让我的AppEngine实例处理多个并发请求?

你只需要做两件事情:

  1. 在你的appengine-web.xml文件中添加语句<threadsafe>true</threadsafe>,这个文件可以在war\WEB-INF文件夹中找到。
  2. 确保你所有请求处理器中的代码都是线程安全的,即在doGet(...)doPost(...)等方法中只使用局部变量,或者确保同步对类或全局变量的所有访问。

这将告诉AppEngine实例服务器框架,你的代码是线程安全的,并且你允许它在不同的线程中多次调用所有的请求处理器来同时处理多个请求。注意: 据我所知,无法在每个servlet上设置这个属性。因此,所有的servlet都需要是线程安全的!

因此,你发布的执行器代码已经包含在每个AppEngine实例的服务器代码中,并且实际上从AppEngine为每个请求创建(或重用)的单独线程的run方法内部调用你的doGet(...)方法。基本上,doGet()已经是你的MyTask()

文档的相关部分在这里(虽然它并没有提供太多信息):https://developers.google.com/appengine/docs/java/config/appconfig#Using_Concurrent_Requests

2. 发布的代码对此(或任何其他)目的有用吗?

在其当前形式下,AppEngine不允许您创建和使用自己的线程来接受请求。它只允许您在doGet(...)处理程序内部使用currentRequestThreadFactory()方法创建线程,但仅用于此一请求的并行处理,而不能同时接受第二个请求(这发生在doGet()之外)。

名称currentRequestThreadFactory()在这里可能有点误导。它并不意味着它将返回RequestThreadscurrentFactory,即处理请求的线程。它的意思是它返回一个Factory,可以在currentRequest内创建Threads。因此,遗憾的是,实际上甚至不允许在当前doGet()执行的范围之外使用返回的ThreadFactory,就像你建议的那样,基于它创建一个Executor并将其保留在类变量中。
对于前端实例,在doGet()调用中创建的任何线程都会在doGet()方法返回时立即终止。对于后端实例,您被允许创建保持运行的线程,但由于您不被允许在这些线程内打开服务器套接字以接受请求,因此这些仍然不允许您自己管理请求处理。
您可以在此处找到有关在appengine servlet内部可以和不能做的更多详细信息:

Java Servlet环境 - 沙盒(特别是Threads部分)

为了完整起见,让我们看看如何使你的代码“合法”:

以下代码应该可以工作,但在处理多个请求并行时没有任何区别。这将仅由你的appengine-web.xml中的<threadsafe>true</threadsafe>设置来确定。因此,从技术上讲,这段代码效率很低,并且将基本线性的程序流程拆分成了两个线程。但它还是在这里:

public class MyServlet implements HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        ThreadFactory threadFactory = ThreadManager.currentRequestThreadFactory();
        Executor executor = Executors.newCachedThreadPool(threadFactory);

        Future<MyResult> result = executor.submit(new MyTask(request)); // Fires off request handling in a separate thread

        writeResponse(response, result.get()); // Waits for thread to complete and builds response. After that, doGet() returns
    }
}

既然您已经在处理当前请求的特定线程内部,那么您肯定应该避免“线程内嵌线程”,而应该采用以下方法:

public class MyServlet implements HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        writeResponse(response, new MyTask(request).call()); // Delegate request handling to MyTask object in current thread and write out returned response
    }
}

或者更好的方法是,将代码从MyTask.call()移动到doGet()方法中。;)

附注-关于您提到的10个同时servlet线程的限制:

这是一个(暂时的)设计决策,允许Google更轻松地控制服务器上的负载(特别是servlet的内存使用)。

您可以在此处找到更多有关这些问题的讨论:

这个话题也一直困扰着我,因为我坚信超轻量级的Servlet代码可以轻松处理数百甚至数千个并发请求,所以由于每个实例只能有10个线程的任意限制而不得不支付更多实例的费用,这让我感到有点恼火。但是阅读我上面发布的链接后,听起来他们已经意识到这一点,并正在努力寻找更好的解决方案。所以,让我们期待5月份Google I/O 2013的公告吧... :)


2

我同意ericsonMarkus A.的评估。

但是,如果出于某种原因(或其他情况),您希望按照使用代码片段作为起点的路径进行操作,我建议您将执行器定义更改为:

private static Executor executor;

这样它就能在多个实例中变为静态。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接