File Coverage

blib/lib/MCE/Channel.pm
Criterion Covered Total %
statement 65 71 91.5
branch 15 32 46.8
condition 10 21 47.6
subroutine 19 20 95.0
pod 2 2 100.0
total 111 146 76.0


line stmt bran cond sub pod time code
1             ###############################################################################
2             ## ----------------------------------------------------------------------------
3             ## Queue-like and two-way communication capability.
4             ##
5             ###############################################################################
6              
7             package MCE::Channel;
8              
9 18     18   519810 use strict;
  18         81  
  18         450  
10 18     18   75 use warnings;
  18         20  
  18         412  
11              
12 18     18   59 no warnings qw( uninitialized once );
  18         32  
  18         786  
13              
14             our $VERSION = '1.887';
15              
16             ## no critic (BuiltinFunctions::ProhibitStringyEval)
17             ## no critic (TestingAndDebugging::ProhibitNoStrict)
18              
19 18     18   10136 use if $^O eq 'MSWin32', 'threads';
  18         199  
  18         131  
20 18     18   748 use if $^O eq 'MSWin32', 'threads::shared';
  18         33  
  18         55  
21              
22 18     18   427 use Carp ();
  18         21  
  18         3166  
23              
24             $Carp::Internal{ (__PACKAGE__) }++;
25              
26             my ( $freeze, $thaw );
27              
28             BEGIN {
29 18 50 33 18   154 if ( $] ge '5.008008' && ! $INC{'PDL.pm'} ) {
30 18         42 local $@;
31 18     18   1048 eval 'use Sereal::Encoder 3.015; use Sereal::Decoder 3.015;';
  18     18   117  
  18         612  
  18         917  
  18         95  
  18         237  
  18         499  
32 18 50       84 if ( ! $@ ) {
33 18         167 my $encoder_ver = int( Sereal::Encoder->VERSION() );
34 18         133 my $decoder_ver = int( Sereal::Decoder->VERSION() );
35 18 50       69 if ( $encoder_ver - $decoder_ver == 0 ) {
36 18         35 $freeze = \&Sereal::Encoder::encode_sereal;
37 18         36 $thaw = \&Sereal::Decoder::decode_sereal;
38             }
39             }
40             }
41              
42 18 50       434 if ( ! defined $freeze ) {
43 0         0 require Storable;
44 0         0 $freeze = \&Storable::freeze;
45 0         0 $thaw = \&Storable::thaw;
46             }
47             }
48              
49 18     18   6604 use MCE::Util ();
  18         49  
  18         2820  
