├── LICENSE.txt ├── README.md ├── csp.html ├── csp.man ├── csp.tcl ├── csp.test ├── doc.tcl ├── pkgIndex.tcl └── test.tcl /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyrite (c) 2015 SecurityKISS Ltd (http://www.securitykiss.com) 3 | 4 | The MIT License (MIT) 5 | 6 | Yes, Mr patent attorney, you have nothing to do here. Find a decent job instead. 7 | Fight intellectual "property". 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## csp - Golang inspired concurrency library for Tcl 2 | 3 | Documentation: 4 | 5 | https://securitykiss-com.github.io/csp/csp.html 6 | 7 | 8 | The csp package for Tcl is a concurrency library based on Communicating Sequential Processes and provides two primitives namely coroutines and channels which allow concurrent programming in the style of Golang. 9 | 10 | The concepts originate in Hoare's Communicating Sequential Processes while the syntax mimics the Golang implementation. 11 | 12 | The CSP concurrency model may be visualized as a set of independent processes (coroutines) sending and receiving messages to the named channels. The control flow in the coroutines is coordinated at the points of sending and receiving messages i.e. the coroutine may need to wait while trying to send or receive. Since it must work in a single-threaded interpreter, waiting is non-blocking. Instead of blocking a waiting coroutine gives way to other coroutines. 13 | 14 | This concurrency model may also be seen as a generalization of Unix named pipes where processes and pipes correspond to coroutines and channels. 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ### Example 23 | 24 | ```tcl 25 | 26 | package require http 27 | package require csp 28 | namespace import csp::* 29 | 30 | proc main {} { 31 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1] 32 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2] 33 | timer t1 400 34 | select { 35 | <- $ch1 { 36 | puts "from first request: [http::data [<- $ch1]]" 37 | } 38 | <- $ch2 { 39 | puts "from second request: [http::data [<- $ch2]]" 40 | } 41 | <- $t1 { 42 | puts "requests timed out at [<- $t1]" 43 | } 44 | } 45 | } 46 | 47 | go main 48 | 49 | vwait forever 50 | 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /csp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | csp - Concurrency 4 | 94 | 95 | 97 | 99 | 101 |
102 |

csp(n) 0.1.0 "Concurrency"

103 |

Name

104 |

csp - Golang style concurrency library based on Communicating Sequential Processes

105 |
106 |

Table Of Contents

107 | 139 |
140 |

Synopsis

141 | 164 |
165 |

Description

166 |

The csp package provides two concurrency primitives namely coroutines and channels which allow concurrent programming in the style of Golang.

167 |

The concepts originate in Hoare's Communicating Sequential Processes while the syntax mimics the Golang implementation.

168 |

The CSP concurrency model may be visualized as a set of independent processes (coroutines) sending and receiving messages to the named channels. The control flow in the coroutines is coordinated at the points of sending and receiving messages i.e. the coroutine may need to wait while trying to send or receive. 169 | Since it must work in a single-threaded interpreter, waiting is non-blocking. Instead of blocking a waiting coroutine gives way to other coroutines.

170 |

This concurrency model may also be seen as a generalization of Unix named pipes where processes and pipes correspond to coroutines and channels.

171 |
172 |

Concepts

173 |
174 |
channel
175 |
176 | 177 |
There are two types of channels.
178 |
179 | 180 | 181 |
Unbuffered channels
182 |

The unbuffered channel is a single value container that can be imagined as a rendez-vous venue where the sender must wait for the receiver to collect the message. 183 | By default channels are unbuffered.

184 |
Buffered channels
185 |

The buffered channel behaves like a FIFO queue.

186 |
187 |
188 |
189 | 190 |
Whether receiver need to wait while trying to receive from a channel depends on the channel's internal state:
191 |
192 | 193 |
ready for receive
194 |

The buffered channel is ready for receive when it is not empty. 195 | The unbuffered channel is ready for receive if there exists a sender waiting with a message on this channel.

196 |
ready for send
197 |

The buffered channel is ready for send when it is not full. 198 | The unbuffered channel is ready for send if there is no other sender already waiting on this channel. Note that

199 |
200 |
201 |

Channel is created with:

202 |

::csp::channel chanVar ?size?

203 |

Where the optional parameter size specifies the maximum number of messages that can be stored in the channel. When the channel is full the sender trying to send more messages to it must wait until any receiver offloads the channel. Waiting means that the sender gives way to other coroutines.

204 |

If the size is zero (default) the created channel is unbuffered which means that the sender coroutine always waits for the receiver to collect the message.

205 |

Channel may be closed with:

206 |

channelObj close

207 |

and is destroyed automatically when all messages are received (the channel is drained).

208 |
209 | 210 |
Channel lifecycle is described by 3 possible states:
211 |
212 | 213 |
created
214 |

Once the channel is created you can send to and receive from the channel.

215 |
closed
216 |

When the channel is closed you can still receive from the channel but you cannot send to it. 217 | Trying to send to the closed channel throws an error. 218 | It is responsibility of the library user to close the unused channels.

219 |
destroyed
220 |

The channel does not exist. 221 | After receiving all messages from the closed channel, the channel is destroyed. 222 | Trying to send to or receive from the destroyed channel throws an error.

223 |
224 |
225 |

Note that creating a large number of channels that are properly closed but not emptied may result in a memory leak.

226 |
coroutine
227 |

Coroutine is a procedure executing concurrently with other coroutines. 228 | Coroutine may send messages to and receive messages from channels. Any coroutine may act as a sender or receiver at different times. If channel is not ready a coroutine waits by giving way to other coroutines. This makes the coroutine execution multiplexed at the points of sending to or receiving from channels.

229 |

Coroutine is created with:

230 |

::csp::go procName ?args?

231 |

where procName is the name of the existing Tcl procedure that will be run as a coroutine. 232 | You can create many coroutines from a single Tcl procedure, possibly called with different arguments.

233 |

Coroutine is destroyed when its execution ends.

234 |

We reuse the term coroutine known from Tcl (modelled on Lua) coroutines, but they are are not equivalent. csp coroutines are implemented in terms of Tcl coroutines and it's better not to mix csp and Tcl coroutines in a single program.

235 |
236 |
237 |

COMMANDS

238 |
239 |
::csp::go procName ?args?
240 |

Create a coroutine by calling procName with arguments args. Returns internal name of the coroutine.

241 |
::csp::channel channelVar ?size?
242 |

Create a channel object that will be further referred as channelObj. The name of the object is contained in variable channelVar.

243 |
244 | 245 |
var channelVar
246 |

Variable channelVar that will be created and will contiain the channel object name.

247 |
number size
248 |

Size of the channel buffer understood as the maximum number of messages that can be buffered in the channel. If size is zero (default) the channel is unbuffered.

249 |
250 |

Returns channel object name.

251 |
channelObj close
252 |

Close the channel channelObj. Returns empty string.

253 |
channelObj <- msg
254 |

Send msg to channel channelObj in a coroutine. Returns empty string.

255 |
channelObj <-! msg
256 |

Send msg to channel channelObj in a script (in the Tcl program main control flow). It is implemented using vwait and has many limitations. Use with care and only in simple scenarios. Returns empty string.

257 |
::csp::<- channelObj
258 |

Receive from channel channelObj in a coroutine. Returns the message received from the channel.

259 |
::csp::<-! channelObj
260 |

Receive from channel channelObj in a script (in the Tcl program main control flow). Returns the message received from the channel.

261 |
::csp::select operation body
262 |

Evaluate set of channels to find which channels are ready and run corresponding block of code. Returns the result of evaluation of the block of code.

263 |
264 | 265 |
list operation
266 |

Operation takes one of 3 forms:

267 |

<- channelObj

268 |

for evaluating whether the channelObj is ready for receive, or

269 |

channelObj <-

270 |

for evaluating whether the channelObj is ready for send, or

271 |

default

272 |

for evaluating default case if no channel is ready.

273 |
block body
274 |

Block of code to be evaluated.

275 |
276 |

The select command provides a way to handle multiple channels. It is a switch like statement where channels are evaluated for readiness. The select command makes the coroutine wait until at least one channel is ready. If multiple channels can proceed, select chooses pseudo-randomly. A default clause, if present, executes immediately if no channel is ready.

277 |
::csp::range varName channelObj body
278 |

Receive from channel until closed in a coroutine.

279 |

This is a foreach like construct that iterates by receiving messages from channel one by one until channel is closed. If channel is not ready for receive, range waits.

280 |
::csp::range! varName channelObj body
281 |

Receive from channel until closed in the main control flow.

282 |

A version of range command that can be used outside of a coroutine. It is implemented using vwait and has many limitations. Use with care and only in simple scenarios.

283 |
::csp::timer channelVar interval
284 |

Create a receive-only channel with scheduled message in interval milliseconds. Trying to receive from the channel will cause the coroutine to wait interval milliseconds since creation. Eventually the received message is a Unix epoch time in microseconds. After receiving the message the channel is closed and destroyed.

285 |

Returns the created channel.

286 |
::csp::ticker channelVar interval ?closeafter?
287 |

Create a receive-only channel with scheduled messages every interval milliseconds.

288 |

Returns the created channel. 289 | The optional closeafter argument determines when the channel is closed. It may take one of the 2 forms:

290 |
    291 | 292 |
  • integerNumber that specifies the number of milliseconds after which the channel will be closed

  • 293 |
  • #integerNumber that specifies number of messages after which the channel will be closed

  • 294 |
295 |

If closeafter argument is not provided, the ticker channel emits messages endlessly.

296 |
::csp::-> channelVar
297 |

Creates a channel and returns a new coroutine that may be called with a single argument. The coroutine is meant for integration with callback-driven code and to be used in place of one-time callback. The channel is placed in channelVar and will be destroyed after receiving a single message. The single argument passed to the callback will be available to receive from the created channel.

298 |

Note that there is a limitation in replacing callbacks with -> command: only a single- or zero- argument callbacks can be replaced. In case of zero-argument callbacks an empty string is sent to the channel.

299 |
::csp::->> channelVar ?size?
300 |

Creates a buffered channel of size size and returns a new coroutine that may be used in place of a callback. The coroutine may be called many times and the callback arguments are internally sent to the created channel.

301 |

Note that there is a limitation in replacing callbacks with -> command: only a single- or zero- argument callbacks can be replaced. In case of zero-argument callbacks an empty string is sent to the channel.

302 |
::csp::forward fromChannel toChannel
303 |

Receive messages from fromChannel and send them to toChannel.

304 |
305 |
306 |

EXAMPLES

307 |

Example 1

308 |

Simple message passing over an unbuffered channel

309 |
310 |     package require csp
311 |     namespace import csp::*
312 |  
313 |     proc sender1 {ch} {
314 |         foreach i {1 2 3 4} {
315 |             puts "Sending $i"
316 |             $ch <- $i
317 |         }
318 |         puts "Closing channel"
319 |         $ch close
320 |     }
321 |  
322 |     proc receiver1 {ch} {
323 |         while 1 {
324 |             puts "Receiving [<- $ch]"
325 |         }
326 |     }
327 |  
328 |     # create unbuffered (rendez-vous) channel
329 |     channel ch
330 |     go sender1 $ch
331 |     go receiver1 $ch
332 |  
333 |     vwait forever
334 | 
335 |

Output:

336 |
337 | Sending 1
338 | Receiving 1
339 | Sending 2
340 | Receiving 2
341 | Sending 3
342 | Receiving 3
343 | Sending 4
344 | Receiving 4
345 | Closing channel
346 | 
347 |

The communication between the coroutines is coordinated because the channel is unbuffered. 348 | The sender waits for the receiver.

349 |
350 |

Example 2

351 |

Simple message passing over a buffered channel

352 |
353 |     package require csp
354 |     namespace import csp::*
355 |  
356 |     proc sender1 {ch} {
357 |         foreach i {1 2 3 4} {
358 |             puts "Sending $i"
359 |             $ch <- $i
360 |         }
361 |         puts "Closing channel"
362 |         $ch close
363 |     }
364 |  
365 |     proc receiver1 {ch} {
366 |         while 1 {
367 |             puts "Receiving [<- $ch]"
368 |         }
369 |     }
370 |  
371 |     # create buffered channel of size 2
372 |     channel ch 2
373 |     go sender1 $ch
374 |     go receiver1 $ch
375 |  
376 |     vwait forever
377 | 
378 |

Output:

379 |
380 | Sending 1
381 | Sending 2
382 | Sending 3
383 | Receiving 1
384 | Receiving 2
385 | Sending 4
386 | Closing channel
387 | Receiving 3
388 | Receiving 4
389 | Error: Cannot receive from a drained (empty and closed) channel ::csp::Channel#1
390 | 
391 |

Since the channel is buffered of size 2, the sender waits only on the third attempt.

392 |

Note that the channel was closed but we still receive messages. Only after the channel was emptied, trying to receive from the channel throws an error.

393 |
394 |

Example 3

395 |

Using range for receiving from the channel until closed.

396 |

We can prevent throwing the error in the previous example by using the range command instead of iterating blindly with while. 397 | Also if the channel is buffered we can send all messages first and iterate to receive using range in a single coroutine.

398 |
399 |     package require csp
400 |     namespace import csp::*
401 |  
402 |     proc senderreceiver {ch} {
403 |         foreach i {1 2 3 4} {
404 |             puts "Sending $i"
405 |             $ch <- $i
406 |         }
407 |         puts "Closing channel"
408 |         $ch close
409 |         range msg $ch {
410 |             puts "Message $msg"
411 |         }
412 |         puts "Received all"
413 |     }
414 |  
415 |     channel ch 10
416 |     go senderreceiver $ch
417 |  
418 |     vwait forever
419 | 
420 |

Output:

421 |
422 | Sending 1
423 | Sending 2
424 | Sending 3
425 | Sending 4
426 | Closing channel
427 | Message 1
428 | Message 2
429 | Message 3
430 | Message 4
431 | Received all
432 | 
433 |
434 |

Example 4

435 |

Channels can be used to coordinate future events. We use after to create coroutine that will send to the channel.

436 |

Instead of using direct callback which cannot keep local state we consume events in adder which can keep sum in local variable.

437 |
438 |     package require csp
439 |     namespace import csp::*
440 |  
441 |     proc adder {ch} {
442 |         set sum 0
443 |         while 1 {
444 |             set number [<- $ch]
445 |             incr sum $number
446 |             puts "adder received $number. The sum is $sum"
447 |         }
448 |     }
449 |  
450 |     proc trigger {ch number} {
451 |         $ch <- $number
452 |     }
453 |  
454 |     channel ch
455 |     go adder $ch
456 |     after 1000 go trigger $ch 1
457 |     after 3000 go trigger $ch 3
458 |     after 5000 go trigger $ch 5
459 |     puts "Enter event loop"
460 |  
461 |     vwait forever
462 | 
463 |

Output:

464 |
465 | Enter event loop
466 | adder received 1. The sum is 1
467 | adder received 3. The sum is 4
468 | adder received 5. The sum is 9
469 | 
470 |
471 |

Example 5

472 |

Use timer to create a channel supplying scheduled messages in the future.

473 |
474 |     package require csp
475 |     namespace import csp::*
476 |  
477 |     proc future {ch} {
478 |         try {
479 |             puts "future happened at  [<- $ch]"
480 |             puts "try to receive again:"
481 |             puts "[<- $ch]"
482 |         } on error {out err} {
483 |             puts "error: $out"
484 |         }
485 |     }
486 |  
487 |     timer ch 2000
488 |     go future $ch
489 |     puts "Enter event loop at [clock microseconds]"
490 |  
491 |     vwait forever
492 | 
493 |

Output:

494 |
495 | Enter event loop at 1434472163190638
496 | future happened at  1434472165189759
497 | try to receive again:
498 | error: Cannot receive from a drained (empty and closed) channel ::csp::Channel#1
499 | 
500 |

Instead of scheduling events with after we can use timer to create a special receive only channel. There will be only one message send to this channel after the specified time so we can pass this channel to another coroutine that will wait for that message. The message from the timer channel represents unix epoch time in microseconds. The timer channel will be automatically destroyed after first receive so trying to receive again will throw an error.

501 |
502 |

Example 6

503 |

Using ticker we can create receive only channel from which we can consume timestamp messages at regular intervals.

504 |
505 |     package require csp
506 |     namespace import csp::*
507 |  
508 |     proc future {ch} {
509 |         set count 0
510 |         while 1 {
511 |             incr count
512 |             puts "future $count received at [<- $ch]"
513 |         }
514 |     }
515 |  
516 |     ticker ch 1000
517 |     go future $ch
518 |     puts "Enter event loop at  [clock microseconds]"
519 |   
520 |     vwait forever
521 | 
522 |

Output:

523 |
524 | Enter event loop at  1434472822879684
525 | future 1 received at 1434472823879110
526 | future 2 received at 1434472824882163
527 | future 3 received at 1434472825884246
528 | ...
529 | 
530 |
531 |

Example 7

532 |

ticker command returns the created channel so we can use it in place in combination with range to further simplify the example

533 |
534 |     package require csp
535 |     namespace import csp::*
536 |  
537 |     proc counter {} {
538 |         range t [ticker ch 1000] {
539 |             puts "received $t"
540 |         }
541 |     }
542 |  
543 |     go counter
544 |  
545 |     vwait forever
546 | 
547 |

Output:

548 |
549 | received 1434474325947677
550 | received 1434474326950822
551 | received 1434474327952904
552 | ...
553 | 
554 |
555 |

Example 8

556 |

Another example of using ticker to implement the canonical countdown counter from Tcl wiki.

557 |
558 |     package require Tk
559 |     package require csp
560 |     namespace import csp::*
561 |  
562 |     proc countdown {varName} {
563 |         upvar $varName var
564 |         range _ [ticker t 1000 #10] {
565 |             incr var -1
566 |         }
567 |     }
568 |  
569 |     set count 10
570 |     label .counter -font {Helvetica 72} -width 3 -textvariable count
571 |     grid .counter -padx 100 -pady 100
572 |     go countdown count
573 | 
574 |
575 |

Example 9

576 |

Closing the channel by another scheduled event breaks the range loop

577 |
578 |     package require csp
579 |     namespace import csp::*
580 |  
581 |     proc counter {ch} {
582 |         range t $ch {
583 |             puts "received $t"
584 |         }
585 |         puts "counter exit"
586 |     }
587 |  
588 |     ticker ch 1000
589 |     go counter $ch
590 |     after 4500 $ch close
591 |     puts "Enter event loop at [clock microseconds]"
592 |  
593 |     vwait forever
594 | 
595 |

Output:

596 |
597 | Enter event loop at 1434474384645704
598 | received 1434474385644900
599 | received 1434474386648105
600 | received 1434474387650088
601 | received 1434474388652345
602 | counter exit
603 | 
604 |
605 |

Example 10

606 |

Redirect callback call argument to a channel using -> command.

607 |
608 |     package require http
609 |     package require csp
610 |     namespace import csp::*
611 |  
612 |     proc main {} {
613 |         http::geturl http://securitykiss.com/rest/now -command [-> ch]
614 |         puts "fetched: [http::data [<- $ch]]"
615 |     }
616 |  
617 |     go main
618 |  
619 |     vwait forever
620 | 
621 |

Output:

622 |
623 | fetched: 1434474568
624 | 
625 |

csp package makes it easy to integrate channels and coroutines with existing event driven code. 626 | Using the -> utility command we can make channels work with callback driven commands and at the same time avoid callback hell.

627 |

-> ch creates a channel ch and returns a new coroutine that may be used in place of a callback. 628 | The channel will be destroyed after receiving a single value. 629 | The single argument passed to the callback will be available to receive from the created channel.

630 |

Such code organization promotes local reasoning - it helps writing linear code with local state kept in proc variables. Otherwise the callback would require keeping state in global variables.

631 |

Note that there is a limitation in replacing callbacks with -> command: only a single- or zero- argument callbacks can be replaced. 632 | In case of zero-argument callbacks an empty string is sent to the channel.

633 |

Note that there is no symmetry in <- <-! -> ->> commands. Every one of them has a different purpose.

634 |
635 |

Example 11

636 |

Use select command to choose ready channels.

637 |
638 |     package require http
639 |     package require csp
640 |     namespace import csp::*
641 |  
642 |     proc main {} {
643 |         http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1]
644 |         http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2]
645 |         select {
646 |             <- $ch1 {
647 |                 puts "from first request: [http::data [<- $ch1]]"
648 |             }
649 |             <- $ch2 {
650 |                 puts "from second request: [http::data [<- $ch2]]"
651 |             }
652 |         }
653 |     }
654 |  
655 |     go main
656 |  
657 |     vwait forever
658 | 
659 |

