Emacs Berlin Meetup: Using Threads in Emacs Lisp - the Tramp Case
Table of Contents
- 1. Emacs Lisp and Threads
- 2. Basic Thread Functions in Emacs Lisp
- 2.1. A thread is the concurrent invocation of a Lisp function.
- 2.2. If Emacs is compiled with thread support, there are some
- 2.3. Thread switching will occur upon explicit request via
- 2.4. The result of a thread evaluation is captured by
- 2.5. A thread could be signaled from another thread by
thread-signal
. - 2.6. There are further convenience variables and functions like
- 3. Thread Synchronization
- 4. I/O
- 5. Tramp Adaptions for Being thread-safe
- 6. Asynchronous File Operations
- 6.1. Shall be triggered by the user. In order to do this,
- 6.2. In the future, this prefix command is not restricted to
- 6.3. Implemented so far for the file visiting family of commands:
- 6.4. Visiting local files would profit only for veeeery large
- 6.5. An alternative approach to use asynchronous file operations
- 7. Outlook
- 7.1. Solve the I/O problem. First for input (keyboard), but
- 7.2. Improve performance. Apply more fine-tuned
thread-yield
- 7.3. Implement thread support for further file operations,
- 7.4. Make file operations for local files thread-aware.
- 7.5. Make
vc-refresh-state
thread-aware (callthried-yield
). - 7.6. Add indication for running asynchronous file operations
- 7.7. Add thread support to other packages, like
dired
.
- 8. The End
- 8.1. Local Variables:
- 8.2. org-confirm-babel-evaluate: nil
- 8.3. org-return-follows-link: t
- 8.4. org-hide-macro-markers: t
- 8.5. eval: (setq large-file-warning-threshold 100000000)
- 8.6. eval: (switch-to-buffer-other-frame (list-threads))
- 8.7. eval: (find-file "/nextcloud:albinus@ford#8081:20180825091134.jpg")
- 8.8. End:
1 Emacs Lisp and Threads
1.1 Treads have been added in Emacs 26.1. No major package
has used it there, 'tho.
1.2 During the current development of Emacs 27, Tramp has
started as first major package to apply threads. While
implementing this, thread support in Emacs has been
extended as needs arise. The following examples are based
on the Emacs git branch feature/tramp-thread-safe
; some
examples will work only there.
1.3 Other packages (Gnus, for example) plan also to use threads.
2 Basic Thread Functions in Emacs Lisp
2.1 A thread is the concurrent invocation of a Lisp function.
It runs until the execution of the function is finished, or it is interrupted by a signal. This is mostly cooperative, meaning that Emacs will only switch execution between threads at well-defined times.
(defun my-thread () (sleep-for 10)) (make-thread #'my-thread "my thread")
There is no real concurrency that two threads run in parallel on different CPUs. There is always only one active thread at any given time. Emacs threads are not intended for number crunching.
2.2 If Emacs is compiled with thread support, there are some
helper functions:
main-thread (current-thread) (all-threads)
2.3 Thread switching will occur upon explicit request via
thread-yield
, when waiting for keyboard input or for
process output (e.g., during accept-process-output
), or
during blocking operations relating to threads, such as
mutex locking or thread-join
.
(defvar wait t) (defun my-thread () (while wait (thread-yield)) (setq wait t)) (setq wait t) (make-thread #'my-thread "my thread 1") (make-thread #'my-thread "my thread 2") ;; (setq wait nil)
2.4 The result of a thread evaluation is captured by
thread-join
. It blocks the current thread until the requested
thread exits, or the current thread is signaled. This result could
be even retrieved by a thread-join
call after that thread has
finished, as many time as requested.
(defun my-thread1 () (sleep-for 10) ;; The result. 42) (defun my-thread2 () (message "Thread %s has returned %s" thread1 (thread-join thread1))) (setq thread1 (make-thread #'my-thread1 "my thread 1")) (setq thread2 (make-thread #'my-thread2 "my thread 2")) ;; (thread-join thread1)
2.5 A thread could be signaled from another thread by thread-signal
.
(defun my-thread1 () (sleep-for 10) (thread-signal thread2 'error '(data))) (defun my-thread2 () (condition-case err (while t (sleep-for 0.1)) (error (message "Thread %s signaled by `%s'" (current-thread) err)))) (setq thread1 (make-thread #'my-thread1 "my thread 1")) (setq thread2 (make-thread #'my-thread2 "my thread 2"))
Restriction: signaling the main thread does not work this way. The main thread just prints the error message.
(defun my-thread () (thread-signal main-thread 'error '(data))) (setq thread (make-thread #'my-thread "my thread"))
2.6 There are further convenience variables and functions like
(thread-name (make-thread #'ignore "my thread")) (thread-live-p (make-thread #'ignore "my thread"))
Read the Elisp manual at elisp#Basic Thread Functions
3 Thread Synchronization
3.1 A mutex is an exclusive lock. At any moment, zero or one
threads may own a mutex. If a thread attempts to acquire a mutex, and the mutex is already owned by some other thread, then the acquiring thread will block until the mutex becomes available. If the mutex is owned by the acquiring thread already, a counter is increased.
When done, a thread owning a mutex shall release it as soon as possible. It is an error not to release a mutex.
While there are basic functions like mutex-lock
and
mutex-unlock
, it is recommended to use the macro with-mutex
.
(defvar mutex (make-mutex "my mutex")) (defun my-thread () (with-mutex mutex (sleep-for 10))) (make-thread #'my-thread "my thread 1") (make-thread #'my-thread "my thread 2")
Read the Elisp manual at elisp#Mutexes
3.2 A condition variable is a way for a thread to block
until some event occurs. A thread can wait on a condition variable, to be woken up when some other thread notifies the condition.
A condition variable is associated with a mutex. For proper operation, the mutex must be acquired, and then a waiting thread must wait on the condition variable.
(defvar mutex (make-mutex "my mutex")) (defvar cond-var (make-condition-variable mutex "my condition variable")) (defun my-thread1 () (with-mutex mutex (condition-wait cond-var)) (sleep-for 10)) (defun my-thread2 () (sleep-for 10) (with-mutex mutex (condition-notify cond-var)) (sleep-for 5)) (make-thread #'my-thread1 "my thread 1") (make-thread #'my-thread2 "my thread 2")
Read the Elisp manual at elisp#Condition Variables
4 I/O
4.1 Read or write to a given file descriptor in Emacs can be
done to only one thread at any time.
4.2 For processes, there are the functions process-thread
and set-process-thread
, which assign process related
file descriptors to a given thread. If another thread
shall take over control, this must be requested
explicitly.
4.3 For all other file descriptors, there exists an
implementation to assign them to a given thread on-the-fly. This does not work well (see bug#25214 and bug#32426), as a consequence keyboard input shall be restricted to the main thread as-of-today.
We're working on this.
4.4 A further problem is how to make it obvious to a user
where keyboard input goes to. Imagine the possible scenario to copy a file, and to remove another file in parallel. Both operations require user confirmation ("Overwrite file a/b?" "Remove file c/d?"), and it must be obvious for which question the answer "y" is intended for. See discussion in the emacs-devel@gnu.org mailing list http://lists.gnu.org/archive/html/emacs-devel/2018-08/msg00456.html
5 Tramp Adaptions for Being thread-safe
5.1 Tramp is just a library of basic file operations,
replacing default file operations for files located on remote hosts.
It does not create any thread on its own. Whether concurrent operations do run, is decided by the user.
Changes to make Tramp thread-safe were surprisingly simple.
5.2 Tramp creates a mutex for every connection to a remote
host. That means, operations for a given connection are
run sequentially (see tramp-file-name-handler
).
5.3 Whenever a Tramp function is invoked, the connection
process is locked to the current thread. This gives a kind of dynamics in locking the process, but it is safe due to the mutex.
5.4 Superfluous save-excursion
calls have been removed.
They made concurrent editing impossible (flipping cursor).
5.5 Minor changes like compatibility functions and thread
information in the debug buffer.
6 Asynchronous File Operations
6.1 Shall be triggered by the user. In order to do this,
there is a new prefix command universal-async-argument
({{{C-x & …}}}), like the prefix argument universal-argument
({{{C-u …}}}) for interactive commands. This is not only
for remote files (Tramp).
6.2 In the future, this prefix command is not restricted to
asynchronous file operations. Any interactive command, which could run also asynchronously, shall use this as user indication.
It is up to the command to decide, what asynchronous
means. For example, gnus
would rather retrieve
articles from the respective servers, and not care about
file operations.
6.3 Implemented so far for the file visiting family of commands:
find-file
{{{C-x & C-x C-f}}}
find-file-other-window
{{{C-x & C-x 4 f}}}
find-file-other-frame
{{{C-x & C-x 5 f}}}
find-file-existing
{{{C-x & M-x find-file-existing}}}
find-file-read-only
{{{C-x & C-x C-r}}}
find-file-read-only-other-window
{{{C-x & C-x 4 r}}}
find-file-read-only-other-frame
{{{C-x & C-x 5 f}}}
find-alternate-file
{{{C-x & C-x C-v}}}
find-alternate-file-other-window
{{{C-x & M-x find-alternate-file-other-window}}}
find-file-literally
{{{C-x & M-x find-file-literally}}}
6.4 Visiting local files would profit only for veeeery large
files (some hundred of MB). Natural candidates are remote files to be visited, and local files to be visited with wildcard.
However, visiting local files has not been adapted yet to
throw sufficient thread-yield
calls. The main thread
keeps blocked. Therefore, remote file operations remain
best candidates as of today.
;; (find-file "/sftp::/var/log/ConsoleKit/history.1" nil t) ;; (find-file "/gdrive:michael.rd.albinus@gmail.com:20180825_091134.jpg" nil t) (find-file "/nextcloud:albinus@ford#8081:20180825_091134.jpg" nil t)
(find-file "/sftp::~/src/emacs/lisp/net/tramp*.el" t t)
(dolist (thread (all-threads)) (unless (eq thread main-thread) (thread-signal thread 'quit nil))) (dolist (buffer (buffer-list)) (when (string-match "^tramp" (buffer-name buffer)) (kill-buffer buffer)))
vc-refresh-state
recomputes the VC state of a file. In
the Tramp case, it often takes more time to compute it
for git, than just loading the file into a buffer.
Therefore, this function gets also an own thread. It
needs to be tuned to throw proper thread-yield
calls.
6.5 An alternative approach to use asynchronous file operations
is user option execute-file-commands-asynchronously
.
If this variable is non-nil, a file will be visited
asynchronously when called interactively. If it is a
regular expression, it must match the file name to be
visited.
It toggles the behavior of ({{{C-x & …}}}).
(setq execute-file-commands-asynchronously tramp-file-name-regexp) (find-file "/sftp::~/src/emacs/lisp/net/tramp*.el" t execute-file-commands-asynchronously)
7 Outlook
7.1 Solve the I/O problem. First for input (keyboard), but
also for arriving events (D-Bus, file notifications, …)
7.2 Improve performance. Apply more fine-tuned thread-yield
calls. Add thread priority, especially with high priority for the main thread.
7.3 Implement thread support for further file operations,
like save-buffer
, copy-file
, rename-file
, …
7.4 Make file operations for local files thread-aware.
7.5 Make vc-refresh-state
thread-aware (call thried-yield
).
7.6 Add indication for running asynchronous file operations
in the background (modeline?).