Perl多线程(1):解释器线程的特性

线程简介

线程(thread)是轻量级进程,和进程同样,都能独立、并行运行,也由父线程建立,并由父线程所拥有,线程也有线程ID做为线程的惟一标识符,也须要等待线程执行完毕后收集它们的退出状态(好比使用join收尸),就像waitpid对待子进程同样。编程

线程运行在进程内部,每一个进程都至少有一个线程,即main线程,它在进程建立以后就存在。线程很是轻量级,一个进程中能够有不少个线程,它们全都在进程内部并行地被调度、运行,就像多进程同样。每一个线程都共享了进程的不少数据,除了线程本身所须要的数据,它们都直接使用父进程的,好比同一个线程解释器、同一段代码、同一段要处理的数据等,但每一个线程都有本身的调用栈(call stack)空间,用来存放某些临时数据、某些状态、某些返回信息等数组

因而,如今开始从多进程编程转入到多线程编程。安全

Perl本身的线程

有些系统不原生支持线程模型(如某些Unix系统),在Perl 5.8中,Perl提供了属于本身的线程模型:解释器线程(interpreter thread, ithreads)。固然,Perl也依旧支持老线程。多线程

  • Thread模块提供老式线程
  • threads模块提供Perl解释器线程

能够经过如下代码来检测操做系统是否支持老式线程、解释器线程。async

#!/usr/bin/perl

BEGIN{
    use Config;
    if ($Config{usethreads}) {print "support old thread\n";}
    if ($Config{useithreads}) {print "support interpreter threads\n";}
}

或者简单的使用perldoc来检查这两个模块是否存在,通常来讲安装Perl的时候就会自动安装它们:函数

$ perldoc Thread
$ perldoc threads

threads模块提供的是面向对象的解释器线程,能够直接使用new方法来建立一个线程,使用其它方法来维护线程。默认状况下,Perl解释器线程不会在线程之间共享数据和状态信息(也就是说数据是线程本地的),若是想要共享,可使用threads::shared。而老式线程模块Thread的线程默认是自动在线程间共享数据的,且于解释器线程相互隔离,在编写复杂程序时这可能会很复杂。性能

实际上,在Perl的解释器线程被建立的时候,会将父线程中全部的变量都拷贝到本身的空间中使之成为私有变量,这样各线程之间就互相隔离了,而且自动实现了线程安全。若是想要在同进程的不一样线程之间共享数据,须要专门使用threads::shared模块将变量共享出去,这样每一个线程都能访问到这个变量。测试

解释器线程这样的行为对编写多线程来讲很是的友好,可是这会影响Perl的线程性能,特别是父线程中数据量较大的时候,建立线程的成本以及内存占用上是很是昂贵的。因此,在使用Perl解释器线程的时候,应当尽可能在数据量还小的时候建立子线程。操作系统

建立线程

Perl线程在不少方面都像fork出来的进程同样,可是在建立线程上,它更像是一个子程序。线程

建立线程的方式有两种:create/new、async,create和new是等价的别名,这3种(其实是两种)建立线程的方式除了语法上不一样,在线程执行上是彻底一致的。

建立线程的标准方法是使用createnew方法(它们是等价的别名),而且给它一个子程序或子程序引用或匿名子程序,这表示建立一个新线程去运行这个子程序。

例如:

use threads;

my $thr = threads->create(\&sub1);

sub sub1 {
    print("In Child Thread\n");
}

这里main线程建立了子线程运行sub1子程序,建立完成后,main线程继续向下运行。

若是子程序要传递参数,直接在create/new的参数位上传递便可。

use threads;

sub threadsub {
    my $self = threads->self;
}

my $thr1 = threads->create(\&threadsub, 'arg1', 'arg2');
# 或者使用new
my $thr2 = threads->new(\&threadsub, @args);

若是使用async建立线程,那么给async一个语句块,就像匿名子程序同样。

use threads;