Output:

660 |
661 | from first request: 1434483100
662 | 
663 |

Previous example with callback channels does not extend to making parallel http requests because one waiting channel would prevent receiving from the other. 664 | The select command chooses which of a set of possible send or receive operations will proceed. In this example select command examines two callback channels and depending on which one is ready for receive first, it evaluates corresponding body block.

665 |
666 |

Example 12

667 |

Combine timer created channel with select to enforce timeouts.

668 |
669 |     package require http
670 |     package require csp
671 |     namespace import csp::*
672 |  
673 |     proc main {} {
674 |         http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1]
675 |         http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2]
676 |         timer t1 400
677 |         select {
678 |             <- $ch1 {
679 |                 puts "from first request: [http::data [<- $ch1]]"
680 |             }
681 |             <- $ch2 {
682 |                 puts "from second request: [http::data [<- $ch2]]"
683 |             }
684 |             <- $t1 {
685 |                 puts "requests timed out at [<- $t1]"
686 |             }
687 |         }
688 |     }
689 |  
690 |     go main
691 |  
692 |     vwait forever
693 | 
694 |

Output:

695 |
696 | requests timed out at 1434484003634953
697 | 
698 |

Since select chooses from the set of channels whichever is ready first, by adding the timer created channel to select from, we can implement timeout as in the example above.

699 |
700 |

Example 13

701 |

Use select with the default clause.

702 |
703 |     package require http
704 |     package require csp
705 |     namespace import csp::*
706 |  
707 |     proc DisplayResult {ch1 ch2} {
708 |         set result [select {
709 |             <- $ch1 {
710 |                 http::data [<- $ch1]
711 |             }
712 |             <- $ch2 {
713 |                 http::data [<- $ch2]
714 |             }
715 |             default {
716 |                 subst "no response was ready"
717 |             }
718 |         }]
719 |         puts "DisplayResult: $result"
720 |     }
721 |  
722 |     proc main {} {
723 |         http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1]
724 |         http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2]
725 |         after 400 go DisplayResult $ch1 $ch2
726 |     }
727 |  
728 |     go main
729 |  
730 |     vwait forever
731 | 
732 |

Output:

733 |
734 | DisplayResult: no response was ready
735 | 
736 |

select command is potentially waiting if no channel is ready. Sometimes we need to proceed no matter what so select makes it possible to return without waiting if the default clause is provided. This example also shows that select has a return value. In this case the result returned by select is either HTTP response or the value specified in the default block if no channel is ready.

