【问题标题】:Is there a thread-safe way to print in Perl?在 Perl 中是否有线程安全的打印方式?
【发布时间】:2014-04-14 15:24:28
【问题描述】:

我目前有一个脚本,它启动线程以在多个目录上执行各种操作。我的脚本的 sn-p 是:

#main
sub BuildInit {

    my $actionStr = "";
    my $compStr   = "";

    my @component_dirs;
    my @compToBeBuilt;
    foreach my $comp (@compList) {
        @component_dirs = GetDirs($comp);    #populates @component_dirs
    }

    print "Printing Action List: @actionList\n";

    #---------------------------------------
    #----   Setup Worker Threads  ----------
    for ( 1 .. NUM_WORKERS ) {
        async {
            while ( defined( my $job = $q->dequeue() ) ) {
                worker($job);
            }
        };
    }

    #-----------------------------------
    #----   Enqueue The Work  ----------
    for my $action (@actionList) {
        my $sem = Thread::Semaphore->new(0);
        $q->enqueue( [ $_, $action, $sem ] ) for @component_dirs;

        $sem->down( scalar @component_dirs );
        print "\n------>> Waiting for prior actions to finish up... <<------\n";
    }

    # Nothing more to do - notify the Queue that we're not adding anything else
    $q->end();
    $_->join() for threads->list();

    return 0;
}

#worker
sub worker {
    my ($job) = @_;
    my ( $component, $action, $sem ) = @$job;
    Build( $component, $action );
    $sem->up();
}

#builder method
sub Build {

    my ( $comp, $action ) = @_;
    my $cmd     = "$MAKE $MAKE_INVOCATION_PATH/$comp ";
    my $retCode = -1;

    given ($action) {
        when ("depend") { $cmd .= "$action >nul 2>&1" }    #suppress output
        when ("clean")  { $cmd .= $action }
        when ("build")  { $cmd .= 'l1' }
        when ("link")   { $cmd .= '' }                     #add nothing; default is to link
        default { die "Action: $action is unknown to me." }
    }

    print "\n\t\t*** Performing Action: \'$cmd\' on $comp ***" if $verbose;

    if ( $action eq "link" ) {

        # hack around potential race conditions -- will only be an issue during linking
        my $tries = 1;
        until ( $retCode == 0 or $tries == 0 ) {
            last if ( $retCode = system($cmd) ) == 2;      #compile error; stop trying
            $tries--;
        }
    }
    else {
        $retCode = system($cmd);
    }
    push( @retCodes, ( $retCode >> 8 ) );

    #testing
    if ( $retCode != 0 ) {
        print "\n\t\t*** ERROR IN $comp: $@ !! ***\n";
        print "\t\t*** Action: $cmd -->> Error Level: " . ( $retCode >> 8 ) . "\n";

        #exit(-1);
    }

    return $retCode;
}

我希望线程安全的print 语句是:print "\n\t\t*** Performing Action: \'$cmd\' on $comp ***" if $verbose; 理想情况下,我希望得到这个输出,然后每个执行$action 的组件,会输出相关的块。但是,这显然现在不起作用 - 输出大部分是交错的,每个线程都吐出它自己的信息。

例如:

ComponentAFile1.cpp
ComponentAFile2.cpp
ComponentAFile3.cpp
ComponentBFile1.cpp
ComponentCFile1.cpp
ComponentBFile2.cpp
ComponentCFile2.cpp
ComponentCFile3.cpp
... etc.

我考虑过使用反引号执行系统命令,并将所有输出捕获在一个大字符串或其他东西中,然后在线程终止时将其全部输出。但是这个问题是(a)它看起来超级低效,并且(b)我需要捕获stderr

任何人都可以找到将每个线程的输出分开的方法吗?

说明: 我想要的输出是:

ComponentAFile1.cpp
ComponentAFile2.cpp
ComponentAFile3.cpp
-------------------  #some separator
ComponentBFile1.cpp
ComponentBFile2.cpp
-------------------  #some separator
ComponentCFile1.cpp
ComponentCFile2.cpp
ComponentCFile3.cpp
... etc.

【问题讨论】:

    标签: multithreading perl thread-safety stdout


    【解决方案1】:

    为确保您的输出不会中断,对 STDOUT 和 STDERR 的访问必须互斥。这意味着在一个线程开始打印和完成打印之间,不允许其他线程打印。这可以使用 Thread::Semaphore[1] 来完成。

    捕获输出并一次性打印出来可以减少线程持有锁的时间。如果您不这样做,您将有效地使您的系统成为单线程系统,因为每个线程在一个线程运行时尝试锁定 STDOUT 和 STDERR。

    其他选项包括:

    1. 为每个线程使用不同的输出文件。
    2. 在每行输出前添加作业 ID,以便稍后对输出进行排序。

    在这两种情况下,您只需将其锁定很短的时间。


    1. # Once
      my $mutex = Thread::Semaphore->new();  # Shared by all threads.
      
      
      # When you want to print.
      $mutex->down();
      print ...;
      STDOUT->flush();
      STDERR->flush();
      $mutex->up();
      

      # Once
      my $mutex = Thread::Semaphore->new();  # Shared by all threads.
      STDOUT->autoflush();
      STDERR->autoflush();
      
      
      # When you want to print.
      $mutex->down();
      print ...;
      $mutex->up();
      

    【讨论】:

    • 第二个建议可以使用$thr-&gt;tid()
    • @Zaid,Job ID 会更有用,但是是的,thread id 也会做得很好。
    • 这非常有用,谢谢。我已经更新了我的原始问题,并澄清了我想要做什么 - 我认为你的回答暗示了如何做到这一点,特别是在前面加上一些标识符,然后对输出进行排序。如果我误解了,请告诉我。
    • 不需要澄清;我已经理解正确了。
    • IPC::Run3(和 IPC::Run)可以轻松捕获 STDOUT 和 STDERR。
    【解决方案2】:

    如果$sem-&gt;down 试图将信号量计数器降低到零以下,您可以利用它的阻塞行为,如perldoc perlthrtut 中所述:

    如果down() 试图将计数器减到零以下,它会阻塞 直到计数器足够大。


    所以这是一个可以做的事情:

    使用计数器 1 初始化所有线程共享的信号量

    my $sem = Thread::Semaphore->new( 1 );
    

    将线程计数器传递给workerBuild

    for my $thr_counter ( 1 .. NUM_WORKERS ) {
        async {
            while ( defined( my $job = $q->dequeue() ) ) {
                worker( $job, $thr_counter );
            }
        };
    }
    
    sub worker {
       my ( $job, $counter ) = @_;
    
       Build( $component, $action, $counter );
    }
    

    -&gt;down-&gt;up 里面Build(别无他处)

    sub Build {
        my ( $comp, $action, $counter ) = @_;
    
        ... # Execute all concurrently-executed code here
    
        $sem->down( 1 << ( $counter -1 ) );
    
        print "\n\t\t*** Performing Action: \'$cmd\' on $comp ***" if $verbose;
    
        # Execute all sequential 'chunks' here
    
        $sem->up( 1 << ( $counter - 1) );
    }
    

    通过使用线程计数器对信号量计数器进行左移,保证线程不会相互踩踏:

    +-----------+---+---+---+---+
    | Thread    | 1 | 2 | 3 | 4 |
    +-----------+---+---+---+---+
    | Semaphore | 1 | 2 | 4 | 8 |
    +-----------+---+---+---+---+
    

    【讨论】:

    • 第二个... 包含打印语句和/或对system 的调用,因此您的...up 的顺序错误,这意味着只有一个线程将在一次。
    • 我看不出有任何理由在信号量块之前不执行system 调用。
    • 因为你最终可能会得到output from system from thread 1\noutput from system from thread 2\nmore output from system from thread 1\n,他在问如何避免。他的线程的输出是交错的,他想阻止这种情况发生。
    • 点得好。我非常专注于传输移位的想法,以至于我忘记了system 分享STDOUT。哦,好吧
    【解决方案3】:

    过去,我通过创建IO 线程并使用它来序列化文件访问来以不同的方式解决此问题。

    例如

    my $output_q = Thread::Queue -> new();
    
    sub writer {
        open ( my $output_fh, ">", $output_filename );
        while ( my $line = $output_q -> dequeue() ) {
            print {$output_fh} $line; 
        }
        close ( $output_fh );
     }
    

    在线程内,'print' by:

    $output_q -> enqueue ( "text_to_print\n"; );
    

    带或不带包装器 - 例如如果它们要进入日志,则用于时间戳语句。 (您可能希望在排队时打上时间戳,而不是在实际打印时打上时间戳)。

    【讨论】:

      猜你喜欢
      • 2023-03-11
      • 2011-12-14
      • 1970-01-01
      • 2013-09-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-04
      • 1970-01-01
      • 2012-08-21
      相关资源
      最近更新 更多