unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* Releasing the thread global_lock from the module API
@ 2024-03-01 14:53 Spencer Baugh
  2024-03-01 16:47 ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: Spencer Baugh @ 2024-03-01 14:53 UTC (permalink / raw)
  To: emacs-devel; +Cc: Philipp Stephani


Lisp threads all take global_lock while they are running Lisp code.
This is good and correct.

Lisp threads can also call module code.  This is also good and correct.

In other languages with an FFI and a global interpreter lock taken by
threads, such as Python, it's possible for a native function called from
the interpreter to release the global lock, and then re-acquire it
before calling any interpreter functions (or automatically re-acquire it
upon returning to the interpreter).

This allows for a limited form of actual parallelism: if a native
function is doing work that doesn't involve the Python interpreter,
e.g. doing numerical computations in native code, it can release the
lock so that it can run in parallel with other threads.  If the native
function needs to call a function which does involve the Python
interpreter, it can re-acquire the global lock around that call.

Could the same functionality be added to the Emacs module API?  Then
module code could release the global lock when doing Emacs-independent
computation, and re-acquire the lock when calling into Emacs.  This
would allow a limited form of parallelism: if a Lisp thread is calling
module code which releases the lock, then the module code could run in
parallel with the rest of Emacs on that thread.

This should be safe, because this is in effect already possibly through
thread-yield: that can be called from anywhere in a Lisp thread, and
releases the global lock, calls the operating system thread_yield
function, and then re-acquires the global lock.  If instead of doing
thread_yield, it did some computation, e.g.

int x = 0
for (int i = 0; i<999999; i++)
  x += i;

that would have the same effect as yielding, but the computation would
run in parallel with the main Emacs thread.  This is what would happen
if module code released the lock, did some Emacs-independent work, and
then re-acquired the lock.

As an additional bonus, this would allow the module API to extend Lisp
threads: if a C library provides some blocking function which does some
complicated form of IO, a module providing bindings for that C library
can release global_lock before calling that function, and then
re-acquire the lock after the function returns.  Then Lisp threads
calling this module function will not block the main Emacs thread.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 14:53 Releasing the thread global_lock from the module API Spencer Baugh
@ 2024-03-01 16:47 ` Eli Zaretskii
  2024-03-01 17:34   ` Spencer Baugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-01 16:47 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel, phst

> From: Spencer Baugh <sbaugh@janestreet.com>
> Cc: Philipp Stephani <phst@google.com>
> Date: Fri, 01 Mar 2024 09:53:41 -0500
> 
> In other languages with an FFI and a global interpreter lock taken by
> threads, such as Python, it's possible for a native function called from
> the interpreter to release the global lock, and then re-acquire it
> before calling any interpreter functions (or automatically re-acquire it
> upon returning to the interpreter).
> 
> This allows for a limited form of actual parallelism: if a native
> function is doing work that doesn't involve the Python interpreter,
> e.g. doing numerical computations in native code, it can release the
> lock so that it can run in parallel with other threads.  If the native
> function needs to call a function which does involve the Python
> interpreter, it can re-acquire the global lock around that call.
> 
> Could the same functionality be added to the Emacs module API?  Then
> module code could release the global lock when doing Emacs-independent
> computation, and re-acquire the lock when calling into Emacs.  This
> would allow a limited form of parallelism: if a Lisp thread is calling
> module code which releases the lock, then the module code could run in
> parallel with the rest of Emacs on that thread.

It isn't clear what exactly do you want added, since calls to
thread-yield and similar APIs (like sleep-for and
accept-process-output), which release and re-acquire the global lock,
are already possible from Lisp threads.  What exactly is missing?

> This should be safe, because this is in effect already possibly through
> thread-yield: that can be called from anywhere in a Lisp thread, and
> releases the global lock, calls the operating system thread_yield
> function, and then re-acquires the global lock.  If instead of doing
> thread_yield, it did some computation, e.g.
> 
> int x = 0
> for (int i = 0; i<999999; i++)
>   x += i;
> 
> that would have the same effect as yielding, but the computation would
> run in parallel with the main Emacs thread.  This is what would happen
> if module code released the lock, did some Emacs-independent work, and
> then re-acquired the lock.

If a module wants to do some Emacs-independent work, it can start a
native thread and do it there, no?  So again, what is missing?

> As an additional bonus, this would allow the module API to extend Lisp
> threads: if a C library provides some blocking function which does some
> complicated form of IO, a module providing bindings for that C library
> can release global_lock before calling that function, and then
> re-acquire the lock after the function returns.  Then Lisp threads
> calling this module function will not block the main Emacs thread.

I think this is already possible, see above.  I guess I'm missing
something.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 16:47 ` Eli Zaretskii
@ 2024-03-01 17:34   ` Spencer Baugh
  2024-03-01 18:44     ` Eli Zaretskii
  2024-03-01 19:30     ` tomas
  0 siblings, 2 replies; 26+ messages in thread
From: Spencer Baugh @ 2024-03-01 17:34 UTC (permalink / raw)
  To: emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Spencer Baugh <sbaugh@janestreet.com>
>> Cc: Philipp Stephani <phst@google.com>
>> Date: Fri, 01 Mar 2024 09:53:41 -0500
>> 
>> In other languages with an FFI and a global interpreter lock taken by
>> threads, such as Python, it's possible for a native function called from
>> the interpreter to release the global lock, and then re-acquire it
>> before calling any interpreter functions (or automatically re-acquire it
>> upon returning to the interpreter).
>> 
>> This allows for a limited form of actual parallelism: if a native
>> function is doing work that doesn't involve the Python interpreter,
>> e.g. doing numerical computations in native code, it can release the
>> lock so that it can run in parallel with other threads.  If the native
>> function needs to call a function which does involve the Python
>> interpreter, it can re-acquire the global lock around that call.
>> 
>> Could the same functionality be added to the Emacs module API?  Then
>> module code could release the global lock when doing Emacs-independent
>> computation, and re-acquire the lock when calling into Emacs.  This
>> would allow a limited form of parallelism: if a Lisp thread is calling
>> module code which releases the lock, then the module code could run in
>> parallel with the rest of Emacs on that thread.
>
> It isn't clear what exactly do you want added, since calls to
> thread-yield and similar APIs (like sleep-for and
> accept-process-output), which release and re-acquire the global lock,
> are already possible from Lisp threads.  What exactly is missing?

Those release and re-acquire the global lock, yes, but they don't allow
module code to run on the thread while the global lock is released.

>> This should be safe, because this is in effect already possibly through
>> thread-yield: that can be called from anywhere in a Lisp thread, and
>> releases the global lock, calls the operating system thread_yield
>> function, and then re-acquires the global lock.  If instead of doing
>> thread_yield, it did some computation, e.g.
>> 
>> int x = 0
>> for (int i = 0; i<999999; i++)
>>   x += i;
>> 
>> that would have the same effect as yielding, but the computation would
>> run in parallel with the main Emacs thread.  This is what would happen
>> if module code released the lock, did some Emacs-independent work, and
>> then re-acquired the lock.
>
> If a module wants to do some Emacs-independent work, it can start a
> native thread and do it there, no?  So again, what is missing?

Yes, it's true that a module can provide parallelism in that way.  The
issue is whether Emacs can run in parallel with the module.

Today, we can do:
- Lisp thread A calls module_work
- On thread A, module_work starts native threads X and Y,
  and does Emacs-independent work across all of these threads in parallel.