my $thr = async {
    ... some code ...
}

这表示新建一个子线程来运行代码块中的代码。

至于选择create/new仍是选择async来建立新线程,随意。可是若是建立多个线程的话,使用create/new比较方便。并且,create/new也同样能建立新线程执行匿名子程序。

my $thr1 = new threads \&threadsub, $arg1;
my $thr2 = new threads \&threadsub, $arg2;
my $thr3 = new threads \&threadsub, $arg3;

# create执行匿名子程序
my $thr = threads->create( sub {...} );

线程标识

因为咱们可能会建立不少个线程,咱们须要区分它们。

第一种方式是经过给不一样线程的子程序传递不一样参数的方式来区分不一样的线程

例如:

my $thr1 = threads->create(\&mysub,"first");
my $thr1 = threads->create(\&mysub,"second");
my $thr1 = threads->create(\&mysub,"third");

sub mysub {
    my $thr_num = shift @_;
    print "I am thread $thr_num\n";
    ...
}

第二种方式是获取threads模块中的线程对象,线程对象中包含了线程的id属性。经过类方法threads->self()能够获取当前线程对象,有了线程对象,能够经过tid()对象方法获取这个线程对象的ID,固然还能够直接使用类方法threads->tid()来获取当前线程对象的ID。

my $myself = threads->self;
my $mytid = $myself->tid();

# 或
my $mytid = threads->tid();

对于已知道tid的线程,可使用类方法threads->object($tid)去获取这个tid的线程对象。注意,object()只能获取正激活的线程对象,对于joined和detached线程(join和detach见下文),都返回undef,不只如此,对于没法收集的线程对象,object()都返回undef,例如收集$tid不存在的线程。

线程对象的ID是从0开始计算的,而后每新建一个子线程,ID就加1.0号线程就是每一个进程建立时的main线程,main线程再建立一个新子线程,这个新子线程的ID就是1。

能够比较两个线程是不是同一个线程,使用equal()方法(或者重载的==!=符号)便可,它们都基于线程ID进行比较:

print "Equal\n" if $self->equal($thr);
print "Equal\n" if $self == $thr;

线程状态和join、detach

Perl中的线程其实是一个子程序代码块,它可能会有子程序的返回值,因此父线程须要接收子线程的返回值。不只如此,就像父进程须要使用wait/waitpid等待子进程并为退出的子进程收尸同样,父线程也须要等待子线程退出并为子线程收尸(作最后的清理工做)。为线程收尸是很重要的,若是只建立了几个运行时间短的子线程,那么操做系统可能会自动为子线程收尸,但建立了一大堆的子线程,操做系统可能不会给咱们什么帮助,咱们要本身去收尸。

join()方法的功能就像waitpid同样,当父线程中将子线程join()后,表示将子线程从父线程管理的一个线程表中加入到父线程监控的另外一个列表中(实际上并不是如此,只是修改了进程的状态而已,稍后解释),这个列表中的全部线程是该父线程都须要等待的。因此,将join()方法的"加入"含义看做是加入到了父线程的某个监控列表中便可

join()作三件事:

  • 等待子线程退出,等待过程当中父线程一直阻塞
  • 子线程退出后,为子线程收尸(OS clean up)
  • 若是子线程有返回值,则收集返回值
    • 而返回值是有上下文的,根据标量(scalar)、列表(list)、空(void)上下文,应该在合理的上下文中使用返回值
    • 线程上下文相关,稍后解释

例如:

use threads;

my ($thr) = threads->create(\&sub1);

# join,父进程等待、收尸、收集返回值
my @returnData = $thr->join();

print 'thread returned: ', join('@', @returnData), "\n";

sub sub1 {
    # 返回值是列表
    return ('fifty-six', 'foo', 2);
}

