11

如果执行 CPU 密集型任务,我认为每个内核有一个线程是最佳的。如果您有一个 4 核 CPU,您可以运行 4 个 CPU 密集型子例程实例而不会受到任何惩罚。例如,我曾经在一个四核 CPU 上实验性地运行了四个 CPU 密集型算法实例。每个过程最多四倍的时间没有减少。在第五个实例中,所有实例都花费了更长的时间。

阻塞操作的情况是什么?假设我有一个包含 1,000 个 URL 的列表。我一直在做以下事情:

(请不要介意任何语法错误,我只是模拟了这个)

my @threads;
foreach my $url (@urlList) {    
     push @threads, async {
         my $response = $ua->get($url);
         return $response->content;   
     }
}

foreach my $thread (@threads) {
    my $response = $thread->join;
    do_stuff($response); 
}

我基本上启动了与 URL 列表中的 URL 一样多的线程。如果有一百万个 URL,那么一百万个线程将被启动。这是最优的,如果不是最优线程数是多少?对于任何可以等待的阻塞 I/O 操作(读取文件、数据库查询等),使用线程是一种很好的做法吗?

相关奖金问题

出于好奇,Perl 线程是否与 Python 和 GIL 一样工作?使用 python 来获得多线程的好处并利用所有内核执行 CPU 密集型任务,您必须使用多处理。

4

4 回答 4

13

出于好奇,Perl 线程是否与 Python 和 GIL 一样工作?使用 python 来获得多线程的好处并利用所有内核来执行 CPU 密集型任务,您必须使用多处理。

不,但结论是一样的。Perl没有一个大锁来保护跨线程的解释器。相反,它为每个不同的线程都有一个重复的解释器。由于一个变量属于一个解释器(并且只有一个解释器),因此默认情况下线程之间不会共享任何数据。当变量被显式共享时,它们被放置在一个共享解释器中,该解释器代表其他线程序列化对共享变量的所有访问。除了这里其他人提到的内存问题外,Perl 中的线程还存在一些严重的性能问题,以及对可以共享的数据类型以及可以用它做什么的限制(有关更多信息,请参阅perlthrtut) .

结果是,如果您需要并行化大量 IO 并且可以使其成为非阻塞的,那么您将从事件循环模型中获得比线程更多的性能。如果您需要并行化无法实现非阻塞的东西,那么多进程可能比 perl 线程更幸运(一旦您熟悉了这种代码,也更容易调试)。

也可以组合这两种模型(例如,一个主要是单进程事件的应用程序,它使用POE::Wheel::RunAnyEvent::Run将某些昂贵的工作传递给子进程,或者一个多进程应用程序有一个事件的父级管理非事件的子级,或者一个节点集群类型设置,其中您有许多预先分叉的事件网络服务器,父级只是accepts 并将 FD 传递给它的子级)。

不过,没有灵丹妙药,至少现在还没有。

于 2013-06-24T15:29:23.607 回答
4

从这里: http: //perldoc.perl.org/threads.html

内存消耗

在大多数系统上,频繁且持续地创建和销毁线程会导致 Perl 解释器的内存占用不断增加。虽然启动线程然后 ->join() 或 ->detach() 很简单,但对于长期存在的应用程序,最好维护一个线程池,并使用队列将它们重用于所需的工作通知待处理工作的线程。该模块的 CPAN 分发版包含一个简单示例 (examples/pool_reuse.pl),说明了可重用线程池的创建、使用和监控。

于 2013-06-24T14:40:08.197 回答
3

让我们看看你的代码。我发现它存在三个问题:

  1. 简单的第一个:您使用->content而不是->decoded_content(charset => 'none').

    ->content返回原始的 HTML 响应主体,如果没有标头中的信息来解码它(例如,它可能被压缩),它是无用的。它有时会起作用。

    ->decoded_content(charset => 'none')给你实际的反应。它总是有效的。

  2. 您处理订单请求中的响应。这意味着您可能会在响应等待服务时被阻止。

    最简单的解决方案是将响应放在Thread::Queue::Any对象中。

    use Thread::Queue::Any qw( );
    
    my $q = Thread::Queue::Any->new();
    
    my $requests = 0;
    for my $url (@urls) {
       ++$requests;
       async {
          ...
          $q->enqueue($response);
       };
    }
    
    while ($requests && my $response = $q->dequeue()) {
       --$requests;
       $_->join for threads->list(threads::joinable);
       ...
    }
    
    $_->join for threads->list();
    
  3. 您创建了许多只使用一次的线程。

    这种方法存在大量开销。一个常见的多线程实践是创建一个持久工作线程池。这些工人执行任何需要完成的工作,然后继续下一个工作而不是退出。作业到池而不是特定线程,以便作业可以尽快启动。除了消除线程创建开销之外,这还允许控制一次运行的线程数。这对于 CPU 密集型任务非常有用。

    但是,您的需求有所不同,因为您使用线程来执行异步 IO。线程创建的 CPU 开销不会对您产生太大影响(尽管它可能会导致启动滞后)。内存相当便宜,但您使用的内存仍然远远超过您的需要。线程确实不适合这项任务。

    执行异步 IO 的系统要好得多,但它们不一定很容易从 Perl 中获得。但是,在您的特定情况下,您最好避免使用线程并使用Net::Curl::Multi按照概要中的示例,您将获得一个非常快速的引擎,能够以非常少的开销发出并行 Web 请求。

    在我的前雇主,我们已经切换到 Net::Curl::Multi 来处理高负载的关键任务网站,而且我们喜欢它。

    如果您想限制对周围代码的更改,很容易创建一个创建 HTTP::Response 对象的包装器。(对我们来说就是这种情况。)请注意,方便地引用底层库(libcurl)是有帮助的,因为 Perl 代码是底层库之上的一个薄层,因为文档非常好,并且因为它记录了所有您可以提供的选项。

于 2013-06-24T17:51:44.400 回答
2

您可能只想考虑一个非阻塞用户代理。我喜欢Mojo::UserAgent,它是Mojolicious套件的一部分。您可能想看一个我为另一个问题模拟的非阻塞爬虫示例。

于 2013-06-24T14:10:12.370 回答