线程数据共享

在介绍Perl解释器线程的时候一直强调,Perl解释器线程在被创建出来的时候,将从父线程中拷贝数据到子线程中,使得数据是线程私有的,并且数据是线程隔离的。如果真的想要在线程间共享数据,需要显式使用threads::shared模块来扩展threads模块的功能。这个模块必须在先导入了threads模块的情况下使用,否则threads::shared模块里的功能都将没效果。

use threads;
use threads::shared;

要共享数据,只需使用threads::shared模块的share方法即可,也可以直接将数据标记上:shared属性的方式来共享。例如:

my $answer = 43;
my @arr = qw(1 2 3);
share($answer);
share(@arr); my $answer :shared = 43;
my @arr :shared = qw(1 2 3);
my %hash :shared = (one=>1, two=>2, three=>3);

使用share()和:shared的区别在于后面这种标记的方式是在编译期间完成的。另外,使用:shared属性标记的方式可以直接共享引用类型的数据,但是share()不允许,它使用prototype限制了只允许接收变量类型的参数,可以使用&share()调用子程序的方式禁用prototype:

my $aref = &share([1 2 3]);

例如:

#!/usr/bin/perl
use strict;
use warnings;
use 5.010; use threads;
use threads::shared; my $foo :shared = 1;
my $bar = 1;
threads->create(
sub {
$foo++;
$bar++;
say "new thread: \$foo=$foo"; # 2
say "new thread: \$bar=$bar"; # 2
}
)->join(); say "main thread: \$foo=$foo"; # 2
say "main thread: \$bar=$bar"; # 1

什么数据会共享

并非所有数据都可以共享,只有普通变量、数组、hash以及已共享数据的引用可以共享。也就是:

Ordinary scalars
Array refs
Hash refs
Scalar refs
Objects based on the above

例如:

use threads;
use threads::shared; my $var = 1; # 未共享数据
my $svar :shared = 2; # 共享标量
my @arr :shared = qw(perl python shell); # 共享数组
my %hash :shared; # 共享hash my $thr = threads->new(\&mysub); sub mysub {
$hash{a} = 1; # 成功
$hash{b} = $var; # 成功:$var是普通变量
$hash{c} = \$svar; # 成功:\$svar是已共享标量
$hash{d} = @arr; # 成功:普通数组
$hash{e} = \@arr; # 成功:已共享数组的引用 # $hash{f} = \$var; # 失败并die:$var未共享标量的引用
# $hash{g} = []; # 失败:未共享数组的引用
# $hash{h} = {a=>1};# 失败:未共享hash的引用
} $thr->join(); # join后文解释
while( my ($key, $value) = each %hash ){
say "$key => $value";
}

如果共享hash或array类型,那么里面的所有元素都对外可见,但并不意味着里面的元素是共享的。共享hash/array和共享它们里面的元素是独立的,共享hash/array只意味着共享它们自身,但里面的元素会暴露。反之,可以直接共享某个元素,但hash/array自身不共享。(经测试,数组的元素无法共享,hash的元素可正常共享)

数据共享的问题:竞态

当多个线程在某一时刻都访问或修改同一个共享数据时,就会出现竞态问题(race condition),它意味着多线程的数据竞争问题。

例如:

use threads;
use threads::shared; my $x :shared = 1;
my $thr1 = threads->new(\&sub1);
my $thr2 = threads->new(\&sub2); $thr1->join();
$thr2->join();
print "$x\n"; sub sub1 { my $foo = $x; $x = $foo + 1; }
sub sub1 { my $bar = $x; $x = $bar + 1; }

执行上面的程序,结果可能会输出2或3,因为两个线程可能都取得x=1的值,也可能后一个线程取得前一个线程加法之后的值。

之所以会发生竞态问题,是因为对多个线程对同个数据的访问和修改时间点无法保证,这个时候数据是线程不安全的,也可称之为线程数据不同步。

所以,要解决数据竞态问题,必须对共享数据的步骤进行协调,比如修改数据时必须保证只能有一个线程去修改,这可以通过锁的方式来实现。

变量锁

threads::shared模块中提供了一个lock()方法,用来将共享数据进行独占锁定,被锁定的数据无法被其它线程修改,直到释放锁其它线程才可以获取锁并修改数据。

例如:

use threads;
use threads::shared; my $var :shared = 3; sub mysub {
...
lock($var);
...
} # 锁在这里自动被释放

没有unlock()这样直接释放锁的方法,而是在退出当前作用域的时候自动释放锁,就像词法变量一样。

另外,锁住hash和数组的时候,仅仅只是锁住它们自身,但lock()无法去锁hash/array中的元素。所以:

lock $myhash{'abc'};    # Error
lock %myhash; # Correct

如果真想基于容器中元素进行锁定,可以使用线程信号量模块Thread::Semaphore,后文会介绍。

另外,lock()是可以递归的,在退出最外层lock()的作用域时释放锁。且递归时重复锁定同一个变量是幂等的。例如:

my $x :shared;
doit();
sub doit {
{
{
lock($x); # Wait for lock
lock($x); # 没任何作用,因为已经锁过一次了
{
lock($x); # 没任何作用,因为已经锁过一次了
{
lock($x); # 没任何作用,因为已经锁过一次了
lockit_some_more();
}
}
} # *** Implicit unlock here ***
}
}
sub lockit_some_more {
lock($x); # 没任何作用,因为已经锁过一次了
} # Nothing happens here

死锁问题

使用锁来协调共享数据的步骤能解决竞态问题,但是如果协调不好,很容易出现死锁问题。死锁是指两个或更多进程/线程互相等待锁的释放,导致每一个进程/线程都无法释放,从而出现无限等待的死循环问题。

例如:

use threads;
use threads::shared; my $x :shared = 4;
my $y :shared = 'foo'; my $thr1 = threads->create(
sub {
lock($x);
sleep 3;
lock($y);
}
); my $thr2 = threads->create(
sub {
lock($y);
sleep 3;
lock($x);
}
); sleep 10;

上面的例子只要运行,两个线程将会出现死锁问题,因为thr1线程锁住$x、thr2锁住$y后,thr1申请$y的锁将等待thr2先释放,同理thr2申请$x的锁将等待thr1先释放。于是出现了互相等待的僵局,谁也不会也无法释放。

解决死锁最简单且最佳的方式是保证所有线程以相同的顺序去锁住每一个数据。例如,所有线程都以先锁住$x,再锁住$y,最后锁住$z的方式去执行代码。

另一个避免死锁的解决方案是尽可能让锁住共享数据的时间段变短,这样出现僵局的几率就会小很多。

但是这两种方式很多时候都派不上用场,因为需要用到锁的情况可能会比较复杂。下面介绍几种方式。

线程队列(Thread::Queue)

(Thread::Queue)队列数据结构(FIFO)是线程安全的,它保证了某些线程从一端写入数据,另一些线程从另一端读取数据。只要队列已经满了,写入操作就自动被阻塞直到有空间支持写操作,只要队列空了,读取操作就会自动阻塞直到队列中有数据可读。这种模式自身就保证了线程安全性。

在Perl中要使用线程队列,需要使用Thread::Queue模块,使用方式很简单。如下示例:

#!/usr/bin/perl
use strict;
use warnings; use threads;
use Thread::Queue; # 创建一个线程队列
my $DataQueue = Thread::Queue->new(); # 创建线程
my $thr = threads->new(
sub {
# 在循环中读取队列
while (my $DataElement = $DataQueue->dequeue()) {
print "Poped $DataElement off the queue\n";
}
}
); # 向队列中写入一个数据
$DataQueue->enqueue(12);
sleep 1; # 再次写入队列3个数据
$DataQueue->enqueue('a','b','c');
sleep 3; # 关闭队列,让读取端不再阻塞
$DataQueue->enqueue(undef); # 等待子线程并为其收尸
$thr->join();

关于Thread::Queue模块的用法,参见:https://www.cnblogs.com/f-ck-need-u/p/10422293.html

Thread::Semaphore

Thread::Semaphore实现了线程信号量,可以通过up()和down()来操作信号量,up()表示增加信号量的值,down()表示减信号量的值,按照锁的角度来看这是申请锁的操作,只要减法操作后信号量的值为负数,这次减法操作就会被阻塞,就像被锁住。

通过Thread::Semaphore的new()方法来创建一个信号量,如果不给任何参数,则默认创建一个信号量值为1的信号量。如果给new()一个整数值N,则表示创建一个信号量值为N的信号量。

使用信号量实现锁机制的示例:

#!/usr/bin/perl

use 5.010;
use threads;
use Thread::Semaphore; # 新建一个信号量
my $sem = Thread::Semaphore->new(); # 全局共享变量
my $gbvar :shared = 0; my $thr1 = threads->create(\&mysub, 1);
my $thr2 = threads->create(\&mysub, 2);
my $thr3 = threads->create(\&mysub, 3); # 每个线程给全局共享变量依次加10
sub mysub {
my $thr_id = shift;
my $try_left = 10;
my $local_value;
sleep 1;
while($try_left--){
# 相当于获取锁
$sem->down(); $local_value = $gbvar;
say "$try_left tries left for sub $thr_id "."(\$gbvar is $gbvar)";
sleep 1;
$local_value++;
$gbvar = $local_value; # 相当于释放锁
$sem->up();
}
} $thr1->join();
$thr2->join();
$thr3->join();

由于信号量可以锁住任何片段的代码,所以它的锁机制非常灵活。

实际上,up()和down()每次操作默认都只增、减1个信号量的值,但可以给它们传递参数来一次性请求加N、减N个信号量值,对于减法操作,如果请求减N导致信号量的值为负数,则该减法操作被阻塞,直到有足够的信号量完成这次减法。这时的信号量就像是一个计数器一样。

例如:

use threads;
use Thread::Semaphore; # 创建一个信号量值为5的信号量
my $sem = Thread::Semaphore->new(5); my $thr1 = threads->new(\&sub1);
my $thr2 = threads->new(\&sub1); sub sub1 {
# 申请锁
$sem->down(5); # 一次减5
... do something here ...
$sem->up(5); # 一次加5
} $thr1->join();
$thr2->join();

