socketry / async

An awesome asynchronous event-driven reactor for Ruby.
MIT License
2.1k stars 86 forks source link

how to make the Async code non blocking? #342

Closed michelson closed 2 weeks ago

michelson commented 2 weeks ago

Sorry for the noob question here, but something that I do not understand is that Async is blocking the code execution; I'm assuming that Async should not block and run concurrently, but I am not sure if I understand this correctly.

Example 1, a running server:


require 'async/io'
      require 'async/io/unix_endpoint'

      @server = Async::IO::Endpoint.unix("./tmp.sock")

      # This async blocks the execution as the server runs forever:
        Async do |task|
          @server.accept do |client|
            Console.logger.info(client, "Accepted connection")
            a = client.read(6)
            sleep 1
            client.send "elloh\n"
            client.close_write
          end
        end

      sleep 3

      10.times do
        UNIXSocket.open("./tmp.sock") do |socket|
          socket << "hello\n"
          p socket.read(6)
          socket.close
        end
      end

now, If I put a Thread new on the Async, it will not block.


require 'async/io'
      require 'async/io/unix_endpoint'

      @server = Async::IO::Endpoint.unix("./tmp.sock")

      Thread.new do 
        Async do |task|
          @server.accept do |client|
            Console.logger.info(client, "Accepted connection")
            a = client.read(6)
            sleep 1
            client.send "elloh\n"
            client.close_write
          end
        end
      end

      sleep 3

      10.times do
        UNIXSocket.open("./tmp.sock") do |socket|
          socket << "hello\n"
          p socket.read(6)
          socket.close
        end
      end

how can I achieve the non block only with async gems?

ioquatix commented 2 weeks ago

Firstly, you should stop using async-io as it's legacy - instead use io-endpoint which has a similar interface.

Regarding your program, you need to have multiple Async blocks, e.g.

#!/usr/bin/env ruby

require 'async'
require 'io/endpoint/unix_endpoint'
require 'io/endpoint/bound_endpoint'

Async do
  endpoint = IO::Endpoint.unix("server.ipc")
  bound_endpoint = endpoint.bound

  server_task = Async do
    bound_endpoint.accept do |client|
      Console.logger.info(client, "Accepted connection")
      while line = client.gets
        client.puts line
      end
    end
  end

  sleep 1

  10.times do
    UNIXSocket.open(endpoint.path) do |server|
      server.puts "Hello World"
      p server.gets
      server.close
    end
  end
ensure
  server_task&.stop
  bound_endpoint&.close
end

In this case we have the server running in a nested task which allows our client to make connections to it, concurrently.

michelson commented 2 weeks ago

Hi @ioquatix, Thanks for the quick reply. If I understand correctly, I should wrap all the code in an async block. While this works, the main async is also blocking, and it is only released because we stopped the server at some point, right? Could async be achieved with a use case in which we want many workers running forever and processing messages or sending messages to each other in a single process program? Is it similar to how goroutines work on Golang? Or even like Thread.new in ruby which runs in background and do not block?

Thanks in advance, tremendous work on the Async

ioquatix commented 2 weeks ago

If I understand correctly, I should wrap all the code in an async block

In general, if you want those parts of the code to run concurrently, then yes.

While this works, the main async is also blocking, and it is only released because we stopped the server at some point, right?

Yes, at some level all programs must wait for the work to be completed.

Could async be achieved with a use case in which we want many workers running forever and processing messages or sending messages to each other in a single process program?

Yes, and there are different strategies for how you achieve this, e.g.

Is it similar to how goroutines work on Golang? Or even like Thread.new in ruby which runs in background and do not block?

This is unstructured concurrency. I don't think it's a good model.

https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

michelson commented 2 weeks ago

Hi Samuel, thanks for the detailed answer. Would it make sense if I used both? For instance, could I have a multi-process container with async-container that spawns hundreds of actors with async-actor?

I'm trying to implement some sort of OTP framework with gen_servers, tcp_server, supervisors, etc. I've implemented a gen_server with Ractor, but I find some of the safety safeguards a little bit limiting and challenging to debug. Async suite seems to be better suited for this use case!