- On thread A, module_work finishes and returns to Lisp.

All Lisp threads will be blocked while the Lisp thread A is running the
module.

We can't do:
- Lisp thread A calls module_work
- On thread A, module_work releases the global lock.
- On thread A, module_work starts native threads X and Y,
  and does Emacs-independent work across all of these threads in parallel.
- Unrelated Lisp thread B is able to take the global lock and run Lisp code
  in parallel with module_work on thread A.
- On thread A, module_work finishes and returns to Lisp.

Releasing the global lock allows Lisp thread B to run while Lisp thread
A is running the module.

>> As an additional bonus, this would allow the module API to extend Lisp
>> threads: if a C library provides some blocking function which does some
>> complicated form of IO, a module providing bindings for that C library
>> can release global_lock before calling that function, and then
>> re-acquire the lock after the function returns.  Then Lisp threads
>> calling this module function will not block the main Emacs thread.
>
> I think this is already possible, see above.  I guess I'm missing
> something.

I don't think it is.  How would a module release global_lock, call a C
library function on the current thread, and then re-acquire the global
lock?  Can you say how you would do that with the APIs we have today?




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 17:34   ` Spencer Baugh
@ 2024-03-01 18:44     ` Eli Zaretskii
  2024-03-01 19:02       ` Spencer Baugh
  2024-03-01 19:30     ` tomas
  1 sibling, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-01 18:44 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel

> From: Spencer Baugh <sbaugh@janestreet.com>
> Date: Fri, 01 Mar 2024 12:34:36 -0500
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> 
> > It isn't clear what exactly do you want added, since calls to
> > thread-yield and similar APIs (like sleep-for and
> > accept-process-output), which release and re-acquire the global lock,
> > are already possible from Lisp threads.  What exactly is missing?
> 
> Those release and re-acquire the global lock, yes, but they don't allow
> module code to run on the thread while the global lock is released.

That's because the Emacs Lisp thread machinery cannot allow that.  A
Lisp thread has full access to the Emacs global state, so we cannot
allow more than one thread to have that at the same time.

> We can't do:
> - Lisp thread A calls module_work
> - On thread A, module_work releases the global lock.
> - On thread A, module_work starts native threads X and Y,

You can do that if the native threads are started before releasing the
global lock.

>   and does Emacs-independent work across all of these threads in parallel.

What do you mean by "Emacs-independent work across all of these
threads in parallel"?  If you want to break out of the restriction of
having only one Lisp thread running at any given time, then this is
IMO impossible without redesigning and reimplementing the current
threads machinery.

> Releasing the global lock allows Lisp thread B to run while Lisp thread
> A is running the module.

As explained above, you can do that already.

> > I think this is already possible, see above.  I guess I'm missing
> > something.
> 
> I don't think it is.  How would a module release global_lock, call a C
> library function on the current thread, and then re-acquire the global
> lock?  Can you say how you would do that with the APIs we have today?

If a thread releases a global lock, it cannot run the Lisp machine
until it re-acquire the global lock.  So what you want is impossible
within the current threads framework, unless you start native
(non-Lisp) threads which cannot safely access the Emacs global state.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 18:44     ` Eli Zaretskii
@ 2024-03-01 19:02       ` Spencer Baugh
  2024-03-01 19:26         ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: Spencer Baugh @ 2024-03-01 19:02 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:
>> > I think this is already possible, see above.  I guess I'm missing
>> > something.
>> 
>> I don't think it is.  How would a module release global_lock, call a C
>> library function on the current thread, and then re-acquire the global
>> lock?  Can you say how you would do that with the APIs we have today?
>
> If a thread releases a global lock, it cannot run the Lisp machine
> until it re-acquire the global lock.

Yes.  But calling a C library function (by some other library not
related to Emacs but used by the module) does not involve calling into
the Lisp machine or any Emacs machinery.  So, because it does not
involve calling into Emacs, we could allow the thread to make that call
without the Emacs global lock which protects Emacs.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 19:02       ` Spencer Baugh
@ 2024-03-01 19:26         ` Eli Zaretskii
  2024-03-01 19:51           ` Spencer Baugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-01 19:26 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel

> From: Spencer Baugh <sbaugh@janestreet.com>
> Cc: emacs-devel@gnu.org
> Date: Fri, 01 Mar 2024 14:02:24 -0500
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> >> > I think this is already possible, see above.  I guess I'm missing
> >> > something.
> >> 
> >> I don't think it is.  How would a module release global_lock, call a C
> >> library function on the current thread, and then re-acquire the global
> >> lock?  Can you say how you would do that with the APIs we have today?
> >
> > If a thread releases a global lock, it cannot run the Lisp machine
> > until it re-acquire the global lock.
> 
> Yes.  But calling a C library function (by some other library not
> related to Emacs but used by the module) does not involve calling into
> the Lisp machine or any Emacs machinery.

There's very little you can do usefully this way without touching some
aspect of the Lisp machine.  Assuming that what you do has some
relevance to the Emacs session, of course.

> So, because it does not involve calling into Emacs, we could allow
> the thread to make that call without the Emacs global lock which
> protects Emacs.

Once again: you can do that if you start a native thread before
releasing the global lock.  Why is that not a solution to what you
have in mind?



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 17:34   ` Spencer Baugh
  2024-03-01 18:44     ` Eli Zaretskii
@ 2024-03-01 19:30     ` tomas
  2024-03-01 23:53       ` Dmitry Gutov
  1 sibling, 1 reply; 26+ messages in thread
From: tomas @ 2024-03-01 19:30 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel

[-- Attachment #1: Type: text/plain, Size: 842 bytes --]

On Fri, Mar 01, 2024 at 12:34:36PM -0500, Spencer Baugh wrote:

[...]

> We can't do:
> - Lisp thread A calls module_work
> - On thread A, module_work releases the global lock.
> - On thread A, module_work starts native threads X and Y,
>   and does Emacs-independent work across all of these threads in parallel.

If X and Y are truly independent of the Lisp machinery, I don't
understand what keeps you from sending both their ways and
terminating thread A, thus setting Lisp free for whatever other
thread wants to take over.

> - Unrelated Lisp thread B is able to take the global lock and run Lisp code
>   in parallel with module_work on thread A.
> - On thread A, module_work finishes and returns to Lisp.

Why has thread A wait up to here? This is what's keeping your thread B
from playing, no?

Cheers
-- 
t

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 195 bytes --]

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 19:26         ` Eli Zaretskii
@ 2024-03-01 19:51           ` Spencer Baugh
  2024-03-01 20:42             ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: Spencer Baugh @ 2024-03-01 19:51 UTC (permalink / raw)
  To: emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Spencer Baugh <sbaugh@janestreet.com>
>> Cc: emacs-devel@gnu.org
>> Date: Fri, 01 Mar 2024 14:02:24 -0500
>> 
>> Eli Zaretskii <eliz@gnu.org> writes:
>> >> > I think this is already possible, see above.  I guess I'm missing
>> >> > something.
>> >> 
>> >> I don't think it is.  How would a module release global_lock, call a C
>> >> library function on the current thread, and then re-acquire the global
>> >> lock?  Can you say how you would do that with the APIs we have today?
>> >
>> > If a thread releases a global lock, it cannot run the Lisp machine
>> > until it re-acquire the global lock.
>> 
>> Yes.  But calling a C library function (by some other library not
>> related to Emacs but used by the module) does not involve calling into
>> the Lisp machine or any Emacs machinery.
>
> There's very little you can do usefully this way without touching some
> aspect of the Lisp machine.  Assuming that what you do has some
> relevance to the Emacs session, of course.

Actually, there is lots of useful stuff that can be done this way.

For example, if a C library internally opens a network socket and sends
a request to a network service, that does not touch the Lisp machine and
so can be done this way.

Is that not useful?  Or do these touch the Lisp machine in some way I
don't know about?

Since it's useful for me, I'd like to write a patch which allow modules
to do this; would it be considered?




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 19:51           ` Spencer Baugh
@ 2024-03-01 20:42             ` Eli Zaretskii
  2024-03-01 21:21               ` Spencer Baugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-01 20:42 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel

> From: Spencer Baugh <sbaugh@janestreet.com>
> Date: Fri, 01 Mar 2024 14:51:01 -0500
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> 
> > There's very little you can do usefully this way without touching some
> > aspect of the Lisp machine.  Assuming that what you do has some
> > relevance to the Emacs session, of course.
> 
> Actually, there is lots of useful stuff that can be done this way.
> 
> For example, if a C library internally opens a network socket and sends
> a request to a network service, that does not touch the Lisp machine and
> so can be done this way.
> 
> Is that not useful?  Or do these touch the Lisp machine in some way I
> don't know about?

Where would you take the data for opening the socket? doesn't that
come from some Lisp program or from a Lisp variable?  And how would
you know what kind of request to send? doesn't that come from Lisp as
well?  And what would you do with the stuff you get in response? don't
you want to put this in some Lisp variable or provide as input for
some Lisp program?  Etc. etc.

> Since it's useful for me, I'd like to write a patch which allow modules
> to do this; would it be considered?

Once again: what cannot you do from a native thread started by the
module?  IOW, why would you need access to the global lock machinery
in the first place, if all you want to do is something that is
unrelated to Emacs and its global state?

The code which manipulates the global lock is extremely fragile, so
exposing this to external agents without a very good reason and
without being able to fore some discipline is a bad idea.  And I don't
yet see such a good reason, let alone how to do that in a way that
will preserve the discipline of taking and releasing the lock.  So
please do try to explain better why native threads that are unrelated
to the lock are not the solution for what you have in mind.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 20:42             ` Eli Zaretskii
@ 2024-03-01 21:21               ` Spencer Baugh
  2024-03-01 21:34                 ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: Spencer Baugh @ 2024-03-01 21:21 UTC (permalink / raw)
  To: emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Spencer Baugh <sbaugh@janestreet.com>
>> Date: Fri, 01 Mar 2024 14:51:01 -0500
>> 
>> Eli Zaretskii <eliz@gnu.org> writes:
>> 
>> > There's very little you can do usefully this way without touching some
>> > aspect of the Lisp machine.  Assuming that what you do has some
>> > relevance to the Emacs session, of course.
>> 
>> Actually, there is lots of useful stuff that can be done this way.
>> 
>> For example, if a C library internally opens a network socket and sends
>> a request to a network service, that does not touch the Lisp machine and
>> so can be done this way.
>> 
>> Is that not useful?  Or do these touch the Lisp machine in some way I
>> don't know about?
>
> Where would you take the data for opening the socket? doesn't that
> come from some Lisp program or from a Lisp variable?  And how would
> you know what kind of request to send? doesn't that come from Lisp as
> well?

Yes: I get those things as arguments from Lisp and then convert them
into the native datastructures of my library, which can be used without
further interacting with the Lisp machine.

Then I would release the lock and call into my library, which does some
useful work which takes a while.

> And what would you do with the stuff you get in response? don't
> you want to put this in some Lisp variable or provide as input for
> some Lisp program?  Etc. etc.

Yes: After I finish the call into my library, I would take the lock
again and call further Lisp functions to put the results back into the
Lisp machine.

>> Since it's useful for me, I'd like to write a patch which allow modules
>> to do this; would it be considered?
>
> Once again: what cannot you do from a native thread started by the
> module?  IOW, why would you need access to the global lock machinery
> in the first place, if all you want to do is something that is
> unrelated to Emacs and its global state?

See above: The call into my library, which takes a while, and is useful,
does not touch the Lisp machine.  But other code around that call does
touch the Lisp machine, and so needs to run with the lock.  The ability
to release and re-acquire the lock means my module doesn't hold the lock
when it doesn't need to.




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 21:21               ` Spencer Baugh
@ 2024-03-01 21:34                 ` Eli Zaretskii
  2024-03-01 21:56                   ` Spencer Baugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-01 21:34 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel

> From: Spencer Baugh <sbaugh@janestreet.com>
> Date: Fri, 01 Mar 2024 16:21:03 -0500
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> 
> > Where would you take the data for opening the socket? doesn't that
> > come from some Lisp program or from a Lisp variable?  And how would
> > you know what kind of request to send? doesn't that come from Lisp as
> > well?
> 
> Yes: I get those things as arguments from Lisp and then convert them
> into the native datastructures of my library, which can be used without
> further interacting with the Lisp machine.
> 
> Then I would release the lock and call into my library, which does some
> useful work which takes a while.

How is this different from starting your own native thread, then
releasing the lock?

> > And what would you do with the stuff you get in response? don't
> > you want to put this in some Lisp variable or provide as input for
> > some Lisp program?  Etc. etc.
> 
> Yes: After I finish the call into my library, I would take the lock
> again and call further Lisp functions to put the results back into the
> Lisp machine.

How is this different from doing the same when your native thread
finishes?

> >> Since it's useful for me, I'd like to write a patch which allow modules
> >> to do this; would it be considered?
> >
> > Once again: what cannot you do from a native thread started by the
> > module?  IOW, why would you need access to the global lock machinery
> > in the first place, if all you want to do is something that is
> > unrelated to Emacs and its global state?
> 
> See above: The call into my library, which takes a while, and is useful,
> does not touch the Lisp machine.  But other code around that call does
> touch the Lisp machine, and so needs to run with the lock.  The ability
> to release and re-acquire the lock means my module doesn't hold the lock
> when it doesn't need to.

You didn't answer my question about doing this from a native thread.

As for re-acquiring the lock: you cannot do that with how the Lisp
threads are currently implemented.  You can only wait for the lock to
be released, and try re-acquiring it; whether you succeed is anyone's
guess, if there are other threads competing for the lock.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 21:34                 ` Eli Zaretskii
@ 2024-03-01 21:56                   ` Spencer Baugh
  2024-03-02  6:43                     ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: Spencer Baugh @ 2024-03-01 21:56 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Spencer Baugh <sbaugh@janestreet.com>
>> Date: Fri, 01 Mar 2024 16:21:03 -0500
>> 
>> Eli Zaretskii <eliz@gnu.org> writes:
>> 
>> > Where would you take the data for opening the socket? doesn't that
>> > come from some Lisp program or from a Lisp variable?  And how would
>> > you know what kind of request to send? doesn't that come from Lisp as
>> > well?
>> 
>> Yes: I get those things as arguments from Lisp and then convert them
>> into the native datastructures of my library, which can be used without
>> further interacting with the Lisp machine.
>> 
>> Then I would release the lock and call into my library, which does some
>> useful work which takes a while.
>
> How is this different from starting your own native thread, then
> releasing the lock?

They are completely different things.

Let me say again what this sub-thread is about:

> If a C library provides some blocking function which does some
> complicated form of IO, a module providing bindings for that C library
> can release global_lock before calling that function, and then
> re-acquire the lock after the function returns.  Then Lisp threads
> calling this module function will not block the main Emacs thread.

So, if I am running the following program in a Lisp thread:

(while (do-emacs-things)
  (let ((input (do-emacs-things)))
    (let ((result (call-into-native-module input)))
      (do-emacs-things result))))

and call-into-native-module unlocks the global lock around the calls
into my library, then during the part of call-into-native-module which
calls into my library, the main Emacs thread will not be blocked and can
run in parallel with call-into-native-module.

>> > And what would you do with the stuff you get in response? don't
>> > you want to put this in some Lisp variable or provide as input for
>> > some Lisp program?  Etc. etc.
>> 
>> Yes: After I finish the call into my library, I would take the lock
>> again and call further Lisp functions to put the results back into the
>> Lisp machine.
>
> How is this different from doing the same when your native thread
> finishes?
>
>> >> Since it's useful for me, I'd like to write a patch which allow modules
>> >> to do this; would it be considered?
>> >
>> > Once again: what cannot you do from a native thread started by the
>> > module?  IOW, why would you need access to the global lock machinery
>> > in the first place, if all you want to do is something that is
>> > unrelated to Emacs and its global state?
>> 
>> See above: The call into my library, which takes a while, and is useful,
>> does not touch the Lisp machine.  But other code around that call does
>> touch the Lisp machine, and so needs to run with the lock.  The ability
>> to release and re-acquire the lock means my module doesn't hold the lock
>> when it doesn't need to.
>
> You didn't answer my question about doing this from a native thread.

I hope my answer above clarifies it.

> As for re-acquiring the lock: you cannot do that with how the Lisp
> threads are currently implemented.  You can only wait for the lock to
> be released, and try re-acquiring it; whether you succeed is anyone's
> guess, if there are other threads competing for the lock.

Yes, that is the normal behavior of locks.  I am well aware of it.  By
"re-acquire", I mean (in your words) "wait for the lock to be released,
and try re-acquiring it".

It is okay if my module has to wait for the main Emacs Lisp thread to
stop running before it gets the global lock again.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 19:30     ` tomas
@ 2024-03-01 23:53       ` Dmitry Gutov
  2024-03-02  5:57         ` tomas
  0 siblings, 1 reply; 26+ messages in thread