50              
51             my $tid = $INC{'threads.pm'} ? threads->tid() : 0;
52              
53             sub new {
54 29     29 1 2909 my ( $class, %argv ) = @_;
55 29 50       180 my $impl = defined( $argv{impl} ) ? ucfirst( lc $argv{impl} ) : 'Mutex';
56              
57             # Replace 'fast' with 'Fast' in the implementation value.
58 29         105 $impl =~ s/fast$/Fast/;
59              
60 29 50 66     197 $impl = 'Threads' if ( $impl eq 'Mutex' && $^O eq 'MSWin32' );
61 29 50 66     144 $impl = 'ThreadsFast' if ( $impl eq 'MutexFast' && $^O eq 'MSWin32' );
62 29 50 33     107 $impl = 'Mutex' if ( $impl eq 'Threads' && $^O eq 'cygwin' );
63 29 50 33     90 $impl = 'MutexFast' if ( $impl eq 'ThreadsFast' && $^O eq 'cygwin' );
64              
65 29 50       1480 eval "require MCE::Channel::$impl; 1;" ||
66             Carp::croak("Could not load Channel implementation '$impl': $@");
67              
68 29         131 my $pkg = 'MCE::Channel::'.$impl;
69 18     18   108 no strict 'refs';
  18         37  
  18         6396  
70              
71 29         148 $pkg->new(%argv);
72             }
73              
74             sub CLONE {
75 0 0   0   0 $tid = threads->tid if $INC{'threads.pm'};
76             }
77              
78             sub DESTROY {
79 19 50   19   31147 my ( $pid, $self ) = ( $tid ? $$ .'.'. $tid : $$, @_ );
80              
81 19 100 66     234 if ( $self->{'init_pid'} && $self->{'init_pid'} eq $pid ) {
82 16         135 MCE::Util::_destroy_socks($self, qw(c_sock c2_sock p_sock p2_sock));
83 16         378 delete($self->{c_mutex}), delete($self->{p_mutex});
84             }
85              
86 19         1289 return;
87             }
88              
89             sub impl {
90 6 50   6 1 94 $_[0]->{'impl'} || 'Not defined';
91             }
92              
93 15     15   43 sub _get_freeze { $freeze; }
94 15     15   41 sub _get_thaw { $thaw; }
95              
96             sub _ended {
97 12     12   105 warn "WARNING: ($_[0]) called on a channel that has been 'end'ed\n";
98              
99 12         7795 return;
100             }
101              
102             sub _read {
103 120     120   236 my $bytes = MCE::Util::_sysread( $_[0], $_[1], my $len = $_[2] );
104 120         215 my $read = $bytes;
105              
106 120   33     386 while ( $bytes && $read != $len ) {
107 0         0 $bytes = MCE::Util::_sysread( $_[0], $_[1], $len - $read, length($_[1]) );
108 0 0       0 $read += $bytes if $bytes;
109             }
110              
111 120         197 return;
112             }
113              
114             sub _pid {
115 29 50   29   151 $tid ? $$ .'.'. $tid : $$;
116             }
117              
118             1;
119              
120             __END__
121              
122             ###############################################################################
123             ## ----------------------------------------------------------------------------
124             ## Module usage.
125             ##
126             ###############################################################################
127              
128             =head1 NAME
129              
130             MCE::Channel - Queue-like and two-way communication capability
131              
132             =head1 VERSION
133              
134             This document describes MCE::Channel version 1.887
135              
136             =head1 SYNOPSIS
137              
138             use MCE::Channel;
139              
140             ########################
141             # Construction
142             ########################
143              
144             # A single producer and many consumers supporting processes and threads
145              
146             my $c1 = MCE::Channel->new( impl => 'Mutex' ); # default implementation
147             my $c2 = MCE::Channel->new( impl => 'Threads' ); # threads::shared locking
148              
149             # Set the mp flag if two or more workers (many producers) will be calling
150             # enqueue/send or recv2/recv2_nb on the left end of the channel
151              
152             my $c3 = MCE::Channel->new( impl => 'Mutex', mp => 1 );
153             my $c4 = MCE::Channel->new( impl => 'Threads', mp => 1 );
154              
155             # Tuned for one producer and one consumer, no locking
156              
157             my $c5 = MCE::Channel->new( impl => 'Simple' );
158              
159             ########################
160             # Queue-like behavior
161             ########################
162              
163             # Send data to consumers
164             $c1->enqueue('item');
165             $c1->enqueue(qw/item1 item2 item3 itemN/);
166              
167             # Receive data
168             my $item = $c1->dequeue(); # item
169             my @items = $c1->dequeue(2); # (item1, item2)
170              
171             # Receive, non-blocking
172             my $item = $c1->dequeue_nb(); # item
173             my @items = $c1->dequeue_nb(2); # (item1, item2)
174              
175             # Signal that there is no more work to be sent
176             $c1->end();
177              
178             ########################
179             # Two-way communication
180             ########################
181              
182             # Producer(s) sending data
183             $c3->send('message');
184             $c3->send(qw/arg1 arg2 arg3/);
185              
186             # Consumer(s) receiving data
187             my $mesg = $c3->recv(); # message
188             my @args = $c3->recv(); # (arg1, arg2, arg3)
189              
190             # Alternatively, non-blocking
191             my $mesg = $c3->recv_nb(); # message
192             my @args = $c3->recv_nb(); # (arg1, arg2, arg3)
193              
194             # A producer signaling no more work to be sent
195             $c3->end();
196              
197             # Consumers(s) sending data
198             $c3->send2('message');
199             $c3->send2(qw/arg1 arg2 arg3/);
200              
201             # Producer(s) receiving data
202             my $mesg = $c3->recv2(); # message
203             my @args = $c3->recv2(); # (arg1, arg2, arg3)
204              
205             # Alternatively, non-blocking
206             my $mesg = $c3->recv2_nb(); # message
207             my @args = $c3->recv2_nb(); # (arg1, arg2, arg3)
208              
209             =head1 DESCRIPTION
210              
211             A MCE::Channel object is a container for sending and receiving data using
212             socketpair handles. Serialization is provided by L<Sereal> if available.
213             Defaults to L<Storable> otherwise. Excluding the C<Simple> implementation,
214             both ends of the C<channel> support many workers concurrently (with mp => 1).
215              
216             =head2 new ( impl => STRING, mp => BOOLEAN )
217              
218             This creates a new channel. Three implementations are provided C<Mutex>,
219             C<Threads>, and C<Simple> indicating the locking mechanism to use
220             C<MCE::Mutex>, C<threads::shared>, and no locking respectively.
221              
222             $chnl = MCE::Channel->new(); # default: impl => 'Mutex', mp => 0
223             # default: impl => 'Threads' on Windows
224              
225             The C<Mutex> implementation supports processes and threads whereas the
226             C<Threads> implementation is suited for Windows and threads only.
227              
228             $chnl = MCE::Channel->new( impl => 'Mutex' ); # MCE::Mutex locking
229             $chnl = MCE::Channel->new( impl => 'Threads' ); # threads::shared locking
230              
231             # on Windows, silently becomes impl => 'Threads' when specifying 'Mutex'
232              
233             Set the C<mp> (m)any (p)roducers option to a true value if there will be two
234             or more workers calling C<enqueue>, <send>, C<recv2>, or C<recv2_nb> on the
235             left end of the channel. This is important to not incur a race condition.
236              
237             $chnl = MCE::Channel->new( impl => 'Mutex', mp => 1 );
238             $chnl = MCE::Channel->new( impl => 'Threads', mp => 1 );
239              
240             # on Windows, silently becomes impl => 'Threads' when specifying 'Mutex'
241              
242             The C<Simple> implementation is optimized for one producer and one consumer max.
243             It omits locking for maximum performance. This implementation is preferred for
244             parent to child communication not shared by another worker.
245              
246             $chnl = MCE::Channel->new( impl => 'Simple' );
247              
248             =head1 QUEUE-LIKE BEHAVIOR
249              
250             =head2 enqueue ( ITEM1 [, ITEM2, ... ] )
251              
252             Appends a list of items onto the left end of the channel. This will block once
253             the internal socket buffer becomes full (i.e. awaiting workers to dequeue on the
254             other end). This prevents producer(s) from running faster than consumer(s).
255              
256             Object (de)serialization is handled automatically using L<Sereal> if available
257             or defaults to L<Storable> otherwise.
258              
259             $chnl->enqueue('item1');
260             $chnl->enqueue(qw/item2 item3 .../);
261              
262             $chnl->enqueue([ array_ref1 ]);
263             $chnl->enqueue([ array_ref2 ], [ array_ref3 ], ...);
264              
265             $chnl->enqueue({ hash_ref1 });
266             $chnl->enqueue({ hash_ref2 }, { hash_ref3 }, ...);
267              
268             =head2 dequeue
269              
270             =head2 dequeue ( COUNT )
271              
272             Removes the requested number of items (default 1) from the right end of the
273             channel. If the channel contains fewer than the requested number of items,
274             the method will block (i.e. until other producer(s) enqueue more items).
275              
276             $item = $chnl->dequeue(); # item1
277             @items = $chnl->dequeue(2); # ( item2, item3 )
278              
279             =head2 dequeue_nb
280              
281             =head2 dequeue_nb ( COUNT )
282              
283             Removes the requested number of items (default 1) from the right end of the
284             channel. If the channel contains fewer than the requested number of items,
285             the method will return what it was able to retrieve and return immediately.
286             If the channel is empty, then returns C<an empty list> in list context or
287             C<undef> in scalar context.
288              
289             $item = $chnl->dequeue_nb(); # array_ref1
290             @items = $chnl->dequeue_nb(2); # ( array_ref2, array_ref3 )
291              
292             =head2 end
293              
294             This is called by a producer to signal that there is no more work to be sent.
295             Once ended, no more items may be sent by the producer. Calling C<end> by
296             multiple producers is not supported.
297              
298             $chnl->end;
299              
300             =head1 TWO-WAY IPC - PRODUCER TO CONSUMER
301              
302             =head2 send ( ARG1 [, ARG2, ... ] )
303              
304             Append data onto the left end of the channel. Unlike C<enqueue>, the values
305             are kept together for the receiving consumer, similarly to calling a method.
306             Object (de)serialization is handled automatically.
307              
308             $chnl->send('item');
309             $chnl->send([ list_ref ]);
310             $chnl->send([ hash_ref ]);
311              
312             $chnl->send(qw/item1 item2 .../);
313             $chnl->send($id, [ list_ref ]);
314             $chnl->send($id, { hash_ref });
315              
316             The fast channel implementations, introduced in MCE 1.877, support one item
317             for C<send>. If you want to pass multiple arguments, simply join the arguments
318             into a string. That means the receiver will need to split the string.
319              
320             $chnl = MCE::Channel->new(impl => "SimpleFast");
321              
322             $chnl->send(join(" ", qw/item1 item2 item3/);
323             my ($item1, $item2, $item3) = split " ", $chnl->recv();
324              
325             =head2 recv
326              
327             =head2 recv_nb
328              
329             Blocking and non-blocking fetch methods from the right end of the channel.
330             For the latter and when the channel is empty, returns C<an empty list> in
331             list context or C<undef> in scalar context.
332              
333             $item = $chnl->recv();
334             $array_ref = $chnl->recv();
335             $hash_ref = $chnl->recv();
336              
337             ($item1, $item2) = $chnl->recv_nb();
338             ($id, $array_ref) = $chnl->recv_nb();
339             ($id, $hash_ref) = $chnl->recv_nb();
340              
341             =head1 TWO-WAY IPC - CONSUMER TO PRODUCER
342              
343             =head2 send2 ( ARG1 [, ARG2, ... ] )
344              
345             Append data onto the right end of the channel. Unlike C<enqueue>, the values
346             are kept together for the receiving producer, similarly to calling a method.
347             Object (de)serialization is handled automatically.
348              
349             $chnl->send2('item');
350             $chnl->send2([ list_ref ]);
351             $chnl->send2([ hash_ref ]);
352              
353             $chnl->send2(qw/item1 item2 .../);
354             $chnl->send2($id, [ list_ref ]);
355             $chnl->send2($id, { hash_ref });
356              
357             The fast channel implementations, introduced in MCE 1.877, support one item
358             for C<send2>. If you want to pass multiple arguments, simply join the arguments
359             into a string. Not to forget, the receiver must split the string as well.
360              
361             $chnl = MCE::Channel->new(impl => "MutexFast");
362              
363             $chnl->send2(join(" ", qw/item1 item2 item3/);
364             my ($item1, $item2, $item3) = split " ", $chnl->recv();
365              
366             =head2 recv2
367              
368             =head2 recv2_nb
369              
370             Blocking and non-blocking fetch methods from the left end of the channel.
371             For the latter and when the channel is empty, returns C<an empty list> in
372             list context or C<undef> in scalar context.
373              
374             $item = $chnl->recv2();
375             $array_ref = $chnl->recv2();
376             $hash_ref = $chnl->recv2();
377              
378             ($item1, $item2) = $chnl->recv2_nb();
379             ($id, $array_ref) = $chnl->recv2_nb();
380             ($id, $hash_ref) = $chnl->recv2_nb();
381              
382             =head1 DEMONSTRATIONS
383              
384             =head2 Example 1 - threads
385              
386             C<MCE::Channel> was made to work efficiently with L<threads>. The reason
387             comes from using L<threads::shared> for locking versus L<MCE::Mutex>.
388              
389             use strict;
390             use warnings;
391              
392             use threads;
393             use MCE::Channel;
394              
395             my $queue = MCE::Channel->new( impl => 'Threads' );
396             my $num_consumers = 10;
397              
398             sub consumer {
399             my $count = 0;
400              
401             # receive items
402             while ( my ($item1, $item2) = $queue->dequeue(2) ) {
403             $count += 2;
404             }
405              
406             # send result
407             $queue->send2( threads->tid => $count );
408             }
409              
410             threads->create('consumer') for 1 .. $num_consumers;
411              
412             ## producer
413              
414             $queue->enqueue($_, $_ * 2) for 1 .. 40000;
415             $queue->end;
416              
417             my %results;
418             my $total = 0;
419              
420             for ( 1 .. $num_consumers ) {
421             my ($id, $count) = $queue->recv2;
422             $results{$id} = $count;
423             $total += $count;
424             }
425              
426             $_->join for threads->list;
427              
428             print $results{$_}, "\n" for keys %results;
429             print "$total total\n\n";
430              
431             __END__
432              
433             # output
434              
435             8034
436             8008
437             8036
438             8058
439             7990
440             7948
441             8068
442             7966
443             7960
444             7932
445             80000 total
446              
447             =head2 Example 2 - MCE::Child
448              
449             The following is similarly threads-like for Perl lacking threads support.
450             It spawns processes instead, thus requires the C<Mutex> channel implementation
451             which is the default if omitted.
452              
453             use strict;
454             use warnings;
455              
456             use MCE::Child;
457             use MCE::Channel;
458              
459             my $queue = MCE::Channel->new( impl => 'Mutex' );
460             my $num_consumers = 10;
461              
462             sub consumer {
463             my $count = 0;
464              
465             # receive items
466             while ( my ($item1, $item2) = $queue->dequeue(2) ) {
467             $count += 2;
468             }
469              
470             # send result
471             $queue->send2( MCE::Child->pid => $count );
472             }
473              
474             MCE::Child->create('consumer') for 1 .. $num_consumers;
475              
476             ## producer
477              
478             $queue->enqueue($_, $_ * 2) for 1 .. 40000;
479             $queue->end;
480              
481             my %results;
482             my $total = 0;
483              
484             for ( 1 .. $num_consumers ) {
485             my ($id, $count) = $queue->recv2;
486             $results{$id} = $count;
487             $total += $count;
488             }
489              
490             $_->join for MCE::Child->list;
491              
492             print $results{$_}, "\n" for keys %results;
493             print "$total total\n\n";
494              
495             =head2 Example 3 - Consumer requests item
496              
497             Like the previous example, but have the manager process await a notification
498             from the consumer before inserting into the queue. This allows the producer
499             to end the channel early (i.e. exit loop).
500              
501             use strict;
502             use warnings;
503              
504             use MCE::Child;
505             use MCE::Channel;
506              
507             my $queue = MCE::Channel->new( impl => 'Mutex' );
508             my $num_consumers = 10;
509              
510             sub consumer {
511             # receive items
512             my $count = 0;
513              
514             while () {
515             # Notify the manager process to send items. This allows the
516             # manager process to enqueue only when requested. The benefit
517             # is being able to end the channel immediately.
518              
519             $queue->send2( MCE::Child->pid ); # channel is bi-directional
520              
521             my ($item1, $item2) = $queue->dequeue(2);
522             last unless ( defined $item1 ); # channel ended
523              
524             $count += 2;
525             }
526              
527             # result
528             return ( MCE::Child->pid => $count );
529             }
530              
531             MCE::Child->create('consumer') for 1 .. $num_consumers;
532              
533             ## producer
534              
535             for my $num (1 .. 40000) {
536             # Await worker notification before inserting (blocking).
537             my $consumer_pid = $queue->recv2;
538             $queue->enqueue($num, $num * 2);
539             }
540              
541             $queue->end;
542              
543             my %results;
544             my $total = 0;
545              
546             for my $child ( MCE::Child->list ) {
547             my ($id, $count) = $child->join;
548             $results{$id} = $count;
549             $total += $count;
550             }
551              
552             print $results{$_}, "\n" for keys %results;
553             print "$total total\n\n";
554              
555             =head2 Example 4 - Many producers
556              
557             Running with 2 or more producers requires setting the C<mp> option. Internally,
558             this enables locking support for the left end of the channel. The C<mp> option
559             applies to C<Mutex> and C<Threads> channel implementations only.
560              
561             Here, using the MCE facility for gathering the final count.
562              
563             use strict;
564             use warnings;
565              
566             use MCE::Flow;
567             use MCE::Channel;
568              
569             my $queue = MCE::Channel->new( impl => 'Mutex', mp => 1 );
570             my $num_consumers = 10;
571              
572             sub consumer {
573             # receive items
574             my $count = 0;
575             while ( my ( $item1, $item2 ) = $queue->dequeue(2) ) {
576             $count += 2;
577             }
578             # send result
579             MCE->gather( MCE->wid => $count );
580             }
581              
582             sub producer {
583             $queue->enqueue($_, $_ * 2) for 1 .. 20000;
584             }
585              
586             ## run 2 producers and many consumers
587              
588             MCE::Flow->init(
589             max_workers => [ 2, $num_consumers ],
590             task_name => [ 'producer', 'consumer' ],
591             task_end => sub {
592             my ($mce, $task_id, $task_name) = @_;
593             if ( $task_name eq 'producer' ) {
594             $queue->end;
595             }
596             }
597             );
598              
599             # consumers call gather above (i.e. send a key-value pair),
600             # have MCE append to a hash
601              
602             my %results = mce_flow \&producer, \&consumer;
603              
604             MCE::Flow->finish;
605              
606             my $total = 0;
607              
608             for ( keys %results ) {
609             $total += $results{$_};
610             print $results{$_}, "\n";
611             }
612              
613             print "$total total\n\n";
614              
615             =head2 Example 5 - Many channels
616              
617             This demonstration configures a channel per consumer. Plus, a common channel
618             for consumers to request the next input item. The C<Simple> implementation is
619             specified for the individual channels whereas locking may be necessary for the
620             C<$ready> channel. However, consumers do not incur reading and what is written
621             is very small (i.e. atomic write is guaranteed by the OS). Thus, am safely
622             choosing the C<Simple> implementation versus C<Mutex>.
623              
624             use strict;
625             use warnings;
626              
627             use MCE::Flow;
628             use MCE::Channel;
629              
630             my $prog_name = $0; $prog_name =~ s{^.*[\\/]}{}g;
631             my $input_size = shift || 3000;
632              
633             unless ($input_size =~ /\A\d+\z/) {
634             print {*STDERR} "usage: $prog_name [ size ]\n";
635             exit 1;
636             }
637              
638             my $consumers = 4;
639              
640             my @chnls = map { MCE::Channel->new( impl => 'Simple' ) } 1 .. $consumers;
641              
642             my $ready = MCE::Channel->new( impl => 'Simple' );
643              
644             sub producer {
645             my $id = 0;
646              
647             # send the next input item upon request
648             for ( 0 .. $input_size - 1 ) {
649             my $chnl_num = $ready->recv2;
650             $chnls[ $chnl_num ]->send( ++$id, $_ );
651             }
652              
653             # signal no more work
654             $_->send( 0, undef ) for @chnls;
655             }
656              
657             sub consumer {
658             my $chnl_num = MCE->task_wid - 1;
659              
660             while () {
661             # notify the producer ready for input
662             $ready->send2( $chnl_num );
663              
664             # retrieve input data
665             my ( $id, $item ) = $chnls[ $chnl_num ]->recv;
666              
667             # leave loop if no more work
668             last unless $id;
669              
670             # compute and send the result to the manager process
671             # ordered output requires an id (must be 1st argument)
672             MCE->gather( $id, [ $item, sqrt($item) ] );
673             }
674             }
675              
676             # A custom 'ordered' output iterator for MCE's gather facility.
677             # It returns a closure block, expecting an ID for 1st argument.
678              
679             sub output_iterator {
680             my %tmp; my $order_id = 1;
681              
682             return sub {
683             my ( $id, $result ) = @_;
684             $tmp{ $id } = $result;
685              
686             while () {
687             last unless exists $tmp{ $order_id };
688             $result = delete $tmp{ $order_id };
689             printf "n: %d sqrt(n): %f\n", $result->[0], $result->[1];
690             $order_id++;
691             }
692             };
693             }
694              
695             # Run one producer and many consumers.
696             # Output to be sent orderly to STDOUT.
697              
698             MCE::Flow->init(
699             gather => output_iterator(),
700             max_workers => [ 1, $consumers ],
701             );
702              
703             MCE::Flow->run( \&producer, \&consumer );
704             MCE::Flow->finish;
705              
706             __END__
707              
708             # Output
709              
710             n: 0 sqrt(n): 0.000000
711             n: 1 sqrt(n): 1.000000
712             n: 2 sqrt(n): 1.414214
713             n: 3 sqrt(n): 1.732051
714             n: 4 sqrt(n): 2.000000
715             n: 5 sqrt(n): 2.236068
716             n: 6 sqrt(n): 2.449490
717             n: 7 sqrt(n): 2.645751
718             n: 8 sqrt(n): 2.828427
719             n: 9 sqrt(n): 3.000000
720             ...
721              
722             =head1 SEE ALSO
723              
724             =over 3
725              
726             =item * L<https://github.com/marioroy/mce-examples/tree/master/chameneos>
727              
728             =item * L<threads::lite>
729              
730             =back
731              
732             =head1 AUTHOR
733              
734             Mario E. Roy, S<E<lt>marioeroy AT gmail DOT comE<gt>>
735              
736             =head1 COPYRIGHT AND LICENSE
737              
738             Copyright (C) 2019-2023 by Mario E. Roy
739              
740             MCE::Channel is released under the same license as Perl.
741              
742             See L<https://dev.perl.org/licenses/> for more information.
743              
744             =cut
745