Search code examples
multithreadingcrystal-lang

Killing a Thread in Crystal lang


I wrote a program a crystal program to calculate prime numbers upto a range with Sieve.

Code

#!/usr/bin/env crystal

def sieve(max)
    t = Thread.new do
        dot, ary, colours = ".", ["\xE2\xA0\x81", "\xE2\xA0\x88", "\xE2\xA0\xA0", "\xE2\xA0\x84"] * 2, [154, 184, 208, 203, 198, 164, 129, 92]
        print "\e[?25l"

        loop do
            ary.size.times do |x|
                print("\e[2K#{ary[x]} \e[38;5;#{colours[x]}mPlease Wait#{dot * x}\e[0m\r")
                sleep(0.1)
            end
        end
    end

    s = [nil, nil] + (2..max).to_a
    s.each do |x|
        next unless x
        break if (sq = x ** 2) > max
        (sq..max).step(x) { |y| s[y] = nil }
    end

    puts "\e[?25h"
    s.tap { |x| x.compact! }
end

p sieve(2_000_000).size

The way I want to display it is

enter image description here

Issue

The problem is the thread isn't killed when puts is writing the sieve. the method sieve(n) just returns an array. The array size then is calculated, and printed. You can see that the animation freezes for a time, and then continues until it's printed and exited. If I use spawn do...end the print in spawn pauses until the sieve is calculated.

Not killing threads causes issues like this enter image description here

In ruby I used to do

t = Thread.new { loop while ... }
<some other time consuming stuff here>

t.kill
return calculated_stuffs

Crystal Details

Crystal 0.31.1 (2019-10-21)

LLVM: 9.0.0 Default target: x86_64-pc-linux-gnu


How to kill a thread in crystal?


Solution

  • Thread is part of Crystal's internal API, and is not meant to be used directly.

    The good news is Crystal natively supports a concurrency model called CSP, where Fibers (light-weight threads) send each others messages over thread-safe Channels in order to coordinate. So, rather than communicating by sharing state, Fibers share state by communicating - as they say in golang.

    For your use case, you could run 3 Fibers:

    • A sieve, generating numbers and sending updates through a channel
    • A monitor, receiving on the sieve's channel, updating the UI and sending a completion message once the sieve is done
    • The main Fiber, waiting for the monitor to notify completion and able to decide what to do with the sieve's result

    enter image description here

    Here is what your code could look like

    record Result, primes : Array(Int32)
    record Tick
    alias SieveUpdate = Result | Tick
    
    def monitor(updates : Channel(SieveUpdate)) : Channel(Result)
      Channel(Result).new.tap { |done|
        spawn do
          dot, ary, colours = ".", ["\xE2\xA0\x81", "\xE2\xA0\x88", "\xE2\xA0\xA0", "\xE2\xA0\x84"] * 2, [154, 184, 208, 203, 198, 164, 129, 92]
          ary_idx = 0
          update_n = 0
          print "\e[?25l"
          loop do
            case value = updates.receive
            when Tick
              next unless (update_n+=1) % 50 == 0 # lower refresh rate
              print("\e[2K#{ary[ary_idx]} \e[38;5;#{colours[ary_idx]}mPlease Wait#{dot * ary_idx}\e[0m\r")
              ary_idx = (ary_idx + 1) % ary.size
            when Result
              puts "\e[?25h"
              done.send value
              break
            end
          end
        end
      }
    end
    
    def sieve(max) : Channel(SieveUpdate)
      Channel(SieveUpdate).new.tap { |updates|
        spawn do
          s = [nil, nil] + (2..max).to_a
          s.each do |x|
              updates.send(Tick.new)
              next unless x
              break if (sq = x ** 2) > max
              (sq..max).step(x) { |y| s[y] = nil }
          end
    
          updates.send Result.new(s.compact.as(Array(Int32)))
        end
      }
    end
    
    updates = sieve(2_000_000)
    done = monitor(updates)
    
    print done.receive.primes.size