morganstanley / hobbes

A language and an embedded JIT compiler

Home Page:http://hobbes.readthedocs.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Creating a new hobbes thread from hobbes

adam-antonik opened this issue · comments

I'm having trouble working out how I could write something like

 spawn :: ((a -> ()), a) -> ()

that would start a new thread and run a hobbes function in it with an argument. If I have the type a fixed at c++ src compile time, then a c function bound for that specific signature seems to work, but do you have any ideas how to go about doing this at hobbes compile time?

For anyone else reading, I'd initially just say that threads are dangerous and we probably should start with type-level reasoning in a principled way to exclude the conditions where threads can go wrong.

Having said that, if we don't mind a little dirty hacking for a POC, we might start with some C++ functions like (as you discovered, we can only interact with C++ via monomorphic types, but we can address that shortly):

void* invokeClosure(void* uc) {
    const auto* c = reinterpret_cast<const hobbes::closure<void()>*>(uc);
    (*c)();
    hobbes::resetMemoryPool();
    return 0;
}
uint64_t makeThread(const hobbes::closure<void()>* c) {
    pthread_t tid;
    if (pthread_create(&tid, 0, &invokeClosure, const_cast<void*>(static_cast<const void*>(c))) == 0) {
        return tid;
    } else {
        return 0;
    }
}
void waitThread(uint64_t t) {
    if (t) {
        void* r = 0;
        pthread_join(pthread_t(t), &r);
    }
}

Then we bind this in, plus a handy 'sleep' function, in the usual way (here I tested in the "hi" shell code):

void bindHiDefs(hobbes::cc& c) {
// ...
  c.bind("sleep", &sleep);
  c.bind("makeThread", &makeThread);
  c.bind("waitThread", &waitThread);
}

So as a basic sanity test:

$ ./hi -s
> waitThread(makeThread(toClosure(\().do{putStrLn("about to wait");sleep(5);putStrLn("done waiting");})))
about to wait
[... 5 seconds pass ...]
done waiting
> 

But now it's easy to pack polymorphic values into closures:

> tspawn = \f x.makeThread(\().f(x))
> waitThread(tspawn(println, [{x=i,y=i*i}|i<-[0..10]]))
 x   y
-- ---
 0   0
 1   1
 2   4
 3   9
 4  16
 5  25
 6  36
 7  49
 8  64
 9  81
10 100

Now at least this has hopefully made the issues involved a bit clearer, though there are still reasonable steps we might take beyond this. For example, we might want a "future" abstraction to get thread results, or maybe we want to nail down what memory is allocated and live in what spaces at what times (areas where the thread stuff starts getting more interesting).

Anyway, I hope that this helps. :)

Thanks, I'd missed hobbes::closure in the codebase, having that makes it obvious, as you show.

Yeah we could use better documentation of the C++ binding interface. It's actually an "open" interface in the sense that you can define custom interpretations of memory through C++ types (though the default set is usually enough for most purposes).

To tell the hobbes compiler how to interpret a C++ type, you specialize hobbes::lift<T> at that type, with the default set of definitions in tylift.H.

You can see the introduction of a C++ type to designate closures there, plus some logic to describe the closure type in the "mono type description" grammar that hobbes has.

It's a bit low-level, but it's a useful stage to know about. This is how we avoid costly conversion operations when moving between C++ and hobbes, instead we just tell hobbes how to interpret the memory that's already been prepared in C++ (and vice versa).

Closing for now since I think I answered your question, but please let me know if you have other thoughts/questions down this path. Multi-threading can be a tricky language and runtime issue.