Skip to content

Commit

Permalink
Changes to code, documentation and examples to support multi-threaded…
Browse files Browse the repository at this point in the history
… workbook generation.
  • Loading branch information
f20 committed Oct 28, 2020
1 parent 42fc240 commit cdf83f5
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 1 deletion.
115 changes: 115 additions & 0 deletions examples/multithreaded1.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/perl

###############################################################################
#
# Example of multi-threaded workbook creation with Excel::Writer::XLSX.
# In this example, each thread waits for a process-wide lock to do its own clean-up.
#
# Copyright 2000-2020, John McNamara, [email protected]
#

use strict;
use warnings;
use Excel::Writer::XLSX;
use Excel::Writer::XLSX::Utility;
use threads;
use threads::shared;

###############################################################################
#
# Declare the shared variable $cleanup_lock_counter.
#
my $cleanup_lock_counter = 0;
share $cleanup_lock_counter;

###############################################################################
#
# Generate workbooks in multiple concurrent threads.
#
my %operators = (
addition => '+',
subtraction => '-',
multiplication => '*',
division => '/',
exponentiation => '^',
);
while ( my ( $operation, $operator ) = each %operators ) {
threads->create( make_runnable( $operation, $operator, 300, 300 ) );
}
$_->join foreach threads->list();

###############################################################################
#
# Create the runnable for a thread to generate workbook files
# and destroy its temporary files in a controlled manner.
#
sub make_runnable {
my ( $operation, $operator, $num_rows, $num_cols ) = @_;
sub {
my @workbookObjects;

{ # increment the shared variable $cleanup_lock_counter.
lock $cleanup_lock_counter;
++$cleanup_lock_counter;
}

# Make the workbook and store the workbook object.
push @workbookObjects,
make_workbook( $operation, $operator, $num_rows, $num_cols );

{ # decrement the shared variable $cleanup_lock_counter.
lock $cleanup_lock_counter;
--$cleanup_lock_counter;
cond_signal $cleanup_lock_counter;
}

# Wait for the shared variable $cleanup_lock_counter to be zero.
lock $cleanup_lock_counter;
cond_wait $cleanup_lock_counter while $cleanup_lock_counter > 0;

# Destruction of temporary files associated with each workbook object.
$_->DESTROY foreach grep { defined $_; } @workbookObjects;
cond_signal $cleanup_lock_counter;

};
}

###############################################################################
#
# Create a Excel::Writer::XLSX file.
#
sub make_workbook {

my ( $operation, $operator, $num_rows, $num_cols ) = @_;
my $long_name = "Table of $operation ($num_rows by $num_cols)";
my $workbook = Excel::Writer::XLSX->new("$long_name.xlsx") or return;
my $worksheet = $workbook->add_worksheet( ucfirst($operation) );
$worksheet->hide_gridlines(2);
my $title_format =
$workbook->add_format( size => 15, bold => 1, num_format => '@' );
my $header_format = $workbook->add_format( bg_color => 46 );
$worksheet->write( 0, 0, $long_name, $title_format );
$worksheet->write( 2, 0, $operator, $header_format );

$worksheet->write( $_ + 2, 0, $_, $header_format ) foreach 1 .. $num_rows;
$worksheet->write( 2, $_, $_, $header_format ) foreach 1 .. $num_cols;

foreach my $row ( 3 .. ( 2 + $num_rows ) ) {
foreach my $col ( 1 .. $num_cols ) {
$worksheet->write( $row, $col,
'='
. xl_rowcol_to_cell( $row, 0, 0, 1 )
. $operator
. xl_rowcol_to_cell( 2, $col, 1, 0 ) );
}
}

# Generate file.
$workbook->close();

# Return workbook object to caller, to control clean-up of temporary files.
$workbook;

}

__END__
95 changes: 95 additions & 0 deletions examples/multithreaded2.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/perl

###############################################################################
#
# Example of multi-threaded workbook creation with Excel::Writer::XLSX.
# In this example, a shared queue collects references to the temporary directories
# and these temporary directories are cleaned up when the process ends.
#
# Copyright 2000-2020, John McNamara, [email protected]
#

use strict;
use warnings;
use Excel::Writer::XLSX;
use Excel::Writer::XLSX::Utility;
use threads;
use Thread::Queue;

###############################################################################
#
# Declare the shared queue $cleanup_queue.
#
my $cleanup_queue = Thread::Queue->new;

###############################################################################
#
# Generate workbooks in multiple concurrent threads.
#
my %operators = (
addition => '+',
subtraction => '-',
multiplication => '*',
division => '/',
exponentiation => '^',
);
while ( my ( $operation, $operator ) = each %operators ) {
threads->create( make_runnable( $operation, $operator, 300, 300 ) );
}
$_->join foreach threads->list();
$cleanup_queue->end;
1 while $cleanup_queue->dequeue;

###############################################################################
#
# Create the runnable for a thread to generate workbook files
# and destroy its temporary files in a controlled manner.
#
sub make_runnable {
my ( $operation, $operator, $num_rows, $num_cols ) = @_;
sub {
my $workbook =
make_workbook( $operation, $operator, $num_rows, $num_cols );
$cleanup_queue->enqueue( $workbook->{_tempdir_object} );
};
}

###############################################################################
#
# Create a Excel::Writer::XLSX file.
#
sub make_workbook {

my ( $operation, $operator, $num_rows, $num_cols ) = @_;
my $long_name = "Table of $operation ($num_rows by $num_cols)";
my $workbook = Excel::Writer::XLSX->new("$long_name.xlsx") or return;
my $worksheet = $workbook->add_worksheet( ucfirst($operation) );
$worksheet->hide_gridlines(2);
my $title_format =
$workbook->add_format( size => 15, bold => 1, num_format => '@' );
my $header_format = $workbook->add_format( bg_color => 46 );
$worksheet->write( 0, 0, $long_name, $title_format );
$worksheet->write( 2, 0, $operator, $header_format );

$worksheet->write( $_ + 2, 0, $_, $header_format ) foreach 1 .. $num_rows;
$worksheet->write( 2, $_, $_, $header_format ) foreach 1 .. $num_cols;

foreach my $row ( 3 .. ( 2 + $num_rows ) ) {
foreach my $col ( 1 .. $num_cols ) {
$worksheet->write( $row, $col,
'='
. xl_rowcol_to_cell( $row, 0, 0, 1 )
. $operator
. xl_rowcol_to_cell( 2, $col, 1, 0 ) );
}
}

# Generate file.
$workbook->close();

# Return workbook object to caller, to control clean-up of temporary files.
$workbook;

}

__END__
2 changes: 1 addition & 1 deletion lib/Excel/Writer/XLSX.pm
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ The reason for this is that Excel::Writer::XLSX relies on Perl's C<DESTROY> mech
To avoid these issues it is recommended that you always close the Excel::Writer::XLSX filehandle using C<close()>.
C<close()> is thread-safe but disposal of the Workbook object is not (because disposal of the Workbook object triggers the non-thread-safe destruction of temporary files by the C<File::Temp> module). In a program in which several threads might be concurrently writing C<Excel::Writer::XLSX> files, the Workbook objects must only be destroyed or allowed to go out of scope within a critical code section that only one thread can be running at any one time.
=head2 set_size( $width, $height )
Expand Down
10 changes: 10 additions & 0 deletions lib/Excel/Writer/XLSX/Workbook.pm
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ sub DESTROY {
local ( $@, $!, $^E, $? );

$self->close() if not $self->{_fileclosed};
delete $self->{_tempdir_object};
}


Expand Down Expand Up @@ -1095,6 +1096,13 @@ sub _store_workbook {

my $self = shift;
my $tempdir = File::Temp->newdir( DIR => $self->{_tempdir} );

# Store the File::Temp object within $self so that the temporary files
# are only removed when the workbook object is destroyed.
# This control over timing is required because the removal
# of File::Temp temporary directories is not thread-safe.
$self->{_tempdir_object} = $tempdir;

my $packager = Excel::Writer::XLSX::Package::Packager->new();
my $zip = Archive::Zip->new();

Expand Down Expand Up @@ -1151,13 +1159,15 @@ sub _store_workbook {
# Add the files to the zip archive. Due to issues with Archive::Zip in
# taint mode we can't use addTree() so we have to build the file list
# with File::Find and pass each one to addFile().
# The no_chdir option to File::Find::find is for thread safety.
my @xlsx_files;

my $wanted = sub { push @xlsx_files, $File::Find::name if -f };

File::Find::find(
{
wanted => $wanted,
no_chdir => 1,
untaint => 1,
untaint_pattern => qr|^(.+)$|
},
Expand Down

0 comments on commit cdf83f5

Please sign in to comment.