Am Sa., 29. Okt. 2022 um 23:52 Uhr schrieb John Cowan <xxxxxx@ccil.org>: > > > > On Sat, Oct 29, 2022 at 12:31 PM Marc Nieper-Wißkirchen <xxxxxx@gmail.com> wrote: > >> Thanks to SRFI 18's notion of abandoned mutexes, you can arrange it so >> that data structures won't be in an undefined condition. This is, in >> principle, no harder than arranging that data structures can be shared >> between two threads. > > > > Per contra. Consider a simple double-entry bookkeeping system in which an amount is atomically subtracted from one account A and added to another account B. Suppose that we use a single global mutex to eliminate issues around deadlock. Then if a thread is destroyed while holding the mutex, it is possible to tell that the account balances are wrong, but not *how* they are wrong: the amount may have been subtracted from A but not added to B or vice versa, or both, or neither. It is unsatisfactory to simply give up in these circumstances. You just have to add a state flag and two shadow cells A' and B'. The state is initially cleared. The thread locks the mutex, copies A to A', and B to B', sets the state, "transfers" the amount from A to B, clears the state, and unlocks the mutex. The invariant is that whenever the state is set, A' and B' hold the old values. So if another thread detects an abandoned mutex, it locks it. If the state flag is set, the thread copies A' and B' into A and B, clears the state flag, and unlocks the mutex. Otherwise, it just unlocks the mutex. A real-world system like a distributed database must also cope with the issues. If exceptions coming from outside can be raised in the mutator thread, you will also have to code similarly. You can have an exception handler around your atomic transfer code, but the exception handler would have to do the same as I sketched. And the exception handler itself could be interrupted at any time. > >> In the current semantic model of Scheme (and of SRFI 18 bar >> extensions), exceptions are synchronously raised in the sense that an >> exception is only raised during specific evaluations, and which >> exceptions are raised is documented in the various specifications. >> Concurrent programming is not impossible in this model. > > > Not impossible, just incomplete in practical terms. In multi-threaded application code, I have never used signal handlers to send signals to other threads. The problem is that these signals can be delivered at any time (e.g. during a money transfer) and not at an application-specific safe point. So all you can do is to have the signal handler (which can't do much in POSIX anyway) set a flag and let the thread code poll this flag whenever it has arrived at a safe point. But then you don't need the signal handler anymore; you can just have the flag set by the signaling thread. POSIX/C/C++'s (I don't know about Java) asynchronous signals are far more limited than what the possibility of raising exceptions at any time in Scheme would amount to. And C and C++ are supposed to be practical programming languages suitable for multi-threading programming. (But, of course, we want Scheme to be more practical, so this is no argument for not adding "asynchronous exceptions"). >> So to implement what you have in mind, the semantic model would have >> to be extended so that every evaluation step can, in principle, cause >> an arbitrary exception to be raised. > > > Of course the size of a step does not have to be allowed to be arbitrarily small. With cooperative cancellation, cancellation happens just when the per-thread cancellation flag is polled. Are you talking about the scheme evaluator's implementation or the (high-level) Scheme program itself? >> I would like to be able to kill a thread that loops forever. > > > I would like it too, but the cost of allowing a thread to be killed at an arbitrary point is too high to pay. Thread-exit! doesn't have to kill a thread "instantly" (whatever this means), just eventually. Only at synchronization points, the temporal order can be observed (and has to be correct). ~~~~ Let me sketch the proposals that are floating around (and correct me if I have misunderstood a thing). Then we can refine them so that we get the details right and so that there is no danger of talking past each other: (1) A procedure (thread-exit! THREAD). This works like SRFI 18's `thread-terminate!' except that it returns immediately. The abnormal termination of a thread happens asynchronously as long as the observable sequence of events remains consistent. SRFI 18's concept of abandoned mutexes is used to prevent critical data structures from entering an undefined state. (2) A procedure like "(thread-raise! THREAD CONDITION)". This procedure causes the evaluator of THREAD to raise a non-continuable exception with condition object CONDITION at some implementation-defined safe point (subject to the same sequential constraints as in (1)). This means that, in principle, any evaluation step may proceed with running the code of the current exception handler. (3) A procedure like (thread-signal! THREAD SIGNAL [TIMEOUT]). This procedure causes the evaluator of THREAD to pause its current evaluation and to invoke THREAD's *signal handler* (a new concept, similar to an exception handler) in the dynamic environment where the current evaluation is paused. When the signal handler returns, its results are delivered to the caller of `thread-signal!` and the paused evaluation of THREAD is continued. One needs an accompanying procedure like `with-signal-handler'. (4) A procedure like (thread-interrupt! THREAD THUNK [TIMEOUT]). This procedure causes the evaluator of THREAD to pause its current evaluation. It then invokes THUNK in the dynamic environment where the current evaluation is paused. When the THUNK returns, its results are delivered to the caller of `thread-interrupt!' and the paused evaluation of THREAD is continued. (5) A procedure like (current-thread-exit!) that is like "thread-exit!" but only for the current thread.