737 |
738 |

Example 14

739 |

Funnel multiple channels into a single channel using forward command.

740 |
741 |     package require http
742 |     package require csp
743 |     namespace import csp::*
744 |  
745 |     proc main {} {
746 |         set urls {
747 |             http://securitykiss.com
748 |             http://meetup.com
749 |             http://reddit.com
750 |             http://google.com
751 |             http://twitter.com
752 |             http://bitcoin.org
753 |         }
754 |         channel f
755 |         foreach url $urls {
756 |             http::geturl $url -method HEAD -command [-> ch]
757 |             forward $ch $f
758 |         }
759 |         after 200 $f close
760 |         range token $f {
761 |             upvar #0 $token state
762 |             puts "$state(http)\t$state(url)"
763 |         }
764 |         puts "main exit"
765 |     }
766 |  
767 |     go main
768 |  
769 |     vwait forever
770 | 
771 |

Output:

772 |
773 | HTTP/1.1 302 Found  http://google.com/
774 | HTTP/1.1 301 Moved Permanently  http://reddit.com/
775 | HTTP/1.1 301 Moved Permanently  http://securitykiss.com/
776 | main exit
777 | 
778 |

When we want to listen on many channels, especially when they are dynamically created for example per URL as in the above example, select command becomes awkward because it requires specifying logic for every channel.

779 |

In the example above we spawn a HTTP request for every URL and forward messages from individual "callback channels" into the single "funnel channel" f. In this way the responses are available in a single channel so we can apply common logic to the results. We also set the timeout for the requests by closing the "funnel channel" after some time. Responses that don't make it within a specified timeout are ignored.

780 |
781 |

Example 15

782 |

Redirect callback multi call argument to a long-lived channel using ->> command.

783 |
784 |     package require Tk
785 |     package require csp
786 |     namespace import csp::*
787 |  
788 |     proc main {} {
789 |         set number 5
790 |         frame .f
791 |         button .f.left -text <<< -command [->> chleft]
792 |         label .f.lbl -font {Helvetica 24} -text $number
793 |         button .f.right -text >>> -command [->> chright]
794 |         grid .f.left .f.lbl .f.right
795 |         grid .f
796 |         while 1 {
797 |             select {
798 |                 <- $chleft {
799 |                     <- $chleft
800 |                     incr number -1
801 |                 }
802 |                 <- $chright {
803 |                     <- $chright
804 |                     incr number
805 |                 }
806 |             }
807 |             .f.lbl configure -text $number
808 |         }
809 |     }
810 |  
811 |     go main
812 | 
813 |

In previous examples the -> command created short-lived disposable callback channels that could be received from only once. 814 | Often an existing command require a callback that will be called many times over long period of time. In such case ->> comes to play. 815 | It returns a coroutine that may be called many times in place of the callback. Callback argument is passed to the newly created buffered channel that can be later received from to consume the messages (callback arguments).

816 |

In this example similar functionality could be achieved in a simpler way using -textvariable on label but it would require a global variable instead of local number.

817 |

The same limitations regarding callback arguments arity apply as for the -> command.

818 |

Note that there is no symmetry in <- <-! -> ->> commands. Every one of them has a different purpose.

819 |
820 |

Example 16

821 |

Channel operations like <- and range can be used only in coroutines. Using coroutines for channel driven coordination is the recommended way of using csp package.

822 |

It may happen that we need to use channels outside of coroutines. It is possible with corresponding <-! and range! commands but there are caveats. 823 | The "bang" terminated commands are implemented using vwait nested calls and have many limitations. Thus they should be used with extra care and only in simple scenarios. Especially it is not guaranteed that they will work correctly if used inside callbacks.

824 |

In this example we show a simple scenario where receiving from the channel in the main script control flow makes sense as a way to synchronize coroutine termination.

825 |
826 |     package require http
827 |     package require csp
828 |     namespace import csp::*
829 |   
830 |     proc worker {ch_quit} {
831 |         http::geturl http://securitykiss.com/rest/now -command [-> ch]
832 |         puts "fetched: [http::data [<- $ch]]"
833 |         $ch_quit <- 1
834 |     }
835 |   
836 |     # termination coordination channel
837 |     channel ch_quit
838 |   
839 |     go worker $ch_quit
840 |  
841 |     <-! $ch_quit
842 | 
843 |

Output:

844 |
845 | fetched: 1434556219
846 | 
847 |

Without the last line the script would exit immediately without giving the coroutine a chance to fetch the url.

848 |
849 |

Example 17

850 |

Following the "bang" terminated command trail, this example shows how range! command may further simplify the previous countdown counter example.

851 |
852 |     package require Tk
853 |     package require csp
854 |     namespace import csp::*
855 |  
856 |     set count 5
857 |     label .counter -font {Helvetica 72} -width 3 -textvariable count
858 |     grid .counter -padx 100 -pady 100
859 |     range! _ [ticker t 1000 #$count] {
860 |         incr count -1
861 |     }
862 | 
863 |
864 |

Example 18

865 |

A more complex example using the already discussed constructs.

