buffer: Added `ThreadPool<"scope = "static>` skeleton. Old design removed, redesign using `mpsc` (`crossbeam::channel`) instead of bare-bones channel impl we were using with `Arc<(Condvar, Mutex<...>)>` (See comment in `buffer::thread_pool` about previous impl"s shortcomings and needless complexity.)
Fortune for reverse's current commit: Curse − 凶refactor
parent
0ce496cafd
commit
bcaa9e0703
@ -0,0 +1,356 @@
|
||||
//! Buffering and resource pooling utilities
|
||||
//!
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
pub use bytes::{
|
||||
Buf, BufMut,
|
||||
Bytes, BytesMut,
|
||||
};
|
||||
use std::{
|
||||
fmt, error,
|
||||
sync::Arc,
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
|
||||
#[cfg(feature="_loan")]
|
||||
const _TODO_REWORK_THIS_INTERFACE_ITS_OVERCOMPLICATED_SHITE: () = {
|
||||
type OwnedResult<S, T, E> = Result<T, (E, S)>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum MaybeRef<'r, T>
|
||||
{
|
||||
Owned(T),
|
||||
Borrowed(&'r T),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Loan<'owner, O: Loaner>
|
||||
{
|
||||
owner: MaybeRef<'owner, O>,
|
||||
item: O::Item<'owner>,
|
||||
}
|
||||
|
||||
impl<'o, O> Loaned<'o, O> for Loan<'o, O>
|
||||
where O: Loaner
|
||||
{
|
||||
fn try_release(self) -> Result<(), <O as Loaner>::Error> {
|
||||
self.item.try_release()
|
||||
}
|
||||
fn try_release_owned(self: Arc<Self>) -> OwnedResult<Arc<Self>, (), <O as Loaner>::Error> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Loaned<'owner, Owner: ?Sized + 'owner>: Sized
|
||||
where Owner: Loaner
|
||||
{
|
||||
//fn owner(&self) -> Option<&Owner>;
|
||||
|
||||
fn try_release(self) -> Result<(), Owner::Error>;
|
||||
fn try_release_owned(self: Arc<Self>) -> OwnedResult<Arc<Self>, (), Owner::Error>
|
||||
where Self: 'owner; // XXX: What? 'owner or 'static??? I think it's `'owner` == `'static` but how tf do I write that?
|
||||
}
|
||||
|
||||
pub trait Loaner {
|
||||
type Item<'owner>: Loaned<'owner, Self>
|
||||
where Self: 'owner;
|
||||
type Error: error::Error + Send + 'static;
|
||||
|
||||
fn try_loan(&self) -> Result<Self::Item<'_>, Self::Error>;
|
||||
fn try_loan_owned(self: Arc<Self>) -> OwnedResult<Arc<Self>, Self::Item<'static>, Self::Error>
|
||||
where Self: 'static;
|
||||
|
||||
fn try_release(&self, item: Self::Item<'_>) -> OwnedResult<Self::Item<'_>, (), Self::Error>;
|
||||
}
|
||||
|
||||
compile_error!("This `trait Loan` design isn't going to fucking work ofc...")
|
||||
};
|
||||
|
||||
/// Thread pooling
|
||||
#[cfg(feature="threads")]
|
||||
pub mod thread_pool {
|
||||
use super::*;
|
||||
|
||||
use std::{
|
||||
mem::{
|
||||
self,
|
||||
ManuallyDrop,
|
||||
},
|
||||
thread::{
|
||||
self,
|
||||
Thread, ThreadId,
|
||||
},
|
||||
ops::Drop,
|
||||
sync::{Arc, Weak,},
|
||||
|
||||
};
|
||||
use parking_lot::{
|
||||
Condvar,
|
||||
Mutex,
|
||||
};
|
||||
use crossbeam::{
|
||||
queue::ArrayQueue,
|
||||
channel,
|
||||
};
|
||||
|
||||
//TODO: Implement `ThreadPool<'scope = 'static>` like below, but instead use `crossbeam::channel` mpsc to send commands to unparked threads (i.e. loaned threads from the pool that have an active user handle.)
|
||||
// The design will be far simpler, since the Arc/Weak, send+recv stuff will be handled fine by `channel` on both ends. The only thing we need dtors for is `force_push()`ing threads back into their pool's ringbuffer, and for force-unparking threads from a pool that are removed (same as loan, except sender should be dropped before unpark happens.)
|
||||
|
||||
#[cfg(feature="_thread-pool-no-mpsc")]
|
||||
const _: () = {
|
||||
#[derive(Debug)]
|
||||
pub enum ThreadHandleKind<'scope, T>
|
||||
{
|
||||
Detached(Option<ThreadId>),
|
||||
Attached(thread::JoinHandle<T>),
|
||||
Scoped(thread::ScopedJoinHandle<'scope, T>),
|
||||
}
|
||||
|
||||
impl<'scope, T> ThreadHandleKind<'scope, T>
|
||||
{
|
||||
/// Get a reference to the attached thread (if there is one.)
|
||||
#[inline]
|
||||
pub fn thread(&self) -> Option<&Thread>
|
||||
{
|
||||
Some(match self {
|
||||
Self::Scoped(s) => s.thread(),
|
||||
Self::Attached(s) => s.thread(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Is this handle attached to a thread?
|
||||
///
|
||||
/// If it is *attached*, this means the thread *should not* outlive the instance (unless the instance is `'static`.)
|
||||
#[inline]
|
||||
pub const fn is_attached(&self) -> bool
|
||||
{
|
||||
match self {
|
||||
Self::Attached(_) |
|
||||
Self::Scoped(_) => true,
|
||||
Self::Detached(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this handle no longer referring to *any* thread?
|
||||
///
|
||||
/// In a *poisoned* state, the handle is useless.
|
||||
#[inline]
|
||||
pub const fn is_poisoned(&self) -> bool
|
||||
{
|
||||
match self {
|
||||
Self::Detached(None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the thread id of the handle, if there is a referred to thread.
|
||||
#[inline]
|
||||
pub fn id(&self) -> Option<ThreadId>
|
||||
{
|
||||
Some(match self {
|
||||
Self::Scoped(x) => x.thread().id(),
|
||||
Self::Attached(x) => x.thread().id(),
|
||||
Self::Detached(Some(id)) => *id,
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a raw thread handle
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct ThreadHandle<'scope, T>(ThreadHandleKind<'scope, T>);
|
||||
|
||||
//TODO: forward `ThreadHandle<'s, T>` methods to `ThreadHandleKind<'s, T>`: `thread(), id(), is_attached()`. Add methods `is_running(), join(), try_join(),` etc...
|
||||
|
||||
impl<'s, T> Drop for ThreadHandle<'s, T>
|
||||
{
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
match mem::replace(&mut self.0, ThreadHandleKind::Detached(None)) {
|
||||
ThreadHandleKind::Attached(a) => drop(a.join()), // Attempt join, ignore panic.
|
||||
ThreadHandleKind::Scoped(s) => drop(s.join().unwrap_or_resume()), // Attempt join, carry panic back through into scope.
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PooledSendContext<T: Send, F: ?Sized + Send>
|
||||
{
|
||||
/// An execute directive `Err(Some(func))` is taken by the thread and replaced by `Ok(func())`.
|
||||
///
|
||||
/// # Direction
|
||||
/// Bidirectional.
|
||||
/// (Handle -> Thread; Thread -> Handle)
|
||||
Execute(Result<T, Option<Box<F>>>),
|
||||
/// Pass a caught panic within an `Execute(Err(Some(F)))` payload back from the thread.
|
||||
///
|
||||
/// # Direction
|
||||
/// Thread -> Handle
|
||||
Panic(UnwindPayload),
|
||||
/// When the loaned `PooledThread` is moved back into the pool, tell the thread to park itself again.
|
||||
///
|
||||
/// # Direction
|
||||
/// Handle -> Thread
|
||||
Park,
|
||||
|
||||
/// The default invariant used for when an operation has been taken by a thread.
|
||||
///
|
||||
/// # When `Taken`.
|
||||
/// The replacement is always `Taken` after a `take(&mut self)` that contains a value.
|
||||
/// The `Park` directive will not be replaced to `Taken`.
|
||||
Taken,
|
||||
|
||||
//NOTE: `Close` variant not needed because `handle` will have a `Weak` reference to `callback_passage`; when the upgrade fails, the thread will exit.
|
||||
}
|
||||
|
||||
impl<T: Send, F: ?Sized + Send> PooledSendContext<T, F>
|
||||
{
|
||||
pub fn take(&mut self) -> Option<Self>
|
||||
{
|
||||
Some(match self {
|
||||
//XXX: I don't think not replacing cloneable variants is a good or useful default. (see below V)
|
||||
//// Clone non-data-holding variants.
|
||||
//Self::Park => Self::Park,
|
||||
//// If the ctx has already been taken, return `None`.
|
||||
//Self::Taken => return None,
|
||||
// Move out of data-holding variants.
|
||||
_ => mem::replace(self, Self::Taken)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A loanable thread that belongs to a pool.
|
||||
///
|
||||
/// # Pooling
|
||||
/// When taken from a `ThreadPool`, the object is *moved* out of the thread-pool's ring-buffer and then pinned in return (See `PooledThread`.)
|
||||
///
|
||||
/// To return the thread to the pool, the `callback_passage` writes a `Park` command to the thread, unpins itself, and then on drop moves itself back into the start of the ring-buffer of the thread pool `owner`
|
||||
#[derive(Debug)]
|
||||
struct PooledThreadHandle<'scope, T, F: ?Sized = dyn FnOnce() -> T + Send + 'scope>
|
||||
where F: FnOnce() -> T + Send + 'scope,
|
||||
T: Send + 'scope
|
||||
{
|
||||
|
||||
/// Passes `F` to `handle` and retrieves `T`.
|
||||
callback_passage: Arc<(Condvar, Mutex<PooledSendContext<T, F>>)>,
|
||||
/// The backing thread, which when started parks itself.
|
||||
/// The `PooledThread` object will unpark the thread when the object is loaned (moved) out of the pool, and re-parked when enter
|
||||
///
|
||||
/// Will wait on `callback_passage?.0` for `Execute` directives, pass them back to the handle, and when a `Park` directive is recieved (i.e. when moved back into the pool) the thread will park itself and wait to be unparked when moved out of the pool or closed again.
|
||||
///
|
||||
/// If `callback_message` is dropped, the thread will exit when unparked (dropped from pool.)
|
||||
//TODO: Ensure unpark on drop?
|
||||
handle: ThreadHandle<'scope, Result<(), ()>>,
|
||||
}
|
||||
|
||||
impl<'scope, T, F:?Sized> PooledThreadHandle<'scope, T, F>
|
||||
where F: FnOnce() -> T + Send + 'scope,
|
||||
T: Send + 'scope
|
||||
{
|
||||
fn send_to_thread_raw(&self, message: PooledSendContext<T, F>) -> bool
|
||||
{
|
||||
let mut ctx = self.callback_passage.1.lock();
|
||||
*ctx = message;
|
||||
self.callback_passage.0.notify_one()
|
||||
}
|
||||
fn send_from_thread_raw(&self, message: PooledSendContext<T, F>)
|
||||
{
|
||||
let mut ctx = self.callback_passage.1.lock();
|
||||
*ctx = message;
|
||||
}
|
||||
|
||||
fn read_in_thread_raw(&self, wait_for: bool) -> Option<PooledSendContext<T, F>>
|
||||
{
|
||||
let mut ctx = self.callback_passage.1.lock(); // NOTE: Should we fair lock here?
|
||||
// Wait on condvar if `wait_for` is true.
|
||||
if wait_for {
|
||||
self.callback_passage.0.wait(&mut ctx);
|
||||
}
|
||||
// Then replace the ctx with `Taken`.
|
||||
ctx.take()
|
||||
}
|
||||
fn try_read_from_thread_raw(&self) -> Option<PooledSendContext<T, F>>
|
||||
{
|
||||
let mut ctx = self.callback_passage.1.try_lock()?;
|
||||
ctx.take()
|
||||
}
|
||||
fn read_from_thread_raw(&self) -> Option<PooledSendContext<T, F>>
|
||||
{
|
||||
let mut ctx = self.callback_passage.1.lock();
|
||||
ctx.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PooledThread<'scope, 'pool: 'scope, T, F: ?Sized = dyn FnOnce() -> T + Send + 'scope>
|
||||
where F: FnOnce() -> T + Send + 'scope,
|
||||
T: Send + 'scope
|
||||
{
|
||||
/// The owner to return `thread` to on drop.
|
||||
owner: Option<&'pool ThreadPool<'scope, T, F>>,
|
||||
/// The pinned thread handle that is returned on drop.
|
||||
thread: ManuallyDrop<PooledThreadHandle<'scope, T, F>>, //XXX: Pin<> needed (or even useful at all) here?
|
||||
}
|
||||
|
||||
impl<'scope, 'pool: 'scope, T, F:?Sized> Drop for PooledThread<'scope, 'pool, T, F>
|
||||
where F: FnOnce() -> T + Send + 'scope,
|
||||
T: Send + 'scope
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
let Some(loaned_from) = self.owner else {
|
||||
// There is no owner.
|
||||
// Instead, unpark the thread after drop.
|
||||
let PooledThreadHandle { callback_passage, handle } = unsafe { ManuallyDrop::take(&mut self.thread) };
|
||||
|
||||
// Drop the Arc, so when `thread` wakes from unpark, it will fail to acquire context.
|
||||
drop(callback_passage);
|
||||
|
||||
if let Some(thread) = handle.0.thread() {
|
||||
thread.unpark();
|
||||
}
|
||||
|
||||
return todo!("How do we know *when* to unpark? How can we unpark *after* dropping the Arc?");
|
||||
};
|
||||
|
||||
// There is an owner. Tell the thread to park itself.
|
||||
let mut handle = unsafe { ManuallyDrop::take(&mut self.thread) };
|
||||
handle.send_to_thread_raw(PooledSendContext::Park);
|
||||
|
||||
// Read the message object, take it from the thread after it notifies us it is about to park.
|
||||
let ctx = {
|
||||
let mut ctx = handle.callback_passage.1.lock();
|
||||
// Before thread parks, it should signal condvar, and callback_passage should not be unique again yet.
|
||||
handle.callback_passage.0.wait_while(&mut ctx, |_| Arc::strong_count(&handle.callback_passage) > 1);
|
||||
ctx.take()
|
||||
};
|
||||
|
||||
// Force insert back into thred pool.
|
||||
if let Some( PooledThreadHandle { callback_passage, handle }) = loaned_from.pool_queue.force_push(handle) {
|
||||
// If one is removed, drop the context..
|
||||
drop(callback_passage);
|
||||
// Then force-unpark the thread.
|
||||
match handle.0.thread() {
|
||||
Some(thread) => thread.unpark(),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
todo!("Should we force `join()` here? Or allow non-scoped handles to be forgotten?")
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct ThreadPool<'scope, T, F: ?Sized = dyn FnOnce() -> T + Send + 'scope>
|
||||
where F: FnOnce() -> T + Send + 'scope,
|
||||
T: Send + 'scope
|
||||
{
|
||||
pool_queue: ArrayQueue<PooledThreadHandle<'scope, T, F>>,
|
||||
}
|
||||
};
|
||||
|
||||
//TODO: Build a ThreadPool<'scope = 'static> using crossbeam::ArrayQueue<...> (XXX: See phone for details on how pool should use un/parking for loaning; and CondVar for passing tasks.) Also, add a `Loan` trait with assoc type `Item<'owner>` and methods `try_loan(&self) -> Result<Self::Item<'_>, ...>`, `return(&self, item: Self::Item<'_>) -> Result<(), ...>` and `try_loan_owned(self: Arc<Self>) -> `MovedResult<Arc<Self>, OwnedLoan<'static, Self>, ...>` (struct OwnedLoad<'scope, L>{ item: Option<L::Item<'scope>> } )`, etc.
|
||||
}
|
Loading…
Reference in new issue