From: Dmitry Gutov @ 2024-03-01 23:53 UTC (permalink / raw)
  To: tomas, Spencer Baugh; +Cc: emacs-devel

On 01/03/2024 21:30, tomas@tuxteam.de wrote:
>> - Unrelated Lisp thread B is able to take the global lock and run Lisp code
>>    in parallel with module_work on thread A.
>> - On thread A, module_work finishes and returns to Lisp.
> Why has thread A wait up to here? This is what's keeping your thread B
> from playing, no?

I imagine thread A will want to continue its execution when the results 
of the "Emacs-independent work" arrive.

Said work might look like making a network request (as Spencer 
outlined), getting a response, parsing the received JSON structure (not 
into Lisp objects yet, just into the native data structures provided by 
the library), and potentially filtering the parsed structure as well.

Then the lock is re-acquired (which will naturally involve some spinning 
waiting for it), and the parsed data is converted into Lisp structures.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 23:53       ` Dmitry Gutov
@ 2024-03-02  5:57         ` tomas
  2024-03-02 15:35           ` Dmitry Gutov
  0 siblings, 1 reply; 26+ messages in thread
From: tomas @ 2024-03-02  5:57 UTC (permalink / raw)
  To: Dmitry Gutov; +Cc: Spencer Baugh, emacs-devel

[-- Attachment #1: Type: text/plain, Size: 2197 bytes --]

On Sat, Mar 02, 2024 at 01:53:05AM +0200, Dmitry Gutov wrote:
> On 01/03/2024 21:30, tomas@tuxteam.de wrote:
> > > - Unrelated Lisp thread B is able to take the global lock and run Lisp code
> > >    in parallel with module_work on thread A.
> > > - On thread A, module_work finishes and returns to Lisp.
> > Why has thread A wait up to here? This is what's keeping your thread B
> > from playing, no?
> 
> I imagine thread A will want to continue its execution when the results of
> the "Emacs-independent work" arrive.

In that case, I think your only choice would be to "pass the continuation":
in A, stash away whatever you need to continue, let A die, and when things
"come back", start a thread A' to pick up where A left.

> Said work might look like making a network request (as Spencer outlined),
> getting a response, parsing the received JSON structure (not into Lisp
> objects yet, just into the native data structures provided by the library),
> and potentially filtering the parsed structure as well.
> 
> Then the lock is re-acquired (which will naturally involve some spinning
> waiting for it), and the parsed data is converted into Lisp structures.

I'm always wary when people call for "threads" when they mean concurrency.
The javascript/browser folks do it all the time with explicit continuations
and don't seem to care. It's messier in one way, but when you start locking
and potentially deadlocking, threads have their own ways of messiness, as
the Java folks know all too well (sometimes I have the hunch that those
two messineesses are complementary in some weird way ;-)

As sketched above, you can even combine things (I'd be scared to do, but
there you go).

Real threads, IMO, only really help you in some cases:

 - when you really want more than one CPU core working on your stuff
   (and distributing to several processes is not an option)
 - when you have to use some lower level lib which blocks (and you
   can't afford to take it apart and re-implement it non-blocking)

My experience (this was mostly plain old C), once you've wrapped your
head around continuation style, it is far easier to debug.

Cheers
-- 
t

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 195 bytes --]

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-01 21:56                   ` Spencer Baugh
@ 2024-03-02  6:43                     ` Eli Zaretskii
  2024-03-02 16:39                       ` sbaugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-02  6:43 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: emacs-devel

> From: Spencer Baugh <sbaugh@janestreet.com>
> Cc: emacs-devel@gnu.org
> Date: Fri, 01 Mar 2024 16:56:55 -0500
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> 
> >> Then I would release the lock and call into my library, which does some
> >> useful work which takes a while.
> >
> > How is this different from starting your own native thread, then
> > releasing the lock?
> 
> They are completely different things.

I think there's a misunderstanding here, because I cannot see how they
could be "completely different things."  See below.

> > If a C library provides some blocking function which does some
> > complicated form of IO, a module providing bindings for that C library
> > can release global_lock before calling that function, and then
> > re-acquire the lock after the function returns.  Then Lisp threads
> > calling this module function will not block the main Emacs thread.
> 
> So, if I am running the following program in a Lisp thread:
> 
> (while (do-emacs-things)
>   (let ((input (do-emacs-things)))
>     (let ((result (call-into-native-module input)))
>       (do-emacs-things result))))
> 
> and call-into-native-module unlocks the global lock around the calls
> into my library, then during the part of call-into-native-module which
> calls into my library, the main Emacs thread will not be blocked and can
> run in parallel with call-into-native-module.

I'm saying that your call-into-native-module method could do the
following:

  . start a native thread running the call into your library
  . release the global lock by calling thread-yield or sleep-for or
    any other API which yields to other Lisp threads
  . wait for the native thread to finish
  . return when it succeeds to acquire the global lock following the
    completion of the native thread

The _only_ difference between the above and what you described is that
portions of call-into-native-module are run in a separate native
thread, and the Lisp thread which runs the above snippet keeps
yielding until the native thread finishes its job.  How is this
"completely different"?

> > You didn't answer my question about doing this from a native thread.
> 
> I hope my answer above clarifies it.

