From ef93c6a2a8b25efff85b6d4eef48811f569375ee Mon Sep 17 00:00:00 2001
From: mihacooper
Date: Mon, 5 Dec 2022 22:09:39 +0100
Subject: [PATCH] Rework thread cancellation, using regular exception (#177)
BREAKS BACK COMPATIBILITY:
- cancellation error can be caught by `pcall`
- `canceled` thread status was renamed to `cancelled`
---
README.md | 83 ++++++++-
src/cpp/channel.cpp | 4 +-
src/cpp/lua-module.cpp | 7 +-
src/cpp/notifier.h | 10 +-
src/cpp/stored-object.cpp | 4 +-
src/cpp/this-thread.cpp | 83 +++++++++
src/cpp/{this_thread.h => this-thread.h} | 4 +-
src/cpp/thread-handle.cpp | 82 +++++++++
src/cpp/{threading.h => thread-handle.h} | 63 +++----
.../{thread_runner.cpp => thread-runner.cpp} | 2 +-
src/cpp/{thread_runner.h => thread-runner.h} | 2 +-
src/cpp/{threading.cpp => thread.cpp} | 166 +++---------------
src/cpp/thread.h | 43 +++++
src/cpp/utils.h | 4 +-
tests/lua/thread-interrupt.lua | 2 +-
tests/lua/thread-stress.lua | 2 +-
tests/lua/thread.lua | 143 ++++++++++++++-
17 files changed, 498 insertions(+), 206 deletions(-)
create mode 100644 src/cpp/this-thread.cpp
rename src/cpp/{this_thread.h => this-thread.h} (88%)
create mode 100644 src/cpp/thread-handle.cpp
rename src/cpp/{threading.h => thread-handle.h} (63%)
rename src/cpp/{thread_runner.cpp => thread-runner.cpp} (96%)
rename src/cpp/{thread_runner.h => thread-runner.h} (95%)
rename src/cpp/{threading.cpp => thread.cpp} (58%)
create mode 100644 src/cpp/thread.h
diff --git a/README.md b/README.md
index ae8b1c2..8dd6e8a 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Requires C++14 compiler compliance. Tested with GCC 4.9+, clang 3.8 and Visual S
* [Important notes](#important-notes)
* [Blocking and nonblocking operations](#blocking-and-nonblocking-operations)
* [Function's upvalues](#functions-upvalues)
+* [Thread cancellation and pausing](#thread-cancellation-and-pausing)
* [API Reference](#api-reference)
* [Thread](#thread)
* [effil.thread()](#runner--effilthreadfunc)
@@ -45,6 +46,7 @@ Requires C++14 compiler compliance. Tested with GCC 4.9+, clang 3.8 and Visual S
* [effil.yield()](#effilyield)
* [effil.sleep()](#effilsleeptime-metric)
* [effil.hardware_threads()](#effilhardware_threads)
+ * [effil.pcall()](#status---effilpcallfunc)
* [Table](#table)
* [effil.table()](#table--effiltabletbl)
* [__newindex: table[key] = value](#tablekey--value)
@@ -71,6 +73,7 @@ Requires C++14 compiler compliance. Tested with GCC 4.9+, clang 3.8 and Visual S
* [effil.size()](#size--effilsizeobj)
* [effil.type()](#effiltype)
+
# How to install
### Build from src on Linux and Mac
1. `git clone --recursive https://github.com/effil/effil effil`
@@ -256,7 +259,7 @@ local worker = effil.thread(function()
effil.sleep(999) -- worker will hang for 999 seconds
end)()
-worker:cancel(1) -- returns true, cause blocking operation was interrupted and thread was canceled
+worker:cancel(1) -- returns true, cause blocking operation was interrupted and thread was cancelled
```
@@ -269,10 +272,71 @@ Working with function Effil can store function environment (`_ENV`) as well. Con
* *Lua = 5.1*: function environment is not stored at all (due to limitations of lua_setfenv we cannot use userdata)
* *Lua > 5.1*: Effil serialize and store function environment only if it's not equal to global environment (`_ENV ~= _G`).
+## Thread cancellation and pausing
+The [`effil.thread`](#runner--effilthreadfunc) can be paused and cancelled using corresponding methods of thread object [`thread:cancel()`](#threadcanceltime-metric) and [`thread:pause()`](#threadpausetime-metric).
+Thread that you try to interrupt can be interrupted in two execution points: explicit and implicit.
+ - Explicit points are [`effil.yield()`](#effilyield)
+
+ Example of explicit interruption point
+
+
+ ```lua
+ local thread = effil.thread(function()
+ while true do
+ effil.yield()
+ end
+ -- will never reach this line
+ end)()
+ thread:cancel()
+ ```
+
+
+
+ - Implicit points are lua debug hook invocation which is set using [lua_sethook](https://www.lua.org/manual/5.3/manual.html#lua_sethook) with LUA_MASKCOUNT.
+ Implicit points are optional and enabled only if [thread_runner.step](#runnerstep) > 0.
+
+ Example of implicit interruption point
+
+
+ ```lua
+ local thread_runner = effil.thread(function()
+ while true do
+ end
+ -- will never reach this line
+ end)
+ thread_runner.step = 10
+ thread = thread_runner()
+ thread:cancel()
+ ```
+
+
+
+ - Additionally thread can be cancelled (but not paused) in any [blocking or non-blocking waiting operation](#blocking-and-nonblocking-operations).
+
+ Example
+
+
+ ```lua
+ local channel = effil.channel()
+ local thread = effil.thread(function()
+ channel:pop() -- thread hangs waiting infinitely
+ -- will never reach this line
+ end)()
+ thread:cancel()
+ ```
+
+
+
+
+ **How does cancellation works?**
+ When you cancel thread it generates lua [`error`](https://lua.org.ru/manual_ru.html#pdf-error) with message `"Effil: thread is cancelled"` when it reaches any interruption point. It means that you can catch this error using [`pcall`](https://lua.org.ru/manual_ru.html#pdf-pcall) but thread will generate new error on next interruption point.
+ If you want to catch your own error but pass cancellation error you can use [effil.pcall()](#status---effilpcallfunc).
+ Status of cancelled thread will be equal to `cancelled` only if it finished with cancellation error. It means that if you catch cancellation error thread may finished with `completed` status or `failed` status if there will be some another error.
+
# API Reference
## Thread
-`effil.thread` is the way to create a thread. Threads can be stopped, paused, resumed and canceled.
+`effil.thread` is the way to create a thread. Threads can be stopped, paused, resumed and cancelled.
All operation with threads can be synchronous (with optional timeout) or asynchronous.
Each thread runs with its own lua state.
@@ -309,7 +373,7 @@ Thread handle provides API for interaction with thread.
Returns thread status.
**output**:
-- `status` - string values describes status of thread. Possible values are: `"running", "paused", "canceled", "completed" and "failed"`.
+- `status` - string values describes status of thread. Possible values are: `"running", "paused", "cancelled", "completed" and "failed"`.
- `err` - error message, if any. This value is specified only if thread status == `"failed"`.
- `stacktrace` - stacktrace of failed thread. This value is specified only if thread status == `"failed"`.
@@ -360,9 +424,20 @@ Suspend current thread.
### `effil.hardware_threads()`
Returns the number of concurrent threads supported by implementation.
-Basically forwards value from [std::thread::hardware_concurrency](https://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency).
+Basically forwards value from [std::thread::hardware_concurrency](https://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency).
**output**: number of concurrent hardware threads.
+### `status, ... = effil.pcall(func, ...)`
+Works exactly the same way as standard [pcall](https://www.lua.org/manual/5.3/manual.html#pdf-pcall) except that it will not catch thread cancellation error caused by [thread:cancel()](#threadcanceltime-metric) call.
+
+**input:**
+ - func - function to call
+ - ... - arguments to pass to functions
+
+**output:**
+ - status - `true` if no error occurred, `false` otherwise
+ - ... - in case of error return one additional result with message of error, otherwise return function call results
+
## Table
`effil.table` is a way to exchange data between effil threads. It behaves almost like standard lua tables.
All operations with shared table are thread safe. **Shared table stores** primitive types (number, boolean, string), function, table, light userdata and effil based userdata. **Shared table doesn't store** lua threads (coroutines) or arbitrary userdata. See examples of shared table usage [here](#examples)
diff --git a/src/cpp/channel.cpp b/src/cpp/channel.cpp
index ce23742..781b6fb 100644
--- a/src/cpp/channel.cpp
+++ b/src/cpp/channel.cpp
@@ -52,7 +52,7 @@ bool Channel::push(const sol::variadic_args& args) {
StoredArray Channel::pop(const sol::optional& duration,
const sol::optional& period) {
- this_thread::interruptionPoint();
+ this_thread::cancellationPoint();
std::unique_lock lock(ctx_->lock_);
{
this_thread::ScopedSetInterruptable interruptable(this);
@@ -70,7 +70,7 @@ StoredArray Channel::pop(const sol::optional& duration,
else { // No time limit
ctx_->cv_.wait(lock);
}
- this_thread::interruptionPoint();
+ this_thread::cancellationPoint();
}
}
diff --git a/src/cpp/lua-module.cpp b/src/cpp/lua-module.cpp
index a8fcf9d..2b3064f 100644
--- a/src/cpp/lua-module.cpp
+++ b/src/cpp/lua-module.cpp
@@ -1,8 +1,9 @@
-#include "threading.h"
+#include "thread.h"
+#include "this-thread.h"
+#include "thread-runner.h"
#include "shared-table.h"
#include "garbage-collector.h"
#include "channel.h"
-#include "thread_runner.h"
#include
@@ -100,6 +101,7 @@ int luaopen_effil(lua_State* L) {
"thread_id", this_thread::threadId,
"sleep", this_thread::sleep,
"yield", this_thread::yield,
+ "pcall", this_thread::pcall,
"table", createTable,
"rawset", SharedTable::luaRawSet,
"rawget", SharedTable::luaRawGet,
@@ -115,7 +117,6 @@ int luaopen_effil(lua_State* L) {
"hardware_threads", std::thread::hardware_concurrency,
sol::meta_function::index, luaIndex
);
-
sol::stack::push(lua, type);
sol::stack::pop(lua);
sol::stack::push(lua, EffilApiMarker());
diff --git a/src/cpp/notifier.h b/src/cpp/notifier.h
index 2fa768b..046e5cb 100644
--- a/src/cpp/notifier.h
+++ b/src/cpp/notifier.h
@@ -1,6 +1,6 @@
#pragma once
-#include
+#include
#include
#include
@@ -29,20 +29,20 @@ public:
}
void wait() {
- this_thread::interruptionPoint();
+ this_thread::cancellationPoint();
this_thread::ScopedSetInterruptable interruptable(this);
std::unique_lock lock(mutex_);
while (!notified_) {
cv_.wait(lock);
- this_thread::interruptionPoint();
+ this_thread::cancellationPoint();
}
}
template
bool waitFor(T period) {
- this_thread::interruptionPoint();
+ this_thread::cancellationPoint();
if (period == std::chrono::seconds(0) || notified_)
return notified_;
@@ -54,7 +54,7 @@ public:
while (!timer.isFinished() &&
cv_.wait_for(lock, timer.left()) != std::cv_status::timeout &&
!notified_) {
- this_thread::interruptionPoint();
+ this_thread::cancellationPoint();
}
return notified_;
}
diff --git a/src/cpp/stored-object.cpp b/src/cpp/stored-object.cpp
index c8a4a0d..bd9c7a4 100644
--- a/src/cpp/stored-object.cpp
+++ b/src/cpp/stored-object.cpp
@@ -1,10 +1,10 @@
#include "stored-object.h"
#include "channel.h"
-#include "threading.h"
+#include "thread.h"
#include "shared-table.h"
#include "function.h"
#include "utils.h"
-#include "thread_runner.h"
+#include "thread-runner.h"
#include