join的三件事中,若是不想要等待子线程执行完毕,可使用detach(),它将子线程脱离父线程,父线程再也不阻塞等待。由于已经脱离,父线程也将再也不为子线程收尸(子线程在执行完毕的时候本身收尸),父线程也没法收集子线程的返回值致使子线程的返回值被丢弃。固然,父子关系还在,只不过当父线程退出时,子线程会继续运行,这时才会摆脱父线程成为孤儿线程,这就像daemon进程(本身成立进程组)和父进程同样。

刚才使用"父线程监控的另外一个列表"来解释join的行为,这是不许确的。实际上,线程有6种状态(这些状态稍后还会解释):

  • detached(和joined是互斥的状态)
  • joined(和detached是互斥的状态)
  • finished execution(执行完但尚未返回,还没退出),实际上是running状态刚结束,能够被join的阶段(joinable)
  • exit
  • died
  • creation failed

当执行detach()后,线程的状态就变成detached,当执行join()后,线程的状态就变成joined。detached线程能够看做是粗略地看做是脱离了父线程,它没法join,父线程也不会对其有等待、收尸、收集返回值行为,只有进程退出时detached线程才默默被终止(detached状态的线程也依然是线程,是进程的内部调度单元,进程终止,线程都将终止)

例如:

use threads;

sub mysub {
    #alarm 10;
    for (1..10){
        print "I am detached thread\n";
        sleep 1;
    }
}

my $thr1 = threads->new(\&mysub)->detach();

print "main thread will exit in 2 seconds\n";
sleep 2;

上面的子线程会被detach,父线程继续运行,在2秒后进程终止,detach后的子线程会被默默终止。

更细分一点,一个线程正常执行子程序到结束能够划分为几个过程:

  • 1.线程入口,开始执行子程序。执行子程序的阶段称为running状态
  • 2.子程序执行完毕,但尚未返回,这个时候是running刚结束状态,也是前文提到的finished execution状态
    • 若是这个线程未被detach,从这个状态开始,这个线程能够被join(除非是detached线程),也就是joinable状态,父线程在这个阶段再也不阻塞
  • 3.线程执行完毕
    • 若是这个线程被join,则父线程对该线程收尸并收集该线程的返回值
    • 若是这个线程被detach,则这个线程本身收尸并退出
    • 若是这个线程未join也未detach,则父线程不会收尸,而且在进程退出时报告相关消息

因此从另外一种分类角度上看,线程能够分为:active、joined、detached三种状态。其中detached线程已被脱离,因此不算是active线程,joined已经表示线程的子程序已经执行完毕了,也不算是active线程,只有unjoined、undetached线程才算是active线程,active包括running、joinable这两个过程。

整个线程的状态和过程能够参考下图:

上面一直忽略了一种状况,线程在join以前就已经运行完毕了。例如:

my $thr1 = threads->create(\&sub1);

# 父线程睡5秒,给子线程5秒的执行时间
sleep 5;
$thr1->join();

子线程先执行完毕,可是父线程还没对它进行join,这时子线程一直处于joinable的状态,其实这个时候子线程基本已经失去意义了,它的返回值和相关信息都保存在线程栈(或调用栈call stack),当父线程对其进行join()的时候,天然能从线程栈中找到返回值或某些信息的栈地址从而取得相关数据,也能从如今开始对其进行收尸行为。

实际上,解释器线程是一个双端链表结构,每一个线程节点记录了本身的属性,包括本身的状态。而main线程中则包含了全部子线程的一些统计信息:

typedef struct {
    /* Structure for 'main' thread
     * Also forms the 'base' for the doubly-linked list of threads */
    ithread main_thread;
 
    /* Protects the creation and destruction of threads*/
    perl_mutex create_destruct_mutex;
 
    UV tid_counter;        # tid计数器,可知道当前已经建立了几个线程
    IV joinable_threads;   # 可join的线程
    IV running_threads;    # 正在运行的线程
    IV detached_threads;   # detached状态的线程
    IV total_threads;      # 总线程数
    IV default_stack_size; # 线程的默认栈空间大小
    IV page_size;
} my_pool_t;