I think it demonstrates some significant misunderstanding, which I
how I have now clarified.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02  5:57         ` tomas
@ 2024-03-02 15:35           ` Dmitry Gutov
  2024-03-02 16:31             ` tomas
  0 siblings, 1 reply; 26+ messages in thread
From: Dmitry Gutov @ 2024-03-02 15:35 UTC (permalink / raw)
  To: tomas; +Cc: Spencer Baugh, emacs-devel

On 02/03/2024 07:57, tomas@tuxteam.de wrote:
> On Sat, Mar 02, 2024 at 01:53:05AM +0200, Dmitry Gutov wrote:
>> On 01/03/2024 21:30, tomas@tuxteam.de wrote:
>>>> - Unrelated Lisp thread B is able to take the global lock and run Lisp code
>>>>     in parallel with module_work on thread A.
>>>> - On thread A, module_work finishes and returns to Lisp.
>>> Why has thread A wait up to here? This is what's keeping your thread B
>>> from playing, no?
>>
>> I imagine thread A will want to continue its execution when the results of
>> the "Emacs-independent work" arrive.
> 
> In that case, I think your only choice would be to "pass the continuation":
> in A, stash away whatever you need to continue, let A die, and when things
> "come back", start a thread A' to pick up where A left.

Almost, except "suspend/yield" instead of "let A die".

And if the Lisp threads are backed by OS threads, such thread is waiting 
without purpose while "suspended" - Spencer's suggestion could put it to 
work in the meantime as well. Though the difference in performance might 
not be very significant, given our overhead in other places.

>> Said work might look like making a network request (as Spencer outlined),
>> getting a response, parsing the received JSON structure (not into Lisp
>> objects yet, just into the native data structures provided by the library),
>> and potentially filtering the parsed structure as well.
>>
>> Then the lock is re-acquired (which will naturally involve some spinning
>> waiting for it), and the parsed data is converted into Lisp structures.
> 
> I'm always wary when people call for "threads" when they mean concurrency.

Yes, our Lisp threads are a bit unusual, terminology-wise.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02 15:35           ` Dmitry Gutov
@ 2024-03-02 16:31             ` tomas
  2024-03-02 21:41               ` sbaugh
  0 siblings, 1 reply; 26+ messages in thread
From: tomas @ 2024-03-02 16:31 UTC (permalink / raw)
  To: Dmitry Gutov; +Cc: Spencer Baugh, emacs-devel

[-- Attachment #1: Type: text/plain, Size: 1305 bytes --]

On Sat, Mar 02, 2024 at 05:35:26PM +0200, Dmitry Gutov wrote:
> On 02/03/2024 07:57, tomas@tuxteam.de wrote:
> > On Sat, Mar 02, 2024 at 01:53:05AM +0200, Dmitry Gutov wrote:
> > > On 01/03/2024 21:30, tomas@tuxteam.de wrote:
> > > > > - Unrelated Lisp thread B is able to take the global lock and run Lisp code
> > > > >     in parallel with module_work on thread A.
> > > > > - On thread A, module_work finishes and returns to Lisp.
> > > > Why has thread A wait up to here? This is what's keeping your thread B
> > > > from playing, no?
> > > 
> > > I imagine thread A will want to continue its execution when the results of
> > > the "Emacs-independent work" arrive.
> > 
> > In that case, I think your only choice would be to "pass the continuation":
> > in A, stash away whatever you need to continue, let A die, and when things
> > "come back", start a thread A' to pick up where A left.
> 
> Almost, except "suspend/yield" instead of "let A die".

This only if you can let Lisp suspend/yield safely -- i.e. in a way nothing
bad happens if someone else gets a turn at playing the "interpreter" [1].

I don't think this is currently possible.

Cheers

[1] Well, with a compiler and that, you'd call it a "runtime", but the
   intention should be clear.

Cheers
-- 
t

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 195 bytes --]

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02  6:43                     ` Eli Zaretskii
@ 2024-03-02 16:39                       ` sbaugh
  2024-03-02 17:02                         ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: sbaugh @ 2024-03-02 16:39 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: Spencer Baugh, emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:
>> From: Spencer Baugh <sbaugh@janestreet.com>
>> Cc: emacs-devel@gnu.org
>> Date: Fri, 01 Mar 2024 16:56:55 -0500
>> 
>> Eli Zaretskii <eliz@gnu.org> writes:
>> 
>> >> Then I would release the lock and call into my library, which does some
>> >> useful work which takes a while.
>> >
>> > How is this different from starting your own native thread, then
>> > releasing the lock?
>> 
>> They are completely different things.
>
> I think there's a misunderstanding here, because I cannot see how they
> could be "completely different things."  See below.

Oh, yes, you are correct, I didn't understand what you were proposing.
Thank you for your patience, your longer explanation has made your
proposal clear to me now.

>> > If a C library provides some blocking function which does some
>> > complicated form of IO, a module providing bindings for that C library
>> > can release global_lock before calling that function, and then
>> > re-acquire the lock after the function returns.  Then Lisp threads
>> > calling this module function will not block the main Emacs thread.
>> 
>> So, if I am running the following program in a Lisp thread:
>> 
>> (while (do-emacs-things)
>>   (let ((input (do-emacs-things)))
>>     (let ((result (call-into-native-module input)))
>>       (do-emacs-things result))))
>> 
>> and call-into-native-module unlocks the global lock around the calls
>> into my library, then during the part of call-into-native-module which
>> calls into my library, the main Emacs thread will not be blocked and can
>> run in parallel with call-into-native-module.
>
> I'm saying that your call-into-native-module method could do the
> following:
>
>   . start a native thread running the call into your library
>   . release the global lock by calling thread-yield or sleep-for or
>     any other API which yields to other Lisp threads
>   . wait for the native thread to finish
>   . return when it succeeds to acquire the global lock following the
>     completion of the native thread
>
> The _only_ difference between the above and what you described is that
> portions of call-into-native-module are run in a separate native
> thread, and the Lisp thread which runs the above snippet keeps
> yielding until the native thread finishes its job.  How is this
> "completely different"?

Oh, yes, that is similar to what I'm proposing.

Just to summarize, if call_into_native_module looks like:

emacs_value call_into_native_module(emacs_env *env, emacs_value input) {
  native_value native_input = convert_to_native(env, input);
  native_value native_output;

  [...some code...]

  return convert_to_emacs(env, native_output);
}

Then the current state of the world ("hold the lock" model):

  native_output = some_native_function(native_input);

If I understand correctly, you are proposing (the "new thread" model):

  native_thread_handle handle = native_thread_create(some_native_function, native_input);
  while (!native_thread_done(handle)) {
    emacs_thread_yield(env);
  }
  native_output = native_thread_result(handle);

And I am proposing (the "release lock" model):

  release_global_lock(env);
  native_output = some_native_function(native_input);
  acquire_global_lock(env);
  
All three of these are used in the same way from Lisp programs.  But the
"new thread" and "release lock" models have the advantage over the "hold
the lock" model that if called from a Lisp thread, that Lisp thread will
run some_native_function in parallel with Lisp execution on other Lisp
threads, including the main Emacs thread.

To check my understanding: does this all seem correct so far, and match
your proposal?

So, the main difference between the "new thread" model and the "release
lock" model is that creating a native thread takes a nontrivial amount
of time; maybe around 0.1 milliseconds.  If some_native_function would
takes less time than that, the thread creation cost will slow down
Emacs, especially because the native module creates the native thread
while holding the Lisp global_lock.