866 |
867 |     # A simple web crawler/scraper demonstrating the csp style programming in Tcl
868 |     # In this example we have 2 coroutines: a crawler and a parser communicating over 2 channels.
869 |     # The crawler receives the url to process from the urls channel and spawns a http request
870 |     # Immediately sends the pair: (url, callback channel from http request) 
871 |     # into a pending requests channel for further processing by the parser.
872 |     # The parser receives the http token from the received callback channel 
873 |     # and fetches the page content from the url in order to extract more urls.
874 |     # The new urls are sent to the urls channel where the crawler takes over again.
875 |   
876 |     package require http
877 |     package require csp
878 |     namespace import csp::*
879 |  
880 |     # The crawler coroutine is initialized with 3 channels:
881 |     # urls - channel with urls waiting to process
882 |     # requests - channel with pending http requests
883 |     # quit - synchronization channel to communicate end of coroutine
884 |     proc crawler {urls requests quit} {
885 |         # list of visited urls
886 |         set visited {}
887 |         range url $urls {
888 |             if {$url ni $visited} {
889 |                 http::geturl $url -command [-> req]
890 |                 lappend visited $url
891 |                 # note we are passing channel object over a channel
892 |                 $requests <- [list $url $req]
893 |             }
894 |         }
895 |         $quit <- 1
896 |     }
897 |  
898 |  
899 |     # The parser coroutine is initialized with 3 channels:
900 |     # urls - channel with urls waiting to process
901 |     # requests - channel with pending http requests
902 |     # quit - synchronization channel to communicate end of coroutine
903 |     proc parser {urls requests quit} {
904 |         set count 0
905 |         range msg $requests {
906 |             lassign $msg url req
907 |             timer timeout 5000
908 |             select {
909 |                 <- $req {
910 |                     set token [<- $req]
911 |                     set data [http::data $token]
912 |                     puts "Fetched URL $url with content size [string length $data] bytes"
913 |                     foreach {_ href} [regexp -nocase -all -inline {href="(.*?)"} $data] {
914 |                         if {![string match http:* $href] && ![string match mailto:* $href]} {
915 |                             # catch error if channel has been closed
916 |                             $urls <- [create_url $url $href]
917 |                         }
918 |                     }
919 |                 }
920 |                 <- $timeout {
921 |                     <- $timeout
922 |                     puts "Request to $url timed out"
923 |                 }
924 |             }
925 |             # we stop after fetching 10 urls
926 |             if {[incr count] >= 10} {
927 |                 $urls close
928 |                 $requests close
929 |             }
930 |         }
931 |         $quit <- 1
932 |     }
933 |  
934 |     # utility function to construct urls
935 |     proc create_url {current href} {
936 |         regexp {(http://[^/]*)(.*)} $current _ base path
937 |         if {[string match /* $href]} {
938 |             return $base$href
939 |         } else {
940 |             return $current$href
941 |         }
942 |     }
943 |  
944 |  
945 |     # channel containing urls to process
946 |     # this channel must have rather large buffer so that the urls to crawl can queue
947 |     channel urls 10000
948 |     # channel containing (url req) pairs representing pending http requests
949 |     # size of this channel determines parallelism i.e. the maximum number of pending requests at the same time
950 |     channel requests 3
951 |     # coordination channels that make the main program wait until coroutines end
952 |     channel crawler_quit
953 |     channel parser_quit
954 |     go crawler $urls $requests $crawler_quit
955 |     go parser $urls $requests $parser_quit
956 |  
957 |     # send the seed url to initiate crawling
958 |     $urls <-! "http://www.tcl.tk/man/tcl8.6/"
959 |  
960 |     # Gracefully exit - wait for coroutines to complete
961 |     <-! $crawler_quit
962 |     <-! $parser_quit
963 | 
964 |

In particular it is worth noting:

965 |
    966 |
  • it is possible to pass a channel object over another channel

  • 967 |
  • use of quit synchronization channel to communicate end of coroutine

  • 968 |
  • closing channels as a way to terminate range iteration

  • 969 |
970 |
971 |
972 |

Keywords

973 |

actors, callback, channel, concurrency, csp, golang

974 |
975 |

Category

976 |

Concurrency

977 |
978 | 981 |
982 | -------------------------------------------------------------------------------- /csp.man: -------------------------------------------------------------------------------- 1 | [comment {-*- tcl -*- doctools manpage}] 2 | [vset VERSION 0.1.0] 3 | [manpage_begin csp n [vset VERSION]] 4 | [keywords csp] 5 | [keywords golang] 6 | [keywords concurrency] 7 | [keywords callback] 8 | [keywords channel] 9 | [keywords actors] 10 | [copyright {2015 SecurityKISS Ltd - MIT License - Feedback and bug reports are welcome}] 11 | [titledesc {Golang style concurrency library based on Communicating Sequential Processes}] 12 | [moddesc {Concurrency}] 13 | [category {Concurrency}] 14 | [require Tcl 8.6] 15 | [require csp [opt [vset VERSION]]] 16 | [description] 17 | [para] 18 | The [package csp] package provides two concurrency primitives namely [term coroutines] and [term channels] which allow concurrent programming in the style of [uri https://en.wikipedia.org/wiki/Go_(programming_language) Golang]. 19 | [para] 20 | The concepts originate in Hoare's [uri https://en.wikipedia.org/wiki/Communicating_sequential_processes {Communicating Sequential Processes}] while the syntax mimics the Golang implementation. 21 | [para] 22 | The CSP concurrency model may be visualized as a set of independent processes (coroutines) sending and receiving messages to the named channels. The control flow in the coroutines is coordinated at the points of sending and receiving messages i.e. the coroutine may need to wait while trying to send or receive. 23 | Since it must work in a single-threaded interpreter, waiting is non-blocking. Instead of blocking the waiting coroutine gives way to other coroutines. 24 | [para] 25 | This concurrency model may also be seen as a generalization of Unix [uri https://en.wikipedia.org/wiki/Named_pipe {named pipes}] where processes and pipes correspond to coroutines and channels. 26 | 27 | [section Concepts] 28 | 29 | [list_begin definitions] 30 | 31 | [def [cmd channel]] 32 | 33 | [list_begin definitions] 34 | [def {There are two types of channels.}] 35 | [list_begin definitions] 36 | 37 | [def [term {Unbuffered channels}]] 38 | [para] 39 | The unbuffered channel is a single value container that can be imagined as a rendez-vous venue where the sender must wait for the receiver to collect the message. 40 | By default channels are unbuffered. 41 | 42 | [def [term {Buffered channels}]] 43 | [para] 44 | The buffered channel behaves like a FIFO queue. 45 | [list_end] 46 | [list_end] 47 | 48 | [para] 49 | [list_begin definitions] 50 | [def {Whether receiver need to wait while trying to receive from a channel depends on the channel's internal state:}] 51 | [list_begin definitions] 52 | [def [term {ready for receive}]] 53 | The buffered channel is ready for receive when it is not empty. 54 | The unbuffered channel is ready for receive if there exists a sender waiting with a message on this channel. 55 | 56 | [def [term {ready for send}]] 57 | The buffered channel is ready for send when it is not full. 58 | The unbuffered channel is ready for send if there is no other sender already waiting on this channel. 59 | [list_end] 60 | [list_end] 61 | 62 | [para] 63 | Channel is created with: 64 | [para] 65 | [cmd ::csp::channel] [arg chanVar] [arg [opt size]] 66 | [para] 67 | Where the optional parameter [arg size] specifies the maximum number of messages that can be stored in the channel. When the channel is full the sender trying to send more messages to it must wait until any receiver offloads the channel. Waiting means that the sender gives way to other coroutines. 68 | [para] 69 | If the size is zero (default) the created channel is unbuffered which means that the sender coroutine always waits for the receiver to collect the message. 70 | [para] 71 | Channel may be closed with: 72 | [para] 73 | [cmd channelObj] [arg close] 74 | [para] 75 | and is destroyed automatically when all messages are received (the channel is drained). 76 | 77 | 78 | 79 | [para] 80 | [list_begin definitions] 81 | [def {Channel lifecycle is described by 3 possible states:}] 82 | [list_begin definitions] 83 | [def [term created]] 84 | Once the channel is created you can send to and receive from the channel. 85 | [def [term closed]] 86 | When the channel is closed you can still receive from the channel but you cannot send to it. 87 | Trying to send to the closed channel throws an error. 88 | It is responsibility of the library user to close the unused channels. 89 | [def [term destroyed]] 90 | The channel does not exist. 91 | After receiving all messages from the closed channel, the channel is destroyed. 92 | Trying to send to or receive from the destroyed channel throws an error. 93 | [list_end] 94 | [list_end] 95 | Note that creating a large number of channels that are properly closed but not emptied may result in a memory leak. 96 | 97 | 98 | [def [cmd coroutine]] 99 | [para] 100 | [term Coroutine] is a procedure executing concurrently with other coroutines. 101 | [term Coroutine] may send messages to and receive messages from [term channels]. Any coroutine may act as a sender or receiver at different times. If [term channel] is not ready a coroutine waits by giving way to other coroutines. This makes the coroutine execution multiplexed at the points of sending to or receiving from channels. 102 | [para] 103 | 104 | [term Coroutine] is created with: 105 | 106 | [para] 107 | [cmd ::csp::go] [arg procName] [arg [opt args]] 108 | 109 | [para] 110 | where [arg procName] is the name of the existing Tcl procedure that will be run as a coroutine. 111 | You can create many coroutines from a single Tcl procedure, possibly called with different arguments. 112 | 113 | [para] 114 | Coroutine is destroyed when its execution ends. 115 | 116 | [para] 117 | We reuse the term [term coroutine] known from Tcl (modelled on Lua) coroutines, but they are are not equivalent. [package csp] coroutines are implemented in terms of Tcl coroutines and it's better not to mix [package csp] and Tcl coroutines in a single program. 118 | [list_end] 119 | 120 | 121 | 122 | [section COMMANDS] 123 | 124 | [para] 125 | 126 | [list_begin definitions] 127 | [call [cmd ::csp::go] [arg procName] [opt [arg args]]] 128 | Create a coroutine by calling [arg procName] with arguments [arg args]. Returns internal name of the coroutine. 129 | 130 | [call [cmd ::csp::channel] [arg channelVar] [opt [arg size]]] 131 | Create a channel object that will be further referred as [cmd channelObj]. The name of the object is contained in variable [arg channelVar]. 132 | [list_begin arguments] 133 | [arg_def var channelVar] 134 | Variable channelVar that will be created and will contiain the channel object name. 135 | [arg_def number size] 136 | Size of the channel buffer understood as the maximum number of messages that can be buffered in the channel. If size is zero (default) the channel is unbuffered. 137 | [list_end] 138 | Returns channel object name. 139 | 140 | [call [cmd channelObj] [arg close]] 141 | Close the channel [arg channelObj]. Returns empty string. 142 | 143 | [call [cmd channelObj] [arg <-] [arg msg]] 144 | Send [arg msg] to channel [arg channelObj] in a coroutine. Returns empty string. 145 | 146 | [call [cmd channelObj] [arg <-!] [arg msg]] 147 | Send [arg msg] to channel [arg channelObj] in a script (in the Tcl program main control flow). It is implemented using vwait and has many limitations. Use with care and only in simple scenarios. Returns empty string. 148 | 149 | [call [cmd ::csp::<-] [arg channelObj]] 150 | Receive from channel [arg channelObj] in a coroutine. Returns the message received from the channel. 151 | 152 | [call [cmd ::csp::<-!] [arg channelObj]] 153 | Receive from channel [arg channelObj] in a script (in the Tcl program main control flow). Returns the message received from the channel. 154 | 155 | [call [cmd ::csp::select] [arg operation] [arg body]] 156 | Evaluate set of channels to find which channels are ready and run corresponding block of code. Returns the result of evaluation of the block of code. 157 | [para] 158 | [list_begin arguments] 159 | [arg_def list operation] 160 | Operation takes one of 3 forms: 161 | [para] 162 | [cmd <-] [arg channelObj] 163 | [para] 164 | for evaluating whether the [arg channelObj] is ready for receive, or 165 | [para] 166 | [arg channelObj] [cmd <-] 167 | [para] 168 | for evaluating whether the [arg channelObj] is ready for send, or 169 | [para] 170 | default 171 | [para] 172 | for evaluating default case if no channel is ready. 173 | [arg_def block body] 174 | Block of code to be evaluated. 175 | [list_end] 176 | [para] 177 | The select command provides a way to handle multiple channels. It is a switch like statement where channels are evaluated for readiness. The select command makes the coroutine wait until at least one channel is ready. If multiple channels can proceed, [cmd select] chooses pseudo-randomly. A default clause, if present, executes immediately if no channel is ready. 178 | 179 | [call [cmd ::csp::range] [arg varName] [arg channelObj] [arg body]] 180 | Receive from channel until closed in a coroutine. 181 | [para] 182 | This is a [cmd foreach] like construct that iterates by receiving messages from channel one by one until channel is closed. If channel is not ready for receive, [cmd range] waits. 183 | 184 | [call [cmd ::csp::range!] [arg varName] [arg channelObj] [arg body]] 185 | Receive from channel until closed in the main control flow. 186 | [para] 187 | A version of [cmd range] command that can be used outside of a coroutine. It is implemented using vwait and has many limitations. Use with care and only in simple scenarios. 188 | 189 | [call [cmd ::csp::timer] [arg channelVar] [arg interval]] 190 | Create a receive-only channel with scheduled message in [arg interval] milliseconds. Trying to receive from the channel will cause the coroutine to wait [arg interval] milliseconds since creation. Eventually the received message is a Unix epoch time in microseconds. After receiving the message the channel is closed and destroyed. 191 | [para] 192 | Returns the created channel. 193 | 194 | [call [cmd ::csp::ticker] [arg channelVar] [arg interval] [arg [opt closeafter]]] 195 | Create a receive-only channel with scheduled messages every [arg interval] milliseconds. 196 | [para] 197 | Returns the created channel. 198 | 199 | The optional [arg closeafter] argument determines when the channel is closed. It may take one of the 2 forms: 200 | [list_begin itemized] 201 | [item] [arg integerNumber] that specifies the number of milliseconds after which the channel will be closed 202 | [item] [arg #integerNumber] that specifies number of messages after which the channel will be closed 203 | [list_end] 204 | If [arg closeafter] argument is not provided, the [cmd ticker] channel emits messages endlessly. 205 | 206 | [call [cmd ::csp::->] [arg channelVar]] 207 | Creates a channel and returns a new coroutine that may be called with a single argument. The coroutine is meant for integration with callback-driven code and to be used in place of one-time callback. The channel is placed in [arg channelVar] and will be destroyed after receiving a single message. The single argument passed to the callback will be available to receive from the created channel. 208 | [para] 209 | Note that there is a limitation in replacing callbacks with -> command: only a single- or zero- argument callbacks can be replaced. In case of zero-argument callbacks an empty string is sent to the channel. 210 | 211 | [call [cmd ::csp::->>] [arg channelVar] [opt [arg size]]] 212 | Creates a buffered channel of size [arg size] and returns a new coroutine that may be used in place of a callback. The coroutine may be called many times and the callback arguments are internally sent to the created channel. 213 | [para] 214 | Note that there is a limitation in replacing callbacks with -> command: only a single- or zero- argument callbacks can be replaced. In case of zero-argument callbacks an empty string is sent to the channel. 215 | 216 | [call [cmd ::csp::forward] [arg fromChannel] [arg toChannel]] 217 | Receive messages from [arg fromChannel] and send them to [arg toChannel]. 218 | 219 | [para] 220 | 221 | [list_end] 222 | 223 | [section EXAMPLES] 224 | [subsection {Example 1}] 225 | Simple message passing over an unbuffered channel 226 | [example { 227 | package require csp 228 | namespace import csp::* 229 | 230 | proc sender1 {ch} { 231 | foreach i {1 2 3 4} { 232 | puts "Sending $i" 233 | $ch <- $i 234 | } 235 | puts "Closing channel" 236 | $ch close 237 | } 238 | 239 | proc receiver1 {ch} { 240 | while 1 { 241 | puts "Receiving [<- $ch]" 242 | } 243 | } 244 | 245 | # create unbuffered (rendez-vous) channel 246 | channel ch 247 | go sender1 $ch 248 | go receiver1 $ch 249 | 250 | vwait forever 251 | }] 252 | 253 | Output: 254 | 255 | [example { 256 | Sending 1 257 | Receiving 1 258 | Sending 2 259 | Receiving 2 260 | Sending 3 261 | Receiving 3 262 | Sending 4 263 | Receiving 4 264 | Closing channel 265 | }] 266 | 267 | The communication between the coroutines is coordinated because the channel is unbuffered. 268 | The sender waits for the receiver. 269 | 270 | 271 | [subsection {Example 2}] 272 | Simple message passing over a buffered channel 273 | 274 | [example { 275 | package require csp 276 | namespace import csp::* 277 | 278 | proc sender1 {ch} { 279 | foreach i {1 2 3 4} { 280 | puts "Sending $i" 281 | $ch <- $i 282 | } 283 | puts "Closing channel" 284 | $ch close 285 | } 286 | 287 | proc receiver1 {ch} { 288 | while 1 { 289 | puts "Receiving [<- $ch]" 290 | } 291 | } 292 | 293 | # create buffered channel of size 2 294 | channel ch 2 295 | go sender1 $ch 296 | go receiver1 $ch 297 | 298 | vwait forever 299 | }] 300 | 301 | Output: 302 | 303 | [example { 304 | Sending 1 305 | Sending 2 306 | Sending 3 307 | Receiving 1 308 | Receiving 2 309 | Sending 4 310 | Closing channel 311 | Receiving 3 312 | Receiving 4 313 | Error: Cannot receive from a drained (empty and closed) channel ::csp::Channel#1 314 | }] 315 | [para] 316 | Since the channel is buffered of size 2, the sender waits only on the third attempt. 317 | [para] 318 | Note that the channel was closed but we still receive messages. Only after the channel was emptied, trying to receive from the channel throws an error. 319 | 320 | 321 | 322 | 323 | [subsection {Example 3}] 324 | [para] 325 | Using [cmd range] for receiving from the channel until closed. 326 | [para] 327 | We can prevent throwing the error in the previous example by using the [cmd range] command instead of iterating blindly with [cmd while]. 328 | Also if the channel is buffered we can send all messages first and iterate to receive using [cmd range] in a single coroutine. 329 | 330 | [example { 331 | package require csp 332 | namespace import csp::* 333 | 334 | proc senderreceiver {ch} { 335 | foreach i {1 2 3 4} { 336 | puts "Sending $i" 337 | $ch <- $i 338 | } 339 | puts "Closing channel" 340 | $ch close 341 | range msg $ch { 342 | puts "Message $msg" 343 | } 344 | puts "Received all" 345 | } 346 | 347 | channel ch 10 348 | go senderreceiver $ch 349 | 350 | vwait forever 351 | }] 352 | 353 | Output: 354 | 355 | [example { 356 | Sending 1 357 | Sending 2 358 | Sending 3 359 | Sending 4 360 | Closing channel 361 | Message 1 362 | Message 2 363 | Message 3 364 | Message 4 365 | Received all 366 | }] 367 | 368 | 369 | 370 | 371 | 372 | [subsection {Example 4}] 373 | [para] 374 | Channels can be used to coordinate future events. We use [cmd after] to create coroutine that will send to the channel. 375 | [para] 376 | Instead of using direct callback which cannot keep local state we consume events in [cmd adder] which can keep sum in local variable. 377 | [para] 378 | [example { 379 | package require csp 380 | namespace import csp::* 381 | 382 | proc adder {ch} { 383 | set sum 0 384 | while 1 { 385 | set number [<- $ch] 386 | incr sum $number 387 | puts "adder received $number. The sum is $sum" 388 | } 389 | } 390 | 391 | proc trigger {ch number} { 392 | $ch <- $number 393 | } 394 | 395 | channel ch 396 | go adder $ch 397 | after 1000 go trigger $ch 1 398 | after 3000 go trigger $ch 3 399 | after 5000 go trigger $ch 5 400 | puts "Enter event loop" 401 | 402 | vwait forever 403 | }] 404 | 405 | Output: 406 | 407 | [example { 408 | Enter event loop 409 | adder received 1. The sum is 1 410 | adder received 3. The sum is 4 411 | adder received 5. The sum is 9 412 | }] 413 | 414 | 415 | 416 | 417 | 418 | [subsection {Example 5}] 419 | [para] 420 | Use [cmd timer] to create a channel supplying scheduled messages in the future. 421 | [para] 422 | [example { 423 | package require csp 424 | namespace import csp::* 425 | 426 | proc future {ch} { 427 | try { 428 | puts "future happened at [<- $ch]" 429 | puts "try to receive again:" 430 | puts "[<- $ch]" 431 | } on error {out err} { 432 | puts "error: $out" 433 | } 434 | } 435 | 436 | timer ch 2000 437 | go future $ch 438 | puts "Enter event loop at [clock microseconds]" 439 | 440 | vwait forever 441 | }] 442 | 443 | Output: 444 | 445 | [example { 446 | Enter event loop at 1434472163190638 447 | future happened at 1434472165189759 448 | try to receive again: 449 | error: Cannot receive from a drained (empty and closed) channel ::csp::Channel#1 450 | }] 451 | [para] 452 | 453 | Instead of scheduling events with [cmd after] we can use [cmd timer] to create a special receive only channel. There will be only one message send to this channel after the specified time so we can pass this channel to another coroutine that will wait for that message. The message from the timer channel represents unix epoch time in microseconds. The timer channel will be automatically destroyed after first receive so trying to receive again will throw an error. 454 | 455 | 456 | 457 | 458 | 459 | [subsection {Example 6}] 460 | [para] 461 | Using [cmd ticker] we can create receive only channel from which we can consume timestamp messages at regular intervals. 462 | 463 | [example { 464 | package require csp 465 | namespace import csp::* 466 | 467 | proc future {ch} { 468 | set count 0 469 | while 1 { 470 | incr count 471 | puts "future $count received at [<- $ch]" 472 | } 473 | } 474 | 475 | ticker ch 1000 476 | go future $ch 477 | puts "Enter event loop at [clock microseconds]" 478 | 479 | vwait forever 480 | }] 481 | 482 | Output: 483 | 484 | [example { 485 | Enter event loop at 1434472822879684 486 | future 1 received at 1434472823879110 487 | future 2 received at 1434472824882163 488 | future 3 received at 1434472825884246 489 | ... 490 | }] 491 | 492 | 493 | 494 | 495 | [subsection {Example 7}] 496 | [para] 497 | 498 | [cmd ticker] command returns the created channel so we can use it in place in combination with [cmd range] to further simplify the example 499 | 500 | [example { 501 | package require csp 502 | namespace import csp::* 503 | 504 | proc counter {} { 505 | range t [ticker ch 1000] { 506 | puts "received $t" 507 | } 508 | } 509 | 510 | go counter 511 | 512 | vwait forever 513 | }] 514 | 515 | Output: 516 | 517 | [example { 518 | received 1434474325947677 519 | received 1434474326950822 520 | received 1434474327952904 521 | ... 522 | }] 523 | 524 | 525 | 526 | 527 | 528 | [subsection {Example 8}] 529 | [para] 530 | 531 | Another example of using [cmd ticker] to implement the canonical countdown counter from [uri http://wiki.tcl.tk/946 {Tcl wiki}]. 532 | 533 | [example { 534 | package require Tk 535 | package require csp 536 | namespace import csp::* 537 | 538 | proc countdown {varName} { 539 | upvar $varName var 540 | range _ [ticker t 1000 #10] { 541 | incr var -1 542 | } 543 | } 544 | 545 | set count 10 546 | label .counter -font {Helvetica 72} -width 3 -textvariable count 547 | grid .counter -padx 100 -pady 100 548 | go countdown count 549 | }] 550 | 551 | 552 | 553 | 554 | 555 | 556 | [subsection {Example 9}] 557 | [para] 558 | Closing the channel by another scheduled event breaks the [cmd range] loop 559 | 560 | [example { 561 | package require csp 562 | namespace import csp::* 563 | 564 | proc counter {ch} { 565 | range t $ch { 566 | puts "received $t" 567 | } 568 | puts "counter exit" 569 | } 570 | 571 | ticker ch 1000 572 | go counter $ch 573 | after 4500 $ch close 574 | puts "Enter event loop at [clock microseconds]" 575 | 576 | vwait forever 577 | }] 578 | 579 | Output: 580 | 581 | [example { 582 | Enter event loop at 1434474384645704 583 | received 1434474385644900 584 | received 1434474386648105 585 | received 1434474387650088 586 | received 1434474388652345 587 | counter exit 588 | }] 589 | 590 | 591 | 592 | [subsection {Example 10}] 593 | [para] 594 | Redirect callback call argument to a channel using [cmd ->] command. 595 | 596 | [example { 597 | package require http 598 | package require csp 599 | namespace import csp::* 600 | 601 | proc main {} { 602 | http::geturl http://securitykiss.com/rest/now -command [-> ch] 603 | puts "fetched: [http::data [<- $ch]]" 604 | } 605 | 606 | go main 607 | 608 | vwait forever 609 | }] 610 | 611 | Output: 612 | 613 | [example { 614 | fetched: 1434474568 615 | }] 616 | 617 | [para] 618 | [package csp] package makes it easy to integrate channels and coroutines with existing event driven code. 619 | Using the [cmd ->] utility command we can make channels work with callback driven commands and at the same time avoid callback hell. 620 | [para] 621 | [cmd ->] [arg ch] creates a channel ch and returns a new coroutine that may be used in place of a callback. 622 | The channel will be destroyed after receiving a single value. 623 | The single argument passed to the callback will be available to receive from the created channel. 624 | [para] 625 | Such code organization promotes local reasoning - it helps writing linear code with local state kept in proc variables. Otherwise the callback would require keeping state in global variables. 626 | [para] 627 | Note that there is a limitation in replacing callbacks with [cmd ->] command: only a single- or zero- argument callbacks can be replaced. 628 | In case of zero-argument callbacks an empty string is sent to the channel. 629 | [para] 630 | Note that there is no symmetry in <- <-! -> ->> commands. Every one of them has a different purpose. 631 | 632 | 633 | [subsection {Example 11}] 634 | [para] 635 | Use [cmd select] command to choose ready channels. 636 | 637 | [example { 638 | package require http 639 | package require csp 640 | namespace import csp::* 641 | 642 | proc main {} { 643 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1] 644 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2] 645 | select { 646 | <- $ch1 { 647 | puts "from first request: [http::data [<- $ch1]]" 648 | } 649 | <- $ch2 { 650 | puts "from second request: [http::data [<- $ch2]]" 651 | } 652 | } 653 | } 654 | 655 | go main 656 | 657 | vwait forever 658 | }] 659 | 660 | Output: 661 | 662 | [example { 663 | from first request: 1434483100 664 | }] 665 | 666 | [para] 667 | Previous example with callback channels does not extend to making parallel http requests because one waiting channel would prevent receiving from the other. 668 | The [cmd select] command chooses which of a set of possible send or receive operations will proceed. In this example [cmd select] command examines two callback channels and depending on which one is ready for receive first, it evaluates corresponding body block. 669 | 670 | 671 | [subsection {Example 12}] 672 | [para] 673 | Combine [cmd timer] created channel with [cmd select] to enforce timeouts. 674 | [example { 675 | package require http 676 | package require csp 677 | namespace import csp::* 678 | 679 | proc main {} { 680 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1] 681 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2] 682 | timer t1 400 683 | select { 684 | <- $ch1 { 685 | puts "from first request: [http::data [<- $ch1]]" 686 | } 687 | <- $ch2 { 688 | puts "from second request: [http::data [<- $ch2]]" 689 | } 690 | <- $t1 { 691 | puts "requests timed out at [<- $t1]" 692 | } 693 | } 694 | } 695 | 696 | go main 697 | 698 | vwait forever 699 | }] 700 | 701 | Output: 702 | 703 | [example { 704 | requests timed out at 1434484003634953 705 | }] 706 | [para] 707 | Since [cmd select] chooses from the set of channels whichever is ready first, by adding the [cmd timer] created channel to select from, we can implement timeout as in the example above. 708 | 709 | 710 | 711 | [subsection {Example 13}] 712 | [para] 713 | Use [cmd select] with the default clause. 714 | 715 | [example { 716 | package require http 717 | package require csp 718 | namespace import csp::* 719 | 720 | proc DisplayResult {ch1 ch2} { 721 | set result [select { 722 | <- $ch1 { 723 | http::data [<- $ch1] 724 | } 725 | <- $ch2 { 726 | http::data [<- $ch2] 727 | } 728 | default { 729 | subst "no response was ready" 730 | } 731 | }] 732 | puts "DisplayResult: $result" 733 | } 734 | 735 | proc main {} { 736 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch1] 737 | http::geturl http://securitykiss.com/rest/slow/now -command [-> ch2] 738 | after 400 go DisplayResult $ch1 $ch2 739 | } 740 | 741 | go main 742 | 743 | vwait forever 744 | }] 745 | 746 | Output: 747 | 748 | [example { 749 | DisplayResult: no response was ready 750 | }] 751 | 752 | [para] 753 | [cmd select] command is potentially waiting if no channel is ready. Sometimes we need to proceed no matter what so [cmd select] makes it possible to return without waiting if the [cmd default] clause is provided. This example also shows that [cmd select] has a return value. In this case the result returned by [cmd select] is either HTTP response or the value specified in the default block if no channel is ready. 754 | 755 | 756 | 757 | 758 | 759 | [subsection {Example 14}] 760 | [para] 761 | Funnel multiple channels into a single channel using [cmd forward] command. 762 | 763 | [example { 764 | package require http 765 | package require csp 766 | namespace import csp::* 767 | 768 | proc main {} { 769 | set urls { 770 | http://securitykiss.com 771 | http://meetup.com 772 | http://reddit.com 773 | http://google.com 774 | http://twitter.com 775 | http://bitcoin.org 776 | } 777 | channel f 778 | foreach url $urls { 779 | http::geturl $url -method HEAD -command [-> ch] 780 | forward $ch $f 781 | } 782 | after 200 $f close 783 | range token $f { 784 | upvar #0 $token state 785 | puts "$state(http)\t$state(url)" 786 | } 787 | puts "main exit" 788 | } 789 | 790 | go main 791 | 792 | vwait forever 793 | }] 794 | 795 | Output: 796 | 797 | [example { 798 | HTTP/1.1 302 Found http://google.com/ 799 | HTTP/1.1 301 Moved Permanently http://reddit.com/ 800 | HTTP/1.1 301 Moved Permanently http://securitykiss.com/ 801 | main exit 802 | }] 803 | 804 | [para] 805 | When we want to listen on many channels, especially when they are dynamically created for example per URL as in the above example, [cmd select] command becomes awkward because it requires specifying logic for every channel. 806 | [para] 807 | In the example above we spawn a HTTP request for every URL and forward messages from individual "callback channels" into the single "funnel channel" [arg f]. In this way the responses are available in a single channel so we can apply common logic to the results. We also set the timeout for the requests by closing the "funnel channel" after some time. Responses that don't make it within a specified timeout are ignored. 808 | 809 | 810 | 811 | [subsection {Example 15}] 812 | [para] 813 | Redirect callback multi call argument to a long-lived channel using [cmd ->>] command. 814 | 815 | [example { 816 | package require Tk 817 | package require csp 818 | namespace import csp::* 819 | 820 | proc main {} { 821 | set number 5 822 | frame .f 823 | button .f.left -text <<< -command [->> chleft] 824 | label .f.lbl -font {Helvetica 24} -text $number 825 | button .f.right -text >>> -command [->> chright] 826 | grid .f.left .f.lbl .f.right 827 | grid .f 828 | while 1 { 829 | select { 830 | <- $chleft { 831 | <- $chleft 832 | incr number -1 833 | } 834 | <- $chright { 835 | <- $chright 836 | incr number 837 | } 838 | } 839 | .f.lbl configure -text $number 840 | } 841 | } 842 | 843 | go main 844 | }] 845 | 846 | [para] 847 | In previous examples the [cmd ->] command created short-lived disposable callback channels that could be received from only once. 848 | Often an existing command require a callback that will be called many times over long period of time. In such case [cmd ->>] comes to play. 849 | It returns a coroutine that may be called many times in place of the callback. Callback argument is passed to the newly created buffered channel that can be later received from to consume the messages (callback arguments). 850 | [para] 851 | In this example similar functionality could be achieved in a simpler way using [arg -textvariable] on [cmd label] but it would require a global variable instead of local [arg number]. 852 | [para] 853 | The same limitations regarding callback arguments arity apply as for the [cmd ->] command. 854 | [para] 855 | Note that there is no symmetry in <- <-! -> ->> commands. Every one of them has a different purpose. 856 | 857 | 858 | 859 | 860 | [subsection {Example 16}] 861 | [para] 862 | Channel operations like [cmd <-] and [cmd range] can be used only in coroutines. Using coroutines for channel driven coordination is the recommended way of using [package csp] package. 863 | [para] 864 | It may happen that we need to use channels outside of coroutines. It is possible with corresponding [cmd <-!] and [cmd range!] commands but there are caveats. 865 | The "bang" terminated commands are implemented using vwait nested calls and have many limitations. Thus they should be used with extra care and only in simple scenarios. Especially it is not guaranteed that they will work correctly if used inside callbacks. 866 | [para] 867 | In this example we show a simple scenario where receiving from the channel in the main script control flow makes sense as a way to synchronize coroutine termination. 868 | 869 | [example { 870 | package require http 871 | package require csp 872 | namespace import csp::* 873 | 874 | proc worker {ch_quit} { 875 | http::geturl http://securitykiss.com/rest/now -command [-> ch] 876 | puts "fetched: [http::data [<- $ch]]" 877 | $ch_quit <- 1 878 | } 879 | 880 | # termination coordination channel 881 | channel ch_quit 882 | 883 | go worker $ch_quit 884 | 885 | <-! $ch_quit 886 | }] 887 | 888 | Output: 889 | 890 | [example { 891 | fetched: 1434556219 892 | }] 893 | 894 | Without the last line the script would exit immediately without giving the coroutine a chance to fetch the url. 895 | 896 | 897 | 898 | [subsection {Example 17}] 899 | [para] 900 | Following the "bang" terminated command trail, this example shows how [cmd range!] command may further simplify the previous countdown counter example. 901 | 902 | [example { 903 | package require Tk 904 | package require csp 905 | namespace import csp::* 906 | 907 | set count 5 908 | label .counter -font {Helvetica 72} -width 3 -textvariable count 909 | grid .counter -padx 100 -pady 100 910 | range! _ [ticker t 1000 #$count] { 911 | incr count -1 912 | } 913 | }] 914 | 915 | 916 | 917 | [subsection {Example 18}] 918 | [para] 919 | A more complex example using the already discussed constructs. 920 | 921 | [example { 922 | # A simple web crawler/scraper demonstrating the csp style programming in Tcl 923 | # In this example we have 2 coroutines: a crawler and a parser communicating over 2 channels. 924 | # The crawler receives the url to process from the urls channel and spawns a http request 925 | # Immediately sends the pair: (url, callback channel from http request) 926 | # into a pending requests channel for further processing by the parser. 927 | # The parser receives the http token from the received callback channel 928 | # and fetches the page content from the url in order to extract more urls. 929 | # The new urls are sent to the urls channel where the crawler takes over again. 930 | 931 | package require http 932 | package require csp 933 | namespace import csp::* 934 | 935 | # The crawler coroutine is initialized with 3 channels: 936 | # urls - channel with urls waiting to process 937 | # requests - channel with pending http requests 938 | # quit - synchronization channel to communicate end of coroutine 939 | proc crawler {urls requests quit} { 940 | # list of visited urls 941 | set visited {} 942 | range url $urls { 943 | if {$url ni $visited} { 944 | http::geturl $url -command [-> req] 945 | lappend visited $url 946 | # note we are passing channel object over a channel 947 | $requests <- [list $url $req] 948 | } 949 | } 950 | $quit <- 1 951 | } 952 | 953 | 954 | # The parser coroutine is initialized with 3 channels: 955 | # urls - channel with urls waiting to process 956 | # requests - channel with pending http requests 957 | # quit - synchronization channel to communicate end of coroutine 958 | proc parser {urls requests quit} { 959 | set count 0 960 | range msg $requests { 961 | lassign $msg url req 962 | timer timeout 5000 963 | select { 964 | <- $req { 965 | set token [<- $req] 966 | set data [http::data $token] 967 | puts "Fetched URL $url with content size [string length $data] bytes" 968 | foreach {_ href} [regexp -nocase -all -inline {href="(.*?)"} $data] { 969 | if {![string match http:* $href] && ![string match mailto:* $href]} { 970 | # catch error if channel has been closed 971 | $urls <- [create_url $url $href] 972 | } 973 | } 974 | } 975 | <- $timeout { 976 | <- $timeout 977 | puts "Request to $url timed out" 978 | } 979 | } 980 | # we stop after fetching 10 urls 981 | if {[incr count] >= 10} { 982 | $urls close 983 | $requests close 984 | } 985 | } 986 | $quit <- 1 987 | } 988 | 989 | # utility function to construct urls 990 | proc create_url {current href} { 991 | regexp {(http://[^/]*)(.*)} $current _ base path 992 | if {[string match /* $href]} { 993 | return $base$href 994 | } else { 995 | return $current$href 996 | } 997 | } 998 | 999 | 1000 | # channel containing urls to process 1001 | # this channel must have rather large buffer so that the urls to crawl can queue 1002 | channel urls 10000 1003 | # channel containing (url req) pairs representing pending http requests 1004 | # size of this channel determines parallelism i.e. the maximum number of pending requests at the same time 1005 | channel requests 3 1006 | # coordination channels that make the main program wait until coroutines end 1007 | channel crawler_quit 1008 | channel parser_quit 1009 | go crawler $urls $requests $crawler_quit 1010 | go parser $urls $requests $parser_quit 1011 | 1012 | # send the seed url to initiate crawling 1013 | $urls <-! "http://www.tcl.tk/man/tcl8.6/" 1014 | 1015 | # Gracefully exit - wait for coroutines to complete 1016 | <-! $crawler_quit 1017 | <-! $parser_quit 1018 | }] 1019 | 1020 | In particular it is worth noting: 1021 | [list_begin itemized] 1022 | [item] it is possible to pass a channel object over another channel 1023 | [item] use of [arg quit] synchronization channel to communicate end of coroutine 1024 | [item] closing channels as a way to terminate [cmd range] iteration 1025 | [list_end] 1026 | 1027 | 1028 | [manpage_end] 1029 | -------------------------------------------------------------------------------- /csp.tcl: -------------------------------------------------------------------------------- 1 | # Copyrite (c) 2015 SecurityKISS Ltd (http://www.securitykiss.com) 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Yes, Mr patent attorney, you have nothing to do here. Find a decent job instead. 6 | # Fight intellectual "property". 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | package require Tcl 8.6 28 | 29 | package provide csp 0.1.0 30 | 31 | namespace eval csp { 32 | # channel as a list/queue for buffered channel or 33 | # venue container for rendez-vous channel (non-buffered size 0 channel) 34 | variable Channel 35 | array set Channel {} 36 | # channel capacity (buffer size) 37 | variable ChannelCap 38 | array set ChannelCap {} 39 | variable ChannelReadOnly 40 | array set ChannelReadOnly {} 41 | variable Routine 42 | array set Routine {} 43 | # counter/uid to produce unique Routine and Channel names 44 | variable Uid 0 45 | 46 | namespace export go channel select timer ticker tickernow range range! <- <-! -> ->> forward 47 | namespace ensemble create 48 | } 49 | 50 | 51 | proc ::csp::ChannelProxy {ch operator val} { 52 | if {$operator eq "close"} { 53 | # delete channel command == channel closed 54 | rename $ch "" 55 | # let the CDrained purge the channel if empty 56 | CDrained $ch 57 | SetResume 58 | return 59 | } 60 | CheckOperator $operator 61 | if {[CReadOnly $ch]} { 62 | error "Cannot send to read only channel $ch" 63 | } 64 | 65 | while {![CSendReady $ch]} { 66 | CheckClosed $ch 67 | Wait$operator 68 | } 69 | CAppend $ch $val 70 | SetResume 71 | # post-send extra logic for rendez-vous channels 72 | if {![CBuffered $ch]} { 73 | # wait again for container empty (once receiver collected the value) 74 | while {![CEmpty $ch]} { 75 | CheckClosed $ch 76 | Wait$operator 77 | } 78 | } 79 | return 80 | } 81 | 82 | 83 | 84 | proc ::csp::Wait<- {} { 85 | yield 86 | } 87 | 88 | proc ::csp::Wait<-! {} { 89 | vwait ::csp::resume 90 | } 91 | 92 | proc ::csp::IsOperator {op} { 93 | return [expr {$op in {<- <-!}}] 94 | } 95 | 96 | proc ::csp::CheckOperator {op} { 97 | if {![IsOperator $op]} { 98 | error "Unrecognized operator $op. Should be <- or <-!" 99 | } 100 | if {[info coroutine] eq ""} { 101 | if {$op eq "<-"} { 102 | error "<- can only be used in a coroutine" 103 | } 104 | } else { 105 | if {$op eq "<-!"} { 106 | error "<-! should not be used in a coroutine" 107 | } 108 | } 109 | } 110 | 111 | # throw error if channel is closed 112 | proc ::csp::CheckClosed {ch} { 113 | if {[CClosed $ch]} { 114 | error "Cannot send to the closed channel $ch" 115 | } 116 | } 117 | 118 | # throw error if incorrect channel name 119 | proc ::csp::CheckName {ch} { 120 | if {![regexp {::csp::Channel#\d+} $ch]} { 121 | error "Wrong channel name: $ch" 122 | } 123 | } 124 | 125 | proc ::csp::CSendReady {ch} { 126 | if {[CClosed $ch]} { 127 | return 0 128 | } 129 | if {[CBuffered $ch]} { 130 | return [expr {! [CFull $ch]}] 131 | } else { 132 | return [CEmpty $ch] 133 | } 134 | } 135 | 136 | proc ::csp::CReceiveReady {ch} { 137 | CheckName $ch 138 | return [expr {![CEmpty $ch]}] 139 | } 140 | 141 | # remove the channel completely 142 | proc ::csp::CPurge {ch} { 143 | variable Channel 144 | variable ChannelCap 145 | variable ChannelReadOnly 146 | CheckName $ch 147 | catch {unset Channel($ch)} 148 | catch {unset ChannelCap($ch)} 149 | catch {unset ChannelReadOnly($ch)} 150 | catch {rename $ch ""} 151 | catch {SetResume} 152 | } 153 | 154 | 155 | # channel chlist ?cap? 156 | # Create channel(s) (with internal name) and place that name in given var 157 | # the default buffer size (capacity) is zero which means rendez-vous channel 158 | proc ::csp::channel {chVars {cap 0}} { 159 | variable Channel 160 | variable ChannelCap 161 | variable CTemplate 162 | lmap chVar $chVars { 163 | upvar $chVar ch 164 | set ch [NewChannel] 165 | # initialize channel as a list or do nothing if exists 166 | set Channel($ch) {} 167 | set ChannelCap($ch) $cap 168 | # create channel object from the template 169 | namespace eval ::csp [string map [list %CHANNEL% $ch] { 170 | proc %CHANNEL% {operator {val ""}} { 171 | ::csp::ChannelProxy %CHANNEL% $operator $val 172 | } 173 | }] 174 | } 175 | } 176 | 177 | # Sending will be possible only by using internal CAppend 178 | proc ::csp::CMakeReadOnly {ch} { 179 | variable ChannelReadOnly 180 | set ChannelReadOnly($ch) 1 181 | } 182 | 183 | proc ::csp::CReadOnly {ch} { 184 | variable ChannelReadOnly 185 | return [info exists ChannelReadOnly($ch)] 186 | } 187 | 188 | 189 | 190 | # A channel is considered closed if no longer exists (but its name is correct) 191 | proc ::csp::CClosed {ch} { 192 | CheckName $ch 193 | # if channel command no longer exists 194 | return [expr {[info procs $ch] eq ""}] 195 | } 196 | 197 | 198 | # uconditionally append to the channel - internal only 199 | proc ::csp::CAppend {ch val} { 200 | variable Channel 201 | lappend Channel($ch) $val 202 | return 203 | } 204 | 205 | proc ::csp::CBuffered {ch} { 206 | variable ChannelCap 207 | return [expr {$ChannelCap($ch) != 0}] 208 | } 209 | 210 | proc ::csp::CEmpty {ch} { 211 | variable Channel 212 | CheckName $ch 213 | if {[info exists Channel($ch)]} { 214 | return [expr {[llength $Channel($ch)] == 0}] 215 | } else { 216 | return 1 217 | } 218 | } 219 | 220 | 221 | proc ::csp::CFull {ch} { 222 | variable Channel 223 | variable ChannelCap 224 | set clen [llength $Channel($ch)] 225 | return [expr {$clen >= $ChannelCap($ch)}] 226 | } 227 | 228 | # return contents of the channel 229 | proc ::csp::CGet {ch} { 230 | variable Channel 231 | return $Channel($ch) 232 | } 233 | 234 | 235 | 236 | # generate new routine name 237 | proc ::csp::NewRoutine {} { 238 | return ::csp::Routine#[incr ::csp::Uid] 239 | } 240 | 241 | proc ::csp::NewChannel {} { 242 | return ::csp::Channel#[incr ::csp::Uid] 243 | } 244 | 245 | 246 | 247 | # invoke proc in a routine 248 | # args should be proc name and arguments 249 | proc ::csp::go {args} { 250 | variable Routine 251 | set rname [::csp::NewRoutine] 252 | coroutine $rname {*}$args 253 | set Routine($rname) 1 254 | SetResume 255 | return $rname 256 | } 257 | 258 | # schedule wake-up 259 | proc ::csp::SetResume {} { 260 | after idle {after 0 ::csp::Resume} 261 | } 262 | 263 | # notify routines to reevaluate resume conditions 264 | # it may be improved by keeping track of the yielding routines (yield [info coroutine]) 265 | proc ::csp::Resume {} { 266 | variable Routine 267 | foreach r [array names Routine] { 268 | if {[info commands $r] eq ""} { 269 | # coroutine must have ended so remove it from the array 270 | catch {unset Routine($r)} 271 | } else { 272 | # cannot run the already running coroutine - catch error when it happens 273 | # this may regularly throw 'coroutine "::csp::Routine#N" is already running' 274 | catch $r 275 | 276 | } 277 | } 278 | set ::csp::resume 1 279 | } 280 | 281 | 282 | proc ::csp::CReceive {ch} { 283 | variable Channel 284 | set elem [lindex $Channel($ch) 0] 285 | set Channel($ch) [lreplace $Channel($ch) 0 0] 286 | return $elem 287 | } 288 | 289 | 290 | proc ::csp::CDrained {ch} { 291 | CheckName $ch 292 | # drained = empty and closed 293 | set drained [expr {![CReceiveReady $ch] && [CClosed $ch]}] 294 | if {$drained} { 295 | # just in case purge every time 296 | CPurge $ch 297 | return 1 298 | } else { 299 | return 0 300 | } 301 | } 302 | 303 | 304 | proc ::csp::ReceiveWith {ch operator} { 305 | CheckOperator $operator 306 | # check if ch contains elements, if so return element, yield otherwise 307 | while {![CReceiveReady $ch]} { 308 | # trying to receive from empty and closed channel should clean up the channel and throw error 309 | if {[CDrained $ch]} { 310 | error "Cannot receive from a drained (empty and closed) channel $ch" 311 | } 312 | Wait$operator 313 | } 314 | set elem [CReceive $ch] 315 | SetResume 316 | return $elem 317 | } 318 | 319 | # Receive from channel, wait if channel not ready, throw error if channel is drained 320 | # Can be only used from coroutine 321 | # Uses yield for wait 322 | proc ::csp::<- {ch} { 323 | return [ReceiveWith $ch <-] 324 | } 325 | 326 | # Receive from channel, wait if channel not ready, throw error if channel is drained 327 | # Can be used from non-coroutine 328 | # Uses vwait for wait. It means it creates nested event loops 329 | # Not ready channel in nested vwait may block an upstream channel that became ready 330 | # Use with care. Avoid if you can. 331 | proc ::csp::<-! {ch} { 332 | return [ReceiveWith $ch <-!] 333 | } 334 | 335 | 336 | # Create a channel and a callback handler being a coroutine which when called 337 | # will send callback single argument to the newly created channel 338 | # The library user should only receive from that channel 339 | # It is designed for one-time callbacks i.e. the coroutine exits after first call. 340 | # The channel will be automatically destroyed after receiving the first message. 341 | # The limitation is that the callback must be a single- or zero- argument callback 342 | # In case of no-argument callback empty string is sent to the channel 343 | proc ::csp::-> {chVar} { 344 | upvar $chVar ch 345 | channel ch 346 | CMakeReadOnly $ch 347 | # this coroutine will not be registered in Routine array for unblocking calls 348 | # it should be called from the userland callback 349 | set routine [NewRoutine] 350 | coroutine $routine OneTimeSender $ch 351 | return $routine 352 | } 353 | 354 | # ch should be a newly created channel with exclusive rights for sending 355 | proc ::csp::OneTimeSender {ch} { 356 | # unconditionally push the callback arguments (returned by yield) to the channel 357 | set arg [yield] 358 | if {![CClosed $ch]} { 359 | CAppend $ch $arg 360 | $ch close 361 | SetResume 362 | } 363 | } 364 | 365 | 366 | # EXPERIMENTAL - works well with Tk sparse events like mouse clicks or key press buttons 367 | # The problem is that in another typical use case i.e. reading from nonblocking file/socket chan 368 | # the constant hammering with events from fileevent starves other coroutines 369 | # and overflows the channel buffer here. 370 | # 371 | # Create a channel and a callback handler being a coroutine which when called 372 | # will send callback single argument to the newly created channel 373 | # The library user should only receive from that channel 374 | # It is designed for multi-time callbacks i.e. the coroutine resumes after every callback. 375 | # The limitation is that the callback must be a single- or zero- argument callback 376 | # In case of no-argument callback empty string is sent to the channel 377 | proc ::csp::->> {chVar {maxbuffer 1000}} { 378 | upvar $chVar ch 379 | channel ch $maxbuffer 380 | CMakeReadOnly $ch 381 | # this coroutine will not be registered in Routine array for unblocking calls 382 | # it should be called from the userland callback 383 | set routine [NewRoutine] 384 | coroutine $routine MultiSender $ch 385 | return $routine 386 | } 387 | 388 | proc ::csp::MultiSender {ch} { 389 | while 1 { 390 | set arg [yield] 391 | # terminate the coroutine if channel not ready for send 392 | if {![CSendReady $ch]} { 393 | break 394 | } 395 | # push the callback arguments (returned by yield) to the channel 396 | CAppend $ch $arg 397 | SetResume 398 | } 399 | } 400 | 401 | proc ::csp::Forward {from to} { 402 | range msg $from { 403 | # destination channel may be closed in the meantime so catch error and don't break the loop 404 | # it is to clear the $from channels 405 | catch {$to <- $msg} 406 | } 407 | } 408 | 409 | proc ::csp::forward {froms to} { 410 | foreach from $froms { 411 | go ::csp::Forward $from $to 412 | } 413 | } 414 | 415 | # The select command provides a way to handle multiple channels. 416 | # It is a switch like statement where channels are evaluated for readiness. 417 | # The select command makes the coroutine wait until at least one channel is ready. 418 | # If multiple can proceed, select chooses pseudo-randomly. 419 | # A default clause, if present, executes immediately if no channel is ready. 420 | proc ::csp::select {a} { 421 | set ready 0 422 | # ensure that operator and channel are substituted only once 423 | set substa {} 424 | foreach {op ch body} $a { 425 | if {$op eq "default"} { 426 | lappend substa $op $ch $body 427 | } else { 428 | lappend substa [uplevel subst $op] [uplevel subst $ch] $body 429 | } 430 | } 431 | while {$ready == 0} { 432 | # (op ch body) triples ready for send/receive 433 | set triples {} 434 | set default 0 435 | set defaultbody {} 436 | set operator {} 437 | foreach {op ch body} $substa { 438 | if {[IsOperator $ch]} { 439 | lassign [list $op $ch] ch op 440 | if {$op ni $operator} { 441 | lappend operator $op 442 | } 443 | if {[CSendReady $ch]} { 444 | lappend triples s $ch $body 445 | } 446 | } elseif {[IsOperator $op]} { 447 | if {$op ni $operator} { 448 | lappend operator $op 449 | } 450 | if {[CReceiveReady $ch]} { 451 | lappend triples r $ch $body 452 | } 453 | } elseif {$op eq "default"} { 454 | set default 1 455 | set defaultbody $ch 456 | } else { 457 | error "Wrong select arguments: $op $ch" 458 | } 459 | } 460 | if {[llength $operator] == 0} { 461 | error "<- or <-! operator required in select" 462 | } 463 | if {[llength $operator] > 1} { 464 | error "<- and <-! should not be mixed in a single select" 465 | } 466 | CheckOperator $operator 467 | set ready [expr {[llength $triples] / 3}] 468 | if {$ready == 0} { 469 | if {$default == 0} { 470 | Wait$operator 471 | } else { 472 | return [uplevel $defaultbody] 473 | } 474 | } 475 | } 476 | 477 | if {$ready == 1} { 478 | set triple $triples 479 | } else { 480 | set random [expr {round(floor(rand()*$ready))}] 481 | set triple [lrange $triples [expr {$random * 3}] [expr {$random * 3 + 2}]] 482 | } 483 | 484 | lassign $triple op ch body 485 | return [uplevel $body] 486 | } 487 | 488 | proc ::csp::timer {chVar interval} { 489 | upvar $chVar ch 490 | after $interval [-> ch] {[clock microseconds]} 491 | return $ch 492 | } 493 | 494 | proc ::csp::TickerRoutine {ch interval closeafter} { 495 | if {![CClosed $ch]} { 496 | if {[regexp {#(\d+)} $closeafter _ ticksleft]} { 497 | if {$ticksleft <= 0} { 498 | $ch close 499 | return 500 | } 501 | set closeafter #[incr ticksleft -1] 502 | } 503 | $ch <- [clock microseconds] 504 | after $interval csp::go TickerRoutine $ch $interval $closeafter 505 | } 506 | } 507 | 508 | # Return ticker channel. First tick in $interval ms 509 | proc ::csp::ticker {chVar interval {closeafter 0}} { 510 | return [ticker_generic $chVar $interval $interval $closeafter] 511 | } 512 | 513 | # Return ticker channel. First tick immediately. 514 | proc ::csp::tickernow {chVar interval {closeafter 0}} { 515 | return [ticker_generic $chVar 0 $interval $closeafter] 516 | } 517 | 518 | # Generic internal ticker function 519 | proc ::csp::ticker_generic {chVar initial_interval interval closeafter} { 520 | upvar 2 $chVar ch 521 | csp::channel ch 522 | if {$closeafter != 0 && [string is integer -strict $closeafter]} { 523 | after $closeafter $ch close 524 | } 525 | after $initial_interval csp::go TickerRoutine $ch $interval $closeafter 526 | return $ch 527 | } 528 | 529 | 530 | # receive from channel until closed in coroutine 531 | proc ::csp::range {varName ch body} { 532 | if {[info coroutine] eq ""} { 533 | error "range can only be used in a coroutine" 534 | } 535 | uplevel [subst -nocommands { 536 | while 1 { 537 | try { 538 | set $varName [<- $ch] 539 | $body 540 | } on error {out err} { 541 | break 542 | } 543 | } 544 | }] 545 | } 546 | 547 | # receive from channel until closed in main control flow 548 | proc ::csp::range! {varName ch body} { 549 | if {[info coroutine] ne ""} { 550 | error "range! should not be used in a coroutine" 551 | } 552 | uplevel [subst -nocommands { 553 | while 1 { 554 | try { 555 | set $varName [<-! $ch] 556 | $body 557 | } on error {out err} { 558 | break 559 | } 560 | } 561 | }] 562 | } 563 | -------------------------------------------------------------------------------- /csp.test: -------------------------------------------------------------------------------- 1 | package require tcltest 2 | namespace import -force ::tcltest::* 3 | #tcltest::configure -match {csp-1.1 csp-1.2 csp-1.3 csp-1.4 csp-1.5 csp-1.6} 4 | tcltest::configure -debug 0 5 | 6 | lappend auto_path [file normalize .] 7 | package require csp 8 | namespace import csp::* 9 | 10 | test csp-1.1 {simple sender receiver rendez-vous} -body { 11 | proc sender {ch} { 12 | foreach i {1 2 3 4} { 13 | puts -nonewline i$i 14 | $ch <- $i 15 | } 16 | } 17 | proc receiver {ch} { 18 | while 1 { 19 | puts -nonewline o[<- $ch] 20 | } 21 | } 22 | channel ch 23 | go sender $ch 24 | go receiver $ch 25 | after 501 set little 1 26 | vwait little 27 | return 28 | } -output {i1o1i2o2i3o3i4o4} 29 | 30 | test csp-1.2 {simple sender receiver buffered channel with close} -body { 31 | proc sender {ch} { 32 | foreach i {1 2 3 4} { 33 | puts -nonewline i$i 34 | $ch <- $i 35 | } 36 | puts -nonewline close 37 | $ch close 38 | } 39 | proc receiver {ch} { 40 | try { 41 | while 1 { 42 | puts -nonewline o[<- $ch] 43 | } 44 | } on error {} { 45 | } 46 | } 47 | channel ch 2 48 | go sender $ch 49 | go receiver $ch 50 | after 502 set little 1 51 | vwait little 52 | return 53 | } -output {i1i2i3o1o2i4closeo3o4} 54 | 55 | test csp-1.3 {ping pong rendez-vous} -body { 56 | proc p1 {chin chout} { 57 | while 1 { 58 | $chout <- [<- $chin] 59 | } 60 | } 61 | proc p2 {chin chout} { 62 | foreach i {1 2 3 4} { 63 | puts -nonewline $i 64 | $chout <- $i 65 | puts -nonewline [<- $chin] 66 | } 67 | } 68 | channel {ch1 ch2} 69 | go p1 $ch1 $ch2 70 | go p2 $ch2 $ch1 71 | after 503 set little 1 72 | vwait little 73 | $ch1 close 74 | $ch2 close 75 | return 76 | } -output {11223344} 77 | 78 | 79 | test csp-1.4 {ping pong buffered channels} -body { 80 | proc p1 {chin chout} { 81 | while 1 { 82 | $chout <- [<- $chin] 83 | } 84 | } 85 | proc p2 {chin chout} { 86 | foreach i {1 2 3 4} { 87 | puts -nonewline $i 88 | $chout <- $i 89 | puts -nonewline [<- $chin] 90 | } 91 | } 92 | channel {ch1 ch2} 5 93 | go p1 $ch1 $ch2 94 | go p2 $ch2 $ch1 95 | after 504 set little 1 96 | vwait little 97 | $ch1 close 98 | $ch2 close 99 | return 100 | } -output {11223344} 101 | 102 | 103 | test csp-1.5 {sync coroutine with main control flow} -body { 104 | proc p1 {ch} { 105 | puts -nonewline [<- $ch] 106 | } 107 | channel ch 108 | go p1 $ch 109 | puts -nonewline a 110 | # this will be run outside of coroutine 111 | after 200 $ch <-! 1 112 | puts -nonewline b 113 | after 505 set little 1 114 | vwait little 115 | puts -nonewline c 116 | $ch close 117 | return 118 | } -output {ab1c} 119 | 120 | 121 | test csp-1.6 {sync coroutine with main control flow - reverse} -body { 122 | proc p1 {ch} { 123 | foreach i {1 2 3} { 124 | puts -nonewline i$i 125 | $ch <- $i 126 | } 127 | } 128 | channel ch 129 | go p1 $ch 130 | puts -nonewline a 131 | # this will be run out of coroutine 132 | after 200 puts -nonewline o\[<-! $ch\] 133 | puts -nonewline b 134 | after 300 puts -nonewline o\[<-! $ch\] 135 | puts -nonewline c 136 | after 400 puts -nonewline o\[<-! $ch\] 137 | puts -nonewline d 138 | after 506 set little 1 139 | vwait little 140 | puts -nonewline e 141 | $ch close 142 | return 143 | } -output {i1abcdo1i2o2i3o3e} 144 | 145 | 146 | test csp-1.7 {simple timer} -body { 147 | timer ch 200 148 | puts -nonewline [<-! $ch] 149 | return 150 | } -match regexp -output {^\d{16}$} 151 | 152 | test csp-1.8 {simple ticker} -body { 153 | ticker ch 508 154 | foreach i {1 2 3} { 155 | puts -nonewline "[<-! $ch] " 156 | } 157 | $ch close 158 | return 159 | } -match regexp -output {^(\d{16} ){3}$} 160 | 161 | test csp-1.9 {range in coroutine} -body { 162 | set callback [->> ch] 163 | after 100 $callback a 164 | after 200 $callback b 165 | after 300 $callback c 166 | after 400 $ch close 167 | proc p1 {ch} { 168 | range v $ch { 169 | puts -nonewline $v 170 | } 171 | puts -nonewline d 172 | } 173 | go p1 $ch 174 | after 509 set little 1 175 | vwait little 176 | return 177 | } -output {abcd} 178 | 179 | test csp-1.10 {range! in main} -body { 180 | set callback [->> ch] 181 | after 100 $callback a 182 | after 200 $callback b 183 | after 300 $callback c 184 | after 400 $ch close 185 | range! v $ch { 186 | puts -nonewline $v 187 | } 188 | puts -nonewline d 189 | return 190 | } -output {abcd} 191 | 192 | test csp-1.11 {} -body { 193 | return 194 | } 195 | 196 | test csp-1.12 {} -body { 197 | return 198 | } 199 | 200 | ::tcltest::cleanupTests 201 | return 202 | 203 | -------------------------------------------------------------------------------- /doc.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh 2 | # 3 | # doctools and textutil::expander must be on this path 4 | lappend auto_path [file normalize ../skt/lib/generic] 5 | 6 | package require doctools 7 | 8 | proc slurp {path} { 9 | set fd [open $path r] 10 | fconfigure $fd -encoding utf-8 11 | set data [read $fd] 12 | close $fd 13 | return $data 14 | } 15 | 16 | proc spit {path content} { 17 | set fd [open $path w] 18 | puts -nonewline $fd $content 19 | close $fd 20 | } 21 | 22 | doctools::new mydoc -format html 23 | set path ./csp.man 24 | set path [file normalize $path] 25 | set dest [file join [file dir $path] [file root [file tail $path]].html] 26 | spit $dest [mydoc format [slurp $path]] 27 | 28 | -------------------------------------------------------------------------------- /pkgIndex.tcl: -------------------------------------------------------------------------------- 1 | # Tcl package index file, version 1.1 2 | # This file is generated by the "pkg_mkIndex" command 3 | # and sourced either when an application starts up or 4 | # by a "package unknown" script. It invokes the 5 | # "package ifneeded" command to set up package-related 6 | # information so that packages will be loaded automatically 7 | # in response to "package require" commands. When this 8 | # script is sourced, the variable $dir must contain the 9 | # full path name of this file's directory. 10 | 11 | package ifneeded csp 0.1.0 [list source [file join $dir csp.tcl]] 12 | -------------------------------------------------------------------------------- /test.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh 2 | package require tcltest 3 | #tcltest::configure -testdir [file normalize .] 4 | tcltest::runAllTests 5 | 6 | --------------------------------------------------------------------------------