Perl多线程(2):数据共享和线程安全的更多相关文章

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

    线程简介 线程(thread)是轻量级进程,和进程一样,都能独立.并行运行,也由父线程创建,并由父线程所拥有,线程也有线程ID作为线程的唯一标识符,也需要等待线程执行完毕后收集它们的退出状态(比如使用 ...

  2. 2.Perl 多线程:Threads(线程返回值)

    use warnings; use strict; use threads; sub TEST{ print "Hello, World!\n"; 'a'/); } #返回列表方法 ...

  3. Perl 多线程模块 Parallel::ForkManager

    Perl 多线程模块 Parallel::ForkManager 一个简单的并行处理模块.这个是用来对付循环的多线程处理. 放在循环前面. Table of Contents 1 Synops内容简介 ...

  4. Perl进程间数据共享

    本文介绍的Perl进程间数据共享内容主体来自于<Pro Perl>的第21章. IPC简介 通过fork创建多个子进程时,进程间的数据共享是个大问题,要么建立一个进程间通信的通道,要么找到 ...

  5. CoreData和SQLite多线程访问时的线程安全

    关于CoreData和SQLite多线程访问时的线程安全问题 数据库读取操作一般都是多线程访问的.在对数据进行读取时,我们要保证其当前状态不能被修改,即读取时加锁,否则就会出现数据错误混乱.IOS中常 ...

  6. C#多线程之旅(3)——线程池

    v博客前言 先交代下背景,写<C#多线程之旅>这个系列文章主要是因为以下几个原因:1.多线程在C/S和B/S架构中用得是非常多的;2.而且多线程的使用是非常复杂的,如果没有用好,容易造成很 ...

  7. (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例

    http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...

  8. 面试题_1_to_16_多线程、并发及线程的基础问题

    多线程.并发及线程的基础问题 1)Java 中能创建 volatile 数组吗?能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组.我的意思是,如果改变引 ...

  9. C#多线程实践——锁和线程安全

    锁实现互斥的访问,用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类: class ThreadUnsafe { static int val1, val2; static void ...

  10. C# 多线程的自动管理(线程池) 基于Task的方式

    C# 多线程的自动管理(线程池) 在多线程的程序中,经常会出现两种情况:    1. 应用程序中线程把大部分的时间花费在等待状态,等待某个事件发生,然后给予响应.这一般使用 ThreadPool(线程 ...

随机推荐

  1. shell基础及变量

    一 Shell概述 1.Shell的作用——命令解释器,“翻译官” shell作为一个人机接口,用于解释用户输入的命令,将命令解释为Linux内核可以执行的2进制代码,并将执行的结果返回在标准终端上. ...

  2. 数位DP -启示录

    http://poj.org/problem?id=3208 一个魔鬼数为包含连续三个666的的数字,给个n(n<5e7)求第n个魔鬼数. 预处理f[i][j],f[i][3]表示由前i位数字构 ...

  3. 4.再来看看逆向——OD的简介

    目录 1.前言 2.一些设置和配置 3.开始了解OD 代码窗口 数据窗口 小端序问题 前言 前3节主要写了恶意代码用到的手段,接下来先写一下关于逆向调试的一些内容.毕竟逆向比较难理解一点. 一些配置和 ...

  4. win10 vscode使用 智能提示

    1.没有第三方库的智能提示 参考:https://code.visualstudio.com/docs/python/editing 1.点开Settings 2.搜索加添加 3.添加后的内容 然后就 ...

  5. centos7 mysql自动备份

    MySQL自动备份shell脚本   在数据库的日常维护工作中,除了保证业务的正常运行以外,就是要对数据库进行备份,以免造成数据库的丢失,从而给企业带来重大经济损失.通常备份可以按照备份时数据库状态分 ...

  6. requirejs + sass 实现的前端及 grunt 自动化构建

    对于 现在的 vue . react .webpack 来说也许有点旧了,有时候,越简单的技术越可靠,备份一下 module.exports = function(grunt) { // Projec ...

  7. 使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之二 (POST 檔案)

    使用 HTML 进行文件上传,已经是很平常的应用了,在手机App里面,也常常会用到这个作业,例如拍照上传,或是从相簿选取照片上传,都是很常见的. 在 HTML 的 Form 里面,要让使用者选择文件上 ...

  8. Daily Pathtracer!安利下不错的Pathtracer学习资料

    0x00 前言 最近看到了我司大网红aras-p(Aras Pranckevičius)的博客开了一个很有趣的新系列<Daily Pathtracer~>,来实现一个简单的ToyPathT ...

  9. CoreProfiler升级到.NetStandard 2.0

    致所有感兴趣的朋友: CoreProfiler和相应的Sample项目cross-app-profiling-demo都已经升级到.NetStandrard 2.0和.NetCore 2.0. 有任何 ...

  10. ReactNative问题随记1 Exception in thread "main" java.lang.RuntimeException: gradle-2.14.1-all.zip

    ReactNative问题随记 想运行在真机上,在运行命令react-native run-android遇到错误如下: Scanning 559 folders for symlinks in D: ...