Reducing the thread creation cost is quite hard; a thread pool can help,
but that is complex and still has substantial cost in communication and
context switching.

So, to avoid the thread creation overhead, a native module should only
create a native thread to run some_native_function if the native module
knows that some_native_function will take a long time.

Unfortunately, most of the time a native module can't know how long
some_native_function will take before calling it.  For example, native
functions which make network calls might return immediately when there's
already data available from the network, but will have to send new
network requests sometimes.  Or, native functions which provide an
in-memory database might be fast or slow depending on the database
contents.

Since the native module doesn't know if some_native_function will take a
long time, the native module needs a way to allow some_native_function
to run in parallel which is cheap, so the native module can do it for
all calls to some_native_function.

The "release lock" model fits this need.  Releasing the lock is
essentially free, and allows some_native_function to run in parallel
with other Lisp threads.  Re-acquiring the lock afterwards will take
time if the operating system switched to other Lisp threads which
acquired the lock, but that's OK: the other Lisp threads are doing
useful work in Lisp in parallel with call_into_native_module waiting for
them to release the lock.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02 16:39                       ` sbaugh
@ 2024-03-02 17:02                         ` Eli Zaretskii
  2024-03-02 20:33                           ` Spencer Baugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-02 17:02 UTC (permalink / raw)
  To: sbaugh; +Cc: sbaugh, emacs-devel

> From: sbaugh@catern.com
> Date: Sat, 02 Mar 2024 16:39:26 +0000 (UTC)
> Cc: Spencer Baugh <sbaugh@janestreet.com>, emacs-devel@gnu.org
> 
> Oh, yes, that is similar to what I'm proposing.
> 
> Just to summarize, if call_into_native_module looks like:
> 
> emacs_value call_into_native_module(emacs_env *env, emacs_value input) {
>   native_value native_input = convert_to_native(env, input);
>   native_value native_output;
> 
>   [...some code...]
> 
>   return convert_to_emacs(env, native_output);
> }
> 
> Then the current state of the world ("hold the lock" model):
> 
>   native_output = some_native_function(native_input);
> 
> If I understand correctly, you are proposing (the "new thread" model):
> 
>   native_thread_handle handle = native_thread_create(some_native_function, native_input);
>   while (!native_thread_done(handle)) {
>     emacs_thread_yield(env);
>   }
>   native_output = native_thread_result(handle);
> 
> And I am proposing (the "release lock" model):
> 
>   release_global_lock(env);
>   native_output = some_native_function(native_input);
>   acquire_global_lock(env);
>   
> All three of these are used in the same way from Lisp programs.  But the
> "new thread" and "release lock" models have the advantage over the "hold
> the lock" model that if called from a Lisp thread, that Lisp thread will
> run some_native_function in parallel with Lisp execution on other Lisp
> threads, including the main Emacs thread.
> 
> To check my understanding: does this all seem correct so far, and match
> your proposal?

Yes.

> So, the main difference between the "new thread" model and the "release
> lock" model is that creating a native thread takes a nontrivial amount
> of time; maybe around 0.1 milliseconds.  If some_native_function would
> takes less time than that, the thread creation cost will slow down
> Emacs, especially because the native module creates the native thread
> while holding the Lisp global_lock.

Why are you worried by 0.1 msec slowdown (if it indeed takes that
long; I think it should be around 10 to 20 usec at most)?  If this
kind of slowdown is important for you, you are using the wrong
software package (and probably the wrong OS as well).

> The "release lock" model fits this need.

But it exposes the sensitive internals and runs the risk of more than
one Lisp thread running at the same time, and thus is not acceptable.
I'm afraid we will have to live with the 0.1 msec overhead.

> Releasing the lock is essentially free

Nothing is ever free in the world of synchronization primitives.  But
that's a btw, not the most important aspect of this.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02 17:02                         ` Eli Zaretskii
@ 2024-03-02 20:33                           ` Spencer Baugh
  2024-03-03  6:13                             ` Eli Zaretskii
  0 siblings, 1 reply; 26+ messages in thread
From: Spencer Baugh @ 2024-03-02 20:33 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: sbaugh, emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:
>> From: sbaugh@catern.com
>> Date: Sat, 02 Mar 2024 16:39:26 +0000 (UTC)
>> Cc: Spencer Baugh <sbaugh@janestreet.com>, emacs-devel@gnu.org
>> 
>> Oh, yes, that is similar to what I'm proposing.
>> 
>> Just to summarize, if call_into_native_module looks like:
>> 
>> emacs_value call_into_native_module(emacs_env *env, emacs_value input) {
>>   native_value native_input = convert_to_native(env, input);
>>   native_value native_output;
>> 
>>   [...some code...]
>> 
>>   return convert_to_emacs(env, native_output);
>> }
>> 
>> Then the current state of the world ("hold the lock" model):
>> 
>>   native_output = some_native_function(native_input);
>> 
>> If I understand correctly, you are proposing (the "new thread" model):
>> 
>>   native_thread_handle handle = native_thread_create(some_native_function, native_input);
>>   while (!native_thread_done(handle)) {
>>     emacs_thread_yield(env);
>>   }
>>   native_output = native_thread_result(handle);
>> 
>> And I am proposing (the "release lock" model):
>> 
>>   release_global_lock(env);
>>   native_output = some_native_function(native_input);
>>   acquire_global_lock(env);
>>   
>> All three of these are used in the same way from Lisp programs.  But the
>> "new thread" and "release lock" models have the advantage over the "hold
>> the lock" model that if called from a Lisp thread, that Lisp thread will
>> run some_native_function in parallel with Lisp execution on other Lisp
>> threads, including the main Emacs thread.
>> 
>> To check my understanding: does this all seem correct so far, and match
>> your proposal?
>
> Yes.
>
>> So, the main difference between the "new thread" model and the "release
>> lock" model is that creating a native thread takes a nontrivial amount
>> of time; maybe around 0.1 milliseconds.  If some_native_function would
>> takes less time than that, the thread creation cost will slow down
>> Emacs, especially because the native module creates the native thread
>> while holding the Lisp global_lock.
>
> Why are you worried by 0.1 msec slowdown (if it indeed takes that
> long; I think it should be around 10 to 20 usec at most)?  If this
> kind of slowdown is important for you, you are using the wrong
> software package (and probably the wrong OS as well).

Because a Lisp program that uses a native module might make thousands of
module calls.  This is fine when each call takes a microsecond.  If we
add, for example, 500 microseconds of overhead to each module call, then
1000 module calls will take half a second.

For example: I have a project.el backend which uses a native module.
Looking up the project for the current directory and then getting the
name of the project makes around 5 module calls.  I have around 200
projects.  That works out to 1000 module calls to get the names of all
my projects.  Currently with the native module backend this takes around
a millisecond.  With 500 extra microseconds per call, it will take half
a second.

>> The "release lock" model fits this need.
>
> But it exposes the sensitive internals and runs the risk of more than
> one Lisp thread running at the same time, and thus is not acceptable.

Yes.  But of course in practice we would find a design which allows
releasing the lock but is hard to misuse.

How about this:

   native_output = env->call_without_lock(some_native_function, native_input);

call_without_lock would be a function which releases the global lock,
calls a specified function, then acquires the global lock again.

That seems hard to misuse.  It is about equivalent to:

   native_output = run_in_native_thread(some_native_function, native_input);

