繁体   English   中英

同时运行100,000个进程

[英]Running 100,000 processes concurrently

我正在模拟一个银行系统,我有100,000个交易要运行。 每种类型的事务都实现了runnable,我可以发生各种类型的事务。

transactions是一个Runnables数组。

理想情况下,以下代码可以解决我的问题:

for (Transaction transaction : transactions) {
    new Thread(transaction).start();
}

但是,显然java.lang.OutOfMemoryError: unable to create new native thread在尝试启动100,000个线程时必然会发生java.lang.OutOfMemoryError: unable to create new native thread线程。

接下来我尝试实现一个ExecutorService来创建一个线程池来管理我的100,000个runnables。

ExecutorService service;
int cpus = Runtime.getRuntime().availableProcessors();
// cpus == 8 in my case
service = Executors.newFixedThreadPool(cpus);

for (Transaction transaction : transactions) {
    service.execute(transaction);
}

尝试这种方法时,长进程会“占用”JVM。 例如,一种类型的事务需要30-60秒才能执行。 在分析应用程序时,在长事务发生时不允许其他线程运行。

在这种情况下,线程6在其单个事务完成之前不允许任何其他线程运行

在这种情况下,线程6在其处理完成之前不允许任何其他线程运行。

所以我的问题是:如何在不遇到内存问题的情况下尽快运行100,000个事务? 如果ExecutorService是答案,那么如何阻止非常长的事务占用JVM并允许其他事务同时运行?

编辑:

我故意强制某些类型的事务发生30-60秒,以确保我的线程程序正常工作。 每个事务锁定一个帐户,有10个帐户。 这是我的方法,它占用了JVM :(由run()调用)

public void makeTransaction() {
    synchronized(account) {
        long timeStarted = System.nanoTime();
        long timeToEnd = timeStarted + nanos;

        this.view = new BatchView(transactionNumber, account.getId());

        this.displayView();

        while(true) {
            if(System.nanoTime() % 1000000000 == 0) {
                System.out.println("batch | " + account.getId());
            }

            if(System.nanoTime() >= timeToEnd) {
                break;
            }
        }
    }
}

每次运行此事务时,只有一个帐户被锁定,剩下9个应该可用于处理的帐户。 为什么JVM不再处理任何线程,而是挂起直到这个长事务完成?

以下是项目缩小版的链接,用于演示问题: 项目

在分析应用程序时,在长事务发生时不允许其他线程运行。

最有可能的是,此任务使用的是单线程资源。 即写入ti的方式可防止并发使用。

如何在不遇到内存问题的情况下尽快运行100,000个事务?

如果事务是CPU绑定的,则应该有一个与您拥有的CPU数量大小相同的池。

如果事务依赖于数据库,则应该查看对它们进行批处理以更有效地利用数据库。

如果ExecutorService是答案,那么如何阻止非常长的事务占用JVM并允许其他事务同时运行?

使交易更短。 如果你的任务运行超过几毫秒,你应该弄清楚为什么这么长时间。 我首先看一下网络/ IO必须如何使用和分析任务。 大多数交易(如果您有大量交易)应该在0.01秒左右或理想情况下远远不够。

您应该非常小心地考虑如何使用共享资源。 如果您的任务使用相同的资源太多,您可能会发现多线程不会更快,甚至更慢。

您的应用程序的问题是很快所有线程都会为同一个帐户分配一个事务,然后除了一个线程之外的所有线程都必须等待。 您可以在下面的屏幕截图中看到这一点,我暂停了应用程序。 线程池-1-thread-3当前正在处理ID为19的Account对象的事务(此id不是您的帐户ID,而是Eclipse分配的唯一对象ID),并且所有其他线程都在等待锁定帐户对象。 帐户对象是您的id为9的帐户对象。

调试器的屏幕截图

为什么会这样? 在事务853中,一个线程启动第一个长时间运行的事务(对于帐户9)。 其他线程继续处理其他事务。 但是,当任何线程到达帐户9的另一个事务时,它将必须停止并等待。 事务857,861和862也用于帐户9,并且每个都阻塞一个线程,所以此时我的所有线程都被阻塞(在我的四核上)。

怎么解决这个? 这取决于您的使用案例。

如果在您的实际程序中保证给定帐户X没有传入事务,只要有另一个事务为X帐户运行,您就不需要更改任何内容。

如果您的帐户数量与线程数量相比非常大,则问题变得更加不可能,因此您可能决定使用它。

如果您的帐户数量相对较少(假设可能少于一百个左右),您应该(如彼得所说)每个帐户都有一个(无限运行)线程,每个帐户都有自己的事务队列。 这可能会更有效,因为线程不需要在共享队列上“战斗”。

另一种解决方案是实施某种形式的“偷工作”。 这意味着每当一个线程被阻塞时,它就会寻找其他一些工作要做。 要实现这一点,首先需要能够检查线程是否可以获取给定帐户的锁定。 使用Java synchronized这是不可能的,所以你需要像ReentrantLock.tryLock()这样的东西。 您还需要能够从每个线程直接访问事务队列,所以我猜你不能在这里使用ExecutorService ,但需要自己实现事务处理(使用LinkedBlockingQueue )。

现在每个线程都会在循环中轮询队列中的事务。 首先,它尝试使用tryLock()获取相应帐户的锁定。 如果失败,请将事务添加到(特定于线程)列表,从队列中获取下一个事务,然后尝试执行此事务,直到找到可以处理的事务。 事务完成后,首先查看列表中的当前可能处理事务,然后再从全局队列中提取另一个事务。 代码可以大致如下:

public BlockingQueue<Transaction> queue = ...; // the global queue for all threads

public void run() {
   LinkedList<Transaction> myTransactions = new LinkedList<>();
   while (true) {
     Transaction t = queue.take();
     while (!t.getLock().tryLock()) {
        myTransactions.add(t);
     }
     try {
       // here we hold the lock for t
       t.makeTransaction();
     } finally {
       t.getLock().unlock();
     }

     Iterator<Transaction> iter = myTransactions.iterator();
     while (iter.hasNext()) {
       t = iter.next();
       if (t.getLock().tryLock()) {
         try {
           t.makeTransaction();
         } finally {
           t.getLock().unlock();
         }
         iter.remove();
       }
     }
   }
 }

请注意,这仍然至少包含以下您可能想要解决的问题:

  • 当一个线程在queue.take()挂起时,它不会检查其列表中的事务是否可用。 因此,如果存在queue为空的时间段(例如,在处理结束时),则可能存在未处理的列表中的事务。
  • 如果某些线程持有大量的锁,剩下的线程可能会占用他们现在无法处理的大量事务,因此它们只会填充其本地列表,从而耗尽全局队列。 释放锁时,许多事务可能已从全局队列中删除,从而在线程可以执行的工作之间产生不平衡(某些线程可能处于空闲状态,而其他线程仍在处理其长时间积压的事务)。

如果你不能为它们获取锁定,那么更简单的替代方法可能是put()事务放入队列(最后),但这会使它们以非常任意的顺序执行(这也可能在上述解决方案中发生,但是也许不是那么极端)。

编辑:更好的解决方案可能是将队列附加到每个帐户而不是特定于线程的列表。 然后,只要发现此帐户被阻止,线程就会将事务添加到相应帐户的队列中。 当一个线程完成帐户X的一个事务时,它应首先查看帐户X的队列,如果在那里添加了任何事务,则在查看全局列表之前。

根据您的硬件计算可以并行处理事务的工作线程数非常重要。 可用于调整线程池大小的公式很少

对于CPU绑定应用程序

N * U或(N + 1)* U.

对于IO绑定应用程序

N * U *(1 + W / C)

其中N - 处理器数量U - 目标CPU利用率W - 等待时间C - 计算时间

例如,如果您的应用程序使用50%CPU并且您有8个内核。 然后为CPU绑定应用程序实现高效的多线程

8 *(0.5)= 4

如果您有4个线程,那么所有核心都将得到有效处理。 一些支持超线程的公猪会发生这种变化

如果您使用笔记本电脑甚至是16核桌面,那么在单独的线程中执行100,000次调用很难。 您将需要一个网格或一组服务器来优化执行此操作。

但是,您仍然可以通过在callback执行任何事务操作来扩展它。 您的吞吐量会增加。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM