Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed fix for issue #182 Thread safety #261

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ perl:
- "5.12"
- "5.10"
- "5.8"
- "5.26-shrplib"
- "5.24-shrplib"
- "5.22-shrplib"
- "5.20-shrplib"
- "5.18-shrplib"
sudo: false

117 changes: 117 additions & 0 deletions examples/multithreaded1.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/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 after creating all the workbooks.
#
# 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, 400, 400 ],
[ $operation, $operator, 420, 420 ],
)
);
}
$_->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 @instructions = @_;
sub {

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

# Make the workbooks and store the workbook objects.
my @workbookObjects = map { make_workbook(@$_); } @instructions;

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

# Once $cleanup_lock_counter is 0, destroy temporary files.
lock $cleanup_lock_counter;
cond_wait $cleanup_lock_counter while $cleanup_lock_counter > 0;
$_->DESTROY foreach @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__
126 changes: 126 additions & 0 deletions examples/multithreaded2.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/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 after creating each workbook.
#
# 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, 400, 400 ],
[ $operation, $operator, 420, 420 ],
)
);
}
$_->join foreach threads->list();

###############################################################################
#
# Create the runnable for a thread to generate workbook files.
#
sub make_runnable {
my @instructions = @_;
sub {
make_workbook_and_clean_up(@$_) foreach @instructions;
};
}

###############################################################################
#
# Wrapper around make_workbook to manage temporary file cleanup.
#
sub make_workbook_and_clean_up {
my ( $operation, $operator, $num_rows, $num_cols ) = @_;

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

# Make the workbook and store the workbook object.
my $workbookObject =
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;
}

# Once $cleanup_lock_counter is 0, destroy temporary files.
lock $cleanup_lock_counter;
cond_wait $cleanup_lock_counter while $cleanup_lock_counter > 0;
$workbookObject->DESTROY;
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__
111 changes: 111 additions & 0 deletions examples/multithreaded3.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/perl

###############################################################################
#
# Example of multi-threaded workbook creation with Excel::Writer::XLSX.
# In this example, each thread cleans up manually its temporary storage after
# creating each workbook. The clean-up code in this example has not been
# properly tested and relying on File::Temp's cleanup process with a lock
# as in the other multithreaded examples would be safer.
#
# Copyright 2000-2020, John McNamara, [email protected]
#

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

###############################################################################
#
# 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, 400, 400 ],
[ $operation, $operator, 420, 420 ],
)
);
}
$_->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 @instructions = @_;
sub {

# Make the workbooks and destroy the temporary files in a thread-safe way.
foreach (@instructions) {
my $workbook = make_workbook(@$_);
$workbook->{_tempdir_object}->unlink_on_destroy(0);
File::Find::find(
{
wanted => sub {
-f $_
? unlink $File::Find::name
: rmdir $File::Find::name;
},
no_chdir => 1,
bydepth => 1,
untaint => 1,
untaint_pattern => qr|^(.+)$|
},
$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 @@ -438,7 +438,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
6 changes: 5 additions & 1 deletion lib/Excel/Writer/XLSX/Chart.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,8 @@ sub _get_gradient_properties {

$gradient->{_colors} = $args->{colors};

$gradient->{_transparency} = $args->{transparency};

if ( $args->{positions} ) {

# Check the positions array has the right number of entries.
Expand Down Expand Up @@ -6628,7 +6630,9 @@ sub _write_a_gs_lst {

# Write the a:srgbClr element.
# TODO: Wait for a feature request to support transparency.
$self->_write_a_srgb_clr( $color );
$self->_write_a_srgb_clr( $color,
$gradient->{_transparency} ? $gradient->{_transparency}[$i] : ()
);

$self->xml_end_tag( 'a:gs' );
}
Expand Down
Loading