which is possible today for module programmers.  Just, call_without_lock
would be much faster.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02 16:31             ` tomas
@ 2024-03-02 21:41               ` sbaugh
  2024-03-03  6:25                 ` tomas
  0 siblings, 1 reply; 26+ messages in thread
From: sbaugh @ 2024-03-02 21:41 UTC (permalink / raw)
  To: tomas; +Cc: Dmitry Gutov, Spencer Baugh, emacs-devel

tomas@tuxteam.de writes:
> On Sat, Mar 02, 2024 at 05:35:26PM +0200, Dmitry Gutov wrote:
>> On 02/03/2024 07:57, tomas@tuxteam.de wrote:
>> > On Sat, Mar 02, 2024 at 01:53:05AM +0200, Dmitry Gutov wrote:
>> > > On 01/03/2024 21:30, tomas@tuxteam.de wrote:
>> > > > > - Unrelated Lisp thread B is able to take the global lock and run Lisp code
>> > > > >     in parallel with module_work on thread A.
>> > > > > - On thread A, module_work finishes and returns to Lisp.
>> > > > Why has thread A wait up to here? This is what's keeping your thread B
>> > > > from playing, no?
>> > > 
>> > > I imagine thread A will want to continue its execution when the results of
>> > > the "Emacs-independent work" arrive.
>> > 
>> > In that case, I think your only choice would be to "pass the continuation":
>> > in A, stash away whatever you need to continue, let A die, and when things
>> > "come back", start a thread A' to pick up where A left.
>> 
>> Almost, except "suspend/yield" instead of "let A die".
>
> This only if you can let Lisp suspend/yield safely -- i.e. in a way nothing
> bad happens if someone else gets a turn at playing the "interpreter" [1].
>
> I don't think this is currently possible.

It is currently possible: this is what thread-yield, sleep-for, and
accept-process-output do when run from a Lisp thread.  The thread pauses
execution and other threads execute.  This has worked since the
introduction of thread support.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02 20:33                           ` Spencer Baugh
@ 2024-03-03  6:13                             ` Eli Zaretskii
  2024-03-03 13:19                               ` sbaugh
  0 siblings, 1 reply; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-03  6:13 UTC (permalink / raw)
  To: Spencer Baugh; +Cc: sbaugh, emacs-devel

> From: Spencer Baugh <sbaugh@catern.com>
> Date: Sat, 02 Mar 2024 20:33:05 +0000 (UTC)
> Cc: sbaugh@janestreet.com, emacs-devel@gnu.org
> 
> >> So, the main difference between the "new thread" model and the "release
> >> lock" model is that creating a native thread takes a nontrivial amount
> >> of time; maybe around 0.1 milliseconds.  If some_native_function would
> >> takes less time than that, the thread creation cost will slow down
> >> Emacs, especially because the native module creates the native thread
> >> while holding the Lisp global_lock.
> >
> > Why are you worried by 0.1 msec slowdown (if it indeed takes that
> > long; I think it should be around 10 to 20 usec at most)?  If this
> > kind of slowdown is important for you, you are using the wrong
> > software package (and probably the wrong OS as well).
> 
> Because a Lisp program that uses a native module might make thousands of
> module calls.  This is fine when each call takes a microsecond.  If we
> add, for example, 500 microseconds of overhead to each module call, then
> 1000 module calls will take half a second.
> 
> For example: I have a project.el backend which uses a native module.
> Looking up the project for the current directory and then getting the
> name of the project makes around 5 module calls.  I have around 200
> projects.  That works out to 1000 module calls to get the names of all
> my projects.  Currently with the native module backend this takes around
> a millisecond.  With 500 extra microseconds per call, it will take half
> a second.

A module whose call takes this little time to complete and whose calls
from Lisp are so massive should not, and need not, use this technique,
because releasing the global lock for such a short time will not
benefit anything.  Releasing a global lock is only beneficial if a
module call does a lot of work, during which other Lisp threads could
do something useful.  but if a module call takes a significant time
(say, a few seconds), then adding a 20 usecs to that time is
insignificant.

> >> The "release lock" model fits this need.
> >
> > But it exposes the sensitive internals and runs the risk of more than
> > one Lisp thread running at the same time, and thus is not acceptable.
> 
> Yes.  But of course in practice we would find a design which allows
> releasing the lock but is hard to misuse.

Sorry, I don't believe this is reliably possible, or safe enough to
justify exposing the lock to Lisp.  Certainly not to modules, which is
an open-ended gateway into the bowels of the Emacs Lisp machine.

> How about this:
> 
>    native_output = env->call_without_lock(some_native_function, native_input);
> 
> call_without_lock would be a function which releases the global lock,
> calls a specified function, then acquires the global lock again.

Sorry, I don't agree to adding such interfaces to Emacs.  And if you
are still not convinced, let's agree to disagree.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-02 21:41               ` sbaugh
@ 2024-03-03  6:25                 ` tomas
  0 siblings, 0 replies; 26+ messages in thread
From: tomas @ 2024-03-03  6:25 UTC (permalink / raw)
  To: sbaugh; +Cc: Dmitry Gutov, Spencer Baugh, emacs-devel

[-- Attachment #1: Type: text/plain, Size: 481 bytes --]

On Sat, Mar 02, 2024 at 09:41:38PM +0000, sbaugh@catern.com wrote:
> tomas@tuxteam.de writes:

[safe suspend wrt. concurrency]

> > I don't think this is currently possible.
> 
> It is currently possible: this is what thread-yield, sleep-for, and
> accept-process-output do when run from a Lisp thread.  The thread pauses
> execution and other threads execute.  This has worked since the
> introduction of thread support.

Thanks for the correction.

Cheers
-- 
t

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 195 bytes --]

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-03  6:13                             ` Eli Zaretskii
@ 2024-03-03 13:19                               ` sbaugh
  2024-03-03 15:42                                 ` Dmitry Gutov
  2024-03-03 15:51                                 ` Eli Zaretskii
  0 siblings, 2 replies; 26+ messages in thread
From: sbaugh @ 2024-03-03 13:19 UTC (permalink / raw)
  To: emacs-devel

Eli Zaretskii <eliz@gnu.org> writes:
>> From: Spencer Baugh <sbaugh@catern.com>
>> Date: Sat, 02 Mar 2024 20:33:05 +0000 (UTC)
>> Cc: sbaugh@janestreet.com, emacs-devel@gnu.org
>> 
>> >> So, the main difference between the "new thread" model and the "release
>> >> lock" model is that creating a native thread takes a nontrivial amount
>> >> of time; maybe around 0.1 milliseconds.  If some_native_function would
>> >> takes less time than that, the thread creation cost will slow down
>> >> Emacs, especially because the native module creates the native thread
>> >> while holding the Lisp global_lock.
>> >
>> > Why are you worried by 0.1 msec slowdown (if it indeed takes that
>> > long; I think it should be around 10 to 20 usec at most)?  If this
>> > kind of slowdown is important for you, you are using the wrong
>> > software package (and probably the wrong OS as well).
>> 
>> Because a Lisp program that uses a native module might make thousands of
>> module calls.  This is fine when each call takes a microsecond.  If we
>> add, for example, 500 microseconds of overhead to each module call, then
>> 1000 module calls will take half a second.
>> 
>> For example: I have a project.el backend which uses a native module.
>> Looking up the project for the current directory and then getting the
>> name of the project makes around 5 module calls.  I have around 200
>> projects.  That works out to 1000 module calls to get the names of all
>> my projects.  Currently with the native module backend this takes around
>> a millisecond.  With 500 extra microseconds per call, it will take half
>> a second.
>
> A module whose call takes this little time to complete and whose calls
> from Lisp are so massive should not, and need not, use this technique,
> because releasing the global lock for such a short time will not
> benefit anything.  Releasing a global lock is only beneficial if a
> module call does a lot of work, during which other Lisp threads could
> do something useful.  but if a module call takes a significant time
> (say, a few seconds), then adding a 20 usecs to that time is
> insignificant.

Yes, to avoid the thread creation overhead, a native module should only
create a native thread to run some_native_function if the native module
knows that some_native_function will take a long time.

Unfortunately, most of the time a native module can't know how long
some_native_function will take before calling it.  For example, native
functions which make network calls might return immediately when there's
already data available from the network, but will have to send new
network requests sometimes.  Or, native functions which provide an
in-memory database might be fast or slow depending on the database
contents.

Since the native module doesn't know if some_native_function will take a
long time, the native module needs a way to allow some_native_function
to run in parallel which is cheap, so the native module can do it for
all calls to some_native_function.

>> >> The "release lock" model fits this need.
>> >
>> > But it exposes the sensitive internals and runs the risk of more than
>> > one Lisp thread running at the same time, and thus is not acceptable.
>> 
>> Yes.  But of course in practice we would find a design which allows
>> releasing the lock but is hard to misuse.
>
> Sorry, I don't believe this is reliably possible, or safe enough to
> justify exposing the lock to Lisp.  Certainly not to modules, which is
> an open-ended gateway into the bowels of the Emacs Lisp machine.
>
>> How about this:
>> 
>>    native_output = env->call_without_lock(some_native_function, native_input);
>> 
>> call_without_lock would be a function which releases the global lock,
>> calls a specified function, then acquires the global lock again.
>
> Sorry, I don't agree to adding such interfaces to Emacs.  And if you
> are still not convinced, let's agree to disagree.

That is understandable, but I think you are not yet appreciating that
this can be a very useful way to introduce parallelism with high
performance.  And that env->call_without_lock is no less safe than the
method you suggested using.

Is there anything that would convince you of such things?




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-03 13:19                               ` sbaugh
@ 2024-03-03 15:42                                 ` Dmitry Gutov
  2024-03-03 15:51                                 ` Eli Zaretskii
  1 sibling, 0 replies; 26+ messages in thread