检查线程的状态

使用threads->list()方法能够列出未detach的线程,列表上下文下返回这些线程列表,标量上下文下返回数量。它有4种形式:

threads->list()  # 返回non-detach、non-joined线程
threads->list(threads::all)  # 同上
threads->list(threads::running)  # non-detached、non-joined的线程对象,即正在运行的线程
threads->list(threads::joinable)  # non-detached、non-joined但joinable的线程对象,即已完成子程序执行但未返回的线程

因此,list()只能统计未detach、未join的线程,::running返回的是正在运行子程序主体的线程,::joinable返回的是已完成子程序主体的线程,::all返回的是它们之和。

此外,咱们还能够直接去测试线程的状态:

$thr->is_running()
若是该线程正在运行,则返回true

$thr->is_joinable()
若是该线程已经完成了子程序的主体(即running刚结束),且未detach未join,换句话说,这个线程是joinable,因而返回true

$thr->is_detached()
threads->is_detached()
测试该线程或线程自身是否已经detach

线程的上下文环境

由于解释器线程其实是一个运行的子程序,而父线程可能须要收集子线程的返回值(join()的行为),而返回值在不一样上下文中有不一样的行为。

仍之前面的示例来解释:

use threads;

# my(xxx):列表上下文
# my xxx:标量上下文
my ($thr) = threads->create(\&sub1);

# join,父进程等待、收尸、收集返回值
# @arr:列表上下文
my @returnData = $thr->join();

print 'thread returned: ', join('@', @returnData), "\n";

sub sub1 {
    # 返回值是列表
    return ('fifty-six', 'foo', 2);
}

上面的建立子线程后,父线程将这个子线程join()时一直阻塞,直到子线程运行完毕,父线程将子线程的返回值收集到数组@returnData中。由于子程序的返回值是一个列表,因此这里join的上下文是列表上下文。

其实,子线程的上下文是在被建立出来的时候决定的,这样子程序中能够出现wantarray()。因此,在线程被建立时、在join时上下文都要指定:前者决定线程入口(即子程序)执行时所处何种上下文,后者决定子程序返回值环境。这两个地方的上下文不必定要同样,例如建立线程的时候在标量上下文环境下,表示子程序在标量上下文中执行,而join的时候能够放在空上下文表示丢弃子程序的返回值。

容许三种上下文:标量上下文、列表上下文、空上下文。

对于join时的上下文没什么好解释的,根据上下文环境将返回值进行赋值而已。可是建立线程时的上下文环境须要解释。有显式和隐式两种方式来指定建立线程时的上下文。

隐式上下文天然是经过所处上下文环境来暗示。

# 列表上下文建立线程
my ($thr) = threads->create(...);

# 标量上下文建立线程
my $thr = threads->create(...);

# 空上下文建立线程
threads->create(...);

显式上下文是在create/new建立线程的时候,在第一个参数位置上指定经过一个hash引用来指定上下文环境。也有两种方式:

# 列表上下文建立线程
my $thr = threads->create({ 'context' => 'list' }, \&sub1)
my $thr = threads->create({ 'list' => 1 }, \&sub1)

# 标量上下文建立线程
my $thr = threads->create({ 'context' => 'scalar' }, \&sub1)
my $thr = threads->create({ 'scalar' => 1 }, \&sub1)

# 空上下文建立线程
my $thr = threads->create({ 'context' => 'void' }, \&sub1)
my $thr = threads->create({ 'void' => 1 }, \&sub1)

线程的退出

正常状况而且大多状况下,线程都应该经过子程序return的方式退出线程。可是也有其它可能。

threads->exit()
线程自身能够调用threads->exit()以便在任什么时候间点退出。这会使得线程在标量上下文返回undef,在列表上下文返回空列表。若是是在main线程中调用`threads->exit()`,则等价于exit(0)

