什么是替代多线程的最佳选择?

8

目前我们的应用程序(Java)在使用线程。但是一次会创建大约1000个或更多的线程。这些线程应该处理数据并将其存储在数据库中。

这消耗了更多的内存和I/O。

有什么最好的替代方法吗? 可扩展性、一致性和性能是主要要求。


6
关于线程的一个基本误解是“越多越好”。通常情况下,事实并非如此,相反,你需要尽可能少的线程,不要超过你拥有的 CPU 数量。即使你的进程受 I/O 绑定,并且因此大部分线程都在等待 I/O,拥有更多线程也可能毫无益处,因为它会减慢你正在进行 I/O 操作的系统。 - biziclop
5
你是否已经在使用线程池?我希望你不要直接创建1000个线程。 - Ravi K Thapliyal
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - biziclop
2
有限的线程池?并行处理的工作量不能超过CPU核心数。如果你没有一千个核心,那么创建一千个线程就没有实际意义了。不过,纤程可能值得你去研究一下。 - Petr Janeček
@RaviThapliyal 目前我们没有使用线程池。根据所需的线程数量,我们直接创建它们。 - Anil Kumar
显示剩余4条评论
4个回答

18

你尝试过线程池了吗?线程池由适量的线程组成(足以使用所有处理器,但不多),重复利用线程(再次减少开销)并同时执行大量任务

这里有一个小例子来给你一个想法。

ExecutorService executor = Executors.newFixedThreadPool(5);
Runnable job = new Runnable() {
     public void run() {
        // do some work
     }
}
executor.execute(job);
如果您查看ScheduledThreadPoolExecutor,您会发现有很多特性可用于执行和调度作业。

如果我使用大小为100的线程池,将会创建100个数据库连接,这与不使用线程池时的连接数量相同。请问是否有任何方法可以创建数据库连接并让所有线程共享使用? - Anil Kumar
在我看来,你应该开一个新的问题,并详细说明你正在做什么以及为什么要这样做。同时,对于大多数情况,你的线程池应该最多有两倍于服务器核心数量的线程。如果你的数据库运行在同一台服务器上,则线程数应该更少。 - Cephalopod

14

尝试了解一下Actor模型

Actor模型是一种并发编程模型,其中工作负载被分配给并行运行的实体,称为actors

它是一种没有共享状态的模型,actors是隔离的,信息可以以消息的形式流动。

玩家接收这些消息,并且只能通过操作消息中的数据(计算或处理数据),向其他玩家发送消息或创建新actor来进行反应。

该模型是互斥锁和线程的高级抽象,消除了开发人员的复杂性,主要是由Ericsson于1973年在Erlang上设计用于构建高度可用的竞争电信系统。

演员是非常轻量级的并发实体。他们使用基于事件驱动的接收循环异步地处理消息。对消息进行模式匹配是表达演员行为的一种方便方式。它们提高了抽象级别,使编写、测试、理解和维护并发和/或分布式系统变得更加容易。您可以关注工作流程——消息在系统中的流动方式——而不是线程、锁和套接字IO等低级原语。
在Java / Scala中,您可以找到基于此演员模型构建的Akka框架(链接1)

1
演员(Actors)往往比纯线程(pure threads)慢得多。在许多情况下可能并不重要。但是当原始速度至关重要时,需要牢记这一点。它们也倾向于有点啰嗦,并且在我看来更难以进行故障排除。 - WestCoastProjects
但是,就其在Spark中的使用而言呢?如果我没记错的话,Spark使用了actor模型。 - eliasah
2
Spark不会在每个JVM上运行许多线程。所谓的“许多”是指数百个或更多。只有达到这个级别,性能差异才真正显现出来。 - WestCoastProjects

9
使用线程池。这样,您可以定义要运行的线程数。每个新任务都被放入队列中等待,直到线程完成旧任务并因此空闲以处理新任务。
这是可扩展的,因为您可以定义要运行的线程数。您可以在具有少量处理核心的设备上选择少量线程以节省内存并减少同步开销,或者在具有多个核心的设备上选择许多线程。例如,如果您在具有4个核心和超线程的设备上运行此操作,则选择8个线程;如果您在具有48个硬件线程的设备上运行,则选择48个线程。
性能通常比为每个任务启动一个新线程要好,因为启动和终止线程确实会产生相当大的开销。线程池重用线程,因此没有这种开销。
它也是一致的,因为Java标准库中有一个线程池实现。

1
作为附注,线程池大小是一个棘手的问题,虽然使用尽可能多的线程来并行运行是一个好的起点,但为了最大化其效果,您应该监视您的应用程序并相应地进行调整。例如,如果您正在执行大量I/O操作,则稍微大一点的线程池可能更好;如果您使用并行GC并且开销过大,请保留几个内核供GC使用等等。 - biziclop
希望在运行时能够更改池参数。 - Skaperen
1
@AnilKumar 总的来说,这可能会更快。想象一下从瓶子里倒出水,哪种方式更好:垂直握着还是倾斜握着?当你把瓶子垂直倾斜时,水流速度更快,但是气泡上升需要花费很多时间。这里也是类似的。重要的是在更改之前和之后测量性能,因为每个问题都是不同的。 - biziclop
正如biziclop所说:这取决于你在做什么。如果这1000个线程执行了很多CPU密集型任务,那么使用线程池可能会更快,因为你的硬件一次只能运行有限数量的线程。如果你的软件线程比硬件线程多,那么CPU无法同时运行所有线程,而是尽可能地运行尽可能多的线程,然后切换到非活动线程。这些上下文切换非常昂贵。因此,较少的线程可能会加速你的任务。 - Dakkaron
1
@AnilKumar 我的建议是从最简单的解决方案开始,并测量它是否足够。我会首先创建某种服务,您可以将任务提交到该服务中,而不是直接创建线程来使用该服务重写应用程序。然后,您可以使用线程池实现该服务。测量性能,如果无法获得足够的性能,则转向略微复杂的解决方案,例如MQs。 - biziclop
显示剩余5条评论

6
我认为你不需要多线程的替代方案,只需要更高效的线程实现。 Quasar 添加了纤程(即轻量级线程)到JVM中,您可以创建数百万个而不是仅有数百个,因此您可以获得与异步框架相同的性能,同时不放弃语言中提供的线程抽象和常规命令控制流构造(顺序,循环等)。
它还将JVM / JDK的线程和纤程统一到一个通用的strand接口下,因此它们可以无缝地互操作,并提供了java.util.concurrent的移植到这个统一的概念。这也意味着您的移植工作将是最小的(如果有的话)。
在纤维或常规线程之上,Quasar还提供了完整的Erlang风格的演员(请参见此处与Akka进行比较),阻止Go-like通道和数据流编程,因此您可以选择最适合您技能和需求的并发编程范例,而不必强制执行任何一个。
它还提供了对流行和标准技术的绑定(作为Comsat项目的一部分),因此您可以保留代码资产,因为移植工作量将是最小的(如果有的话)。出于同样的原因,您也可以轻松地选择退出。
目前Quasar已经支持Java 7和8的绑定,在Pulsar项目下支持Clojure,并且支持JetBrains的Kotlin。由于基于JVM字节码仪器,只要有一个集成模块,Quasar就可以与任何JVM语言一起使用,并提供构建附加模块的工具。
从Java9开始,将自动进行仪器化,不再需要任何集成模块。

如何使用纤程?有任何教程吗? - Anil Kumar
以上链接的Quasar、Pulsar和Comsat文档非常全面且作为教程很好;它们还链接了Quasar Maven原型和对应的Gradle模板,Comsat Maven原型和对应的Gradle模板,Comsat示例,Comsat Ring Leiningen模板,���Akka/Play移植的quasar-stocks和一个Comsat-jOOQ示例。在blog中可以找到大量信息和教程,例如Screencast等。Google Quasar/Pulsar和Comsat论坛也是很好的信息来源。 - circlespainter
3
看起来这是你的雇主开发的技术。你可能应该声明你的从属关系:http://meta.stackexchange.com/questions/57497/limits-for-self-promotion-in-answers#59302 - James_pic
1
谢谢您指出来,是的,我是Quasar/Pulsar/Comsat开发团队的一员。 - circlespainter

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