From: Dmitry Gutov @ 2024-03-03 15:42 UTC (permalink / raw)
  To: sbaugh, emacs-devel

On 03/03/2024 15:19, sbaugh@catern.com wrote:
>>> How about this:
>>>
>>>     native_output = env->call_without_lock(some_native_function, native_input);
>>>
>>> call_without_lock would be a function which releases the global lock,
>>> calls a specified function, then acquires the global lock again.
>> Sorry, I don't agree to adding such interfaces to Emacs.  And if you
>> are still not convinced, let's agree to disagree.
> That is understandable, but I think you are not yet appreciating that
> this can be a very useful way to introduce parallelism with high
> performance.  And that env->call_without_lock is no less safe than the
> method you suggested using.
> 
> Is there anything that would convince you of such things?

Perhaps some data from benchmarking real-life code?

For example, you could take the route proposed by Eli, implement your 
intended workload, make some measurements, and then try to calculate 
whether the native thread creation overhead makes a noticeable difference.



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: Releasing the thread global_lock from the module API
  2024-03-03 13:19                               ` sbaugh
  2024-03-03 15:42                                 ` Dmitry Gutov
@ 2024-03-03 15:51                                 ` Eli Zaretskii
  1 sibling, 0 replies; 26+ messages in thread
From: Eli Zaretskii @ 2024-03-03 15:51 UTC (permalink / raw)
  To: sbaugh; +Cc: emacs-devel

> From: sbaugh@catern.com
> Date: Sun, 03 Mar 2024 13:19:04 +0000
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> >
> > A module whose call takes this little time to complete and whose calls
> > from Lisp are so massive should not, and need not, use this technique,
> > because releasing the global lock for such a short time will not
> > benefit anything.  Releasing a global lock is only beneficial if a
> > module call does a lot of work, during which other Lisp threads could
> > do something useful.  but if a module call takes a significant time
> > (say, a few seconds), then adding a 20 usecs to that time is
> > insignificant.
> 
> Yes, to avoid the thread creation overhead, a native module should only
> create a native thread to run some_native_function if the native module
> knows that some_native_function will take a long time.
> 
> Unfortunately, most of the time a native module can't know how long
> some_native_function will take before calling it.  For example, native
> functions which make network calls might return immediately when there's
> already data available from the network, but will have to send new
> network requests sometimes.  Or, native functions which provide an
> in-memory database might be fast or slow depending on the database
> contents.

I believe the cases where it is not known in advance whether a module
call can take only a few tens or hundreds of microseconds or longer
are very rare.  And if the timing depends on some internally
maintained data, such as the size of a DB, the decision can even be
made dynamically, at least in some cases (although I'd consider that a
premature optimization).

> Since the native module doesn't know if some_native_function will take a
> long time

I think in most cases it can and does know.  In general, only rare
module calls will take such a long time that releasing the lock will
be justified.  Module calls that are fast enough shouldn't bother.

> > Sorry, I don't agree to adding such interfaces to Emacs.  And if you
> > are still not convinced, let's agree to disagree.
> 
> That is understandable, but I think you are not yet appreciating that
> this can be a very useful way to introduce parallelism with high
> performance.

I think I do appreciate the odds.  More importantly, I have a lot of
gray hair from fixing various problems with our threads.  I also don't
believe Emacs is a platform suitable for high-performance parallelism.

> Is there anything that would convince you of such things?

Unlikely.  Maybe a complete redesign of how threads are switched in
Emacs.  I don't think you have any idea how fragile the machinery we
have now really is.



^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2024-03-03 15:51 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-03-01 14:53 Releasing the thread global_lock from the module API Spencer Baugh
2024-03-01 16:47 ` Eli Zaretskii
2024-03-01 17:34   ` Spencer Baugh
2024-03-01 18:44     ` Eli Zaretskii
2024-03-01 19:02       ` Spencer Baugh
2024-03-01 19:26         ` Eli Zaretskii
2024-03-01 19:51           ` Spencer Baugh
2024-03-01 20:42             ` Eli Zaretskii
2024-03-01 21:21               ` Spencer Baugh
2024-03-01 21:34                 ` Eli Zaretskii
2024-03-01 21:56                   ` Spencer Baugh
2024-03-02  6:43                     ` Eli Zaretskii
2024-03-02 16:39                       ` sbaugh
2024-03-02 17:02                         ` Eli Zaretskii
2024-03-02 20:33                           ` Spencer Baugh
2024-03-03  6:13                             ` Eli Zaretskii
2024-03-03 13:19                               ` sbaugh
2024-03-03 15:42                                 ` Dmitry Gutov
2024-03-03 15:51                                 ` Eli Zaretskii
2024-03-01 19:30     ` tomas
2024-03-01 23:53       ` Dmitry Gutov
2024-03-02  5:57         ` tomas
2024-03-02 15:35           ` Dmitry Gutov
2024-03-02 16:31             ` tomas
2024-03-02 21:41               ` sbaugh
2024-03-03  6:25                 ` tomas

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).