threads->exit(status)
在线程中调用时,等价于threads->exit(),退出状态码status会被忽略。在main线程中调用时,等价于exit(status)

die()
直接调用die函数会让线程直接退出,若是设置了 $SIG{__DIE__} 的信号处理机制,则调用该处理方法,像通常状况下的die同样

exit(status)
在线程内部调用exit()函数会致使整个程序终止(进程中断),因此不建议在线程内部调用exit()。可是能够改变exit()终止整个程序的行为,见下面几个设置

use threads 'exit'=>'threads_only'
全局设置,使得在线程内部调用exit()时不会致使整个程序终止,而是只让线程终止。因为这是全局设置,因此不是很建议设置。另外,该设置对main线程无效

threads->create({'exit'=>'thread_only},\&sub1)
在建立线程的时候,就设置该线程中的exit()只退出当前线程

$thr->set_thread_exit_only(bool)
修改当前线程中的exit()效果。若是给了true值,则线程内部调用exit()将只退出该线程,给false值,则终止整个程序。对main线程无效

threads->set_thread_exit_only(bool)
类方法,给true值表示当前线程中的exit()只退出当前线程。对main线程无效

最可能须要的退出方式是threads->exit()threads->exit(status),若是对于线程中严重错误的问题,则可能须要的是die或exit()来终止整个程序。

线程暂时放弃CPU

有时候可能想要让某个线程短暂地放弃CPU转交给其它线程,可使用yield()类方法。

threads->yield();

yield()和操做系统平台有关,不必定真的有效,且出让CPU的时间也必定能保证。

线程的信号处理

在threads定义的解释器线程中,能够在线程内部定义信号处理器(signal handler),并经过$thr->kill(SIGNAME)的方式发送信号(对于某些自动触发的信号处理,稍后解释),kill方法会返回线程对象以便进行链式调用方法。

例如,在main线程中发送SIGKILL信号,并在线程内部处理这个信号。

use threads;

sub thr_func {
    # Thread signal handler for SIGKILL
    $SIG{KILL} = sub { 
        print "Caught Signal: SIGKILL\n";
        threads->exit();
    }
    ...
}

my $thr = threads->create('thr_func');

...

# send SIGKILL to terminate thread, then detach 
# it so that it will clean up automatically
$thr->kill('KILL')->detach();

其实,threads对于线程信号的处理方式是模拟的,不是真的从操做系统发送信号(操做系统发送的信号是给进程的,会被main线程捕获)。模拟的逻辑也很简单,经过threads->kill()发送信号给指定线程,而后经过调用子程序中的%SIG中的signal handler便可。

例如上面的示例中,咱们想要发送KILL信号,但这个信号不是操做系统发送的,而是模拟了一个KILL信号,表示是要终止线程的执行,因而调用线程中的SIGKILL对应的signal handler,仅此而已。

可是,有些信号是某些状况下自动触发的,好比在线程中使用一个alarm计时器,在计时结束时它会发送SIGALRM信号给进程,这会使得整个进程都退出,而不只仅是那个单独的线程,这显然不是咱们所期待的结果。

实际上,操做系统所发送的信号都会在main线程中被捕获。因此若是想要处理上面的问题,只需在main线程中定义对应操做系统发送的信号的signal handler,并在handler中从新使用threads->kill()发送这个信号给指定线程,从而间接实现"信号->线程"的机制

例如,在线程中使用alarm并在计时结束的时候中止该线程。

use threads qw(yield);

# 带有计时器的线程
my $thr = threads->create(
    sub {
        threads->yield();
        eval {
            $SIG{ALRM} = sub {die "Timeout";};
            alarm 10;
            ... do somework ...
        };
        if ( $@ =~ /Timeout/) {
            warn "thread timeout";
        }
    }
);

$SIG{ALRM} = sub { $thr->kill('ALRM') };
... main thread continue ...
相关文章
相关标签/搜索