ThreadController interface extracted from Thread descriptor
This commit is contained in:
parent
650213fc4c
commit
34debfcb7b
@ -4,11 +4,12 @@ import ru.landgrafhomyak.multitasking_0.fibers.Fiber
|
|||||||
|
|
||||||
@Suppress("EXPECT_ACTUAL_IR_INCOMPATIBILITY")
|
@Suppress("EXPECT_ACTUAL_IR_INCOMPATIBILITY")
|
||||||
public expect class Thread {
|
public expect class Thread {
|
||||||
|
override fun toString(): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of thread for debug purposes.
|
* Name of thread for debug purposes.
|
||||||
*/
|
*/
|
||||||
public val name: String
|
public val name: String
|
||||||
override fun toString(): String
|
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,8 +2,6 @@ package ru.landgrafhomyak.multitasking_0.threads
|
|||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.locks.LockSupport
|
import java.util.concurrent.locks.LockSupport
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import kotlin.concurrent.withLock
|
|
||||||
import java.lang.Thread as jThread
|
import java.lang.Thread as jThread
|
||||||
import ru.landgrafhomyak.multitasking_0.fibers.Fiber
|
import ru.landgrafhomyak.multitasking_0.fibers.Fiber
|
||||||
import ru.landgrafhomyak.multitasking_0.SingleThreadEventLoop
|
import ru.landgrafhomyak.multitasking_0.SingleThreadEventLoop
|
||||||
@ -11,129 +9,21 @@ import ru.landgrafhomyak.multitasking_0.WrongCallerThreadException
|
|||||||
import ru.landgrafhomyak.multitasking_0.threads.sync.ResumeThreadCallback
|
import ru.landgrafhomyak.multitasking_0.threads.sync.ResumeThreadCallback
|
||||||
|
|
||||||
@Suppress("AMBIGUOUS_EXPECTS", "ACTUAL_WITHOUT_EXPECT")
|
@Suppress("AMBIGUOUS_EXPECTS", "ACTUAL_WITHOUT_EXPECT")
|
||||||
public actual sealed class Thread {
|
public actual class Thread(
|
||||||
|
@JvmField
|
||||||
|
internal val _nativeThread: jThread
|
||||||
|
) {
|
||||||
private val _threadLocalMethods = this.ThreadLocalMethodsImpl()
|
private val _threadLocalMethods = this.ThreadLocalMethodsImpl()
|
||||||
|
|
||||||
protected abstract val _nativeThread: jThread
|
actual override fun toString(): String = "<platform native thread name='${this._nativeThread.name}'>"
|
||||||
protected val _sync: ReentrantLock = ReentrantLock()
|
|
||||||
protected var _isExplicitlyDestroyed: Boolean = false
|
|
||||||
protected var _uncaughtException: Throwable? = null
|
|
||||||
|
|
||||||
|
|
||||||
actual abstract override fun toString(): String
|
|
||||||
|
|
||||||
public actual val name: String
|
public actual val name: String
|
||||||
get() {
|
get() = this._nativeThread.name
|
||||||
this._sync.withLock {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
throw IllegalStateException("Thread destroyed")
|
|
||||||
}
|
|
||||||
return this._nativeThread.name
|
|
||||||
}
|
|
||||||
|
|
||||||
public actual val isDaemon: Boolean
|
|
||||||
get() {
|
|
||||||
this._sync.withLock {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
throw IllegalStateException("Thread destroyed")
|
|
||||||
}
|
|
||||||
return this._nativeThread.isDaemon
|
|
||||||
}
|
|
||||||
public actual val state: State
|
|
||||||
get() {
|
|
||||||
this._sync.withLock {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
return State.DESTROYED
|
|
||||||
|
|
||||||
when (this._nativeThread.state) {
|
|
||||||
jThread.State.NEW -> return State.NEW
|
|
||||||
jThread.State.RUNNABLE -> return State.STARTED
|
|
||||||
jThread.State.BLOCKED -> return State.STARTED
|
|
||||||
jThread.State.WAITING -> return State.STARTED
|
|
||||||
jThread.State.TIMED_WAITING -> return State.STARTED
|
|
||||||
jThread.State.TERMINATED -> {
|
|
||||||
if (this._uncaughtException == null)
|
|
||||||
return State.FINISHED_SUCCESSFULLY
|
|
||||||
else
|
|
||||||
return State.FINISHED_WITH_ERROR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public actual val uncaughtException: Throwable
|
|
||||||
get() {
|
|
||||||
this._sync.withLock {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
throw IllegalStateException("Thread destroyed")
|
|
||||||
|
|
||||||
val e = this._uncaughtException
|
|
||||||
if (e != null)
|
|
||||||
return e
|
|
||||||
|
|
||||||
when (this._nativeThread.state) {
|
|
||||||
jThread.State.NEW, jThread.State.RUNNABLE, jThread.State.BLOCKED, jThread.State.WAITING, jThread.State.TIMED_WAITING ->
|
|
||||||
throw IllegalStateException("Thread not finished yet")
|
|
||||||
|
|
||||||
jThread.State.TERMINATED -> throw IllegalStateException("Thread finished without uncaught exceptions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public actual fun start() {
|
|
||||||
this._sync.withLock {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
throw IllegalStateException("Thread already was started, finished and destroyed")
|
|
||||||
// stdlib's errors aren't verbose
|
|
||||||
when (this._nativeThread.state) {
|
|
||||||
jThread.State.NEW -> {}
|
|
||||||
jThread.State.TERMINATED -> throw IllegalStateException("Thread already was started and finished")
|
|
||||||
jThread.State.RUNNABLE, jThread.State.BLOCKED, jThread.State.WAITING, jThread.State.TIMED_WAITING ->
|
|
||||||
throw IllegalStateException("Thread already was started")
|
|
||||||
}
|
|
||||||
|
|
||||||
this._nativeThread.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requires explicit synchronization
|
|
||||||
*/
|
|
||||||
protected fun _assertJoinAllowed() {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
throw IllegalStateException("Can't join on destroyed thread")
|
|
||||||
if (this._nativeThread.state == jThread.State.NEW)
|
|
||||||
throw IllegalStateException("Can't join on thread that isn't started")
|
|
||||||
}
|
|
||||||
|
|
||||||
public actual abstract fun join()
|
|
||||||
|
|
||||||
public actual fun releaseResources() {
|
|
||||||
this._sync.withLock {
|
|
||||||
if (this._isExplicitlyDestroyed)
|
|
||||||
throw IllegalStateException("Thread already destroyed")
|
|
||||||
when (this._nativeThread.state) {
|
|
||||||
jThread.State.NEW -> {}
|
|
||||||
jThread.State.TERMINATED -> {}
|
|
||||||
jThread.State.RUNNABLE, jThread.State.BLOCKED, jThread.State.WAITING, jThread.State.TIMED_WAITING ->
|
|
||||||
throw IllegalStateException("Thread already was started")
|
|
||||||
}
|
|
||||||
this._isExplicitlyDestroyed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public actual enum class State {
|
|
||||||
NEW,
|
|
||||||
STARTED,
|
|
||||||
FINISHED_SUCCESSFULLY,
|
|
||||||
FINISHED_WITH_ERROR,
|
|
||||||
DESTROYED
|
|
||||||
}
|
|
||||||
|
|
||||||
private object CurrentThreadVariable : ThreadLocal<Thread?>() {
|
private object CurrentThreadVariable : ThreadLocal<Thread?>() {
|
||||||
override fun initialValue(): Thread? {
|
override fun initialValue(): Thread? {
|
||||||
return ExternalThread(jThread.currentThread())
|
return Thread(jThread.currentThread())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,8 +36,8 @@ public actual sealed class Thread {
|
|||||||
get() = this._tl_currentThread.get()?._threadLocalMethods ?: throw RuntimeException("This java thread can't be wrapped")
|
get() = this._tl_currentThread.get()?._threadLocalMethods ?: throw RuntimeException("This java thread can't be wrapped")
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
public actual fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): Thread =
|
public actual fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): ThreadController =
|
||||||
DrivenPlatformThread(name, isDaemon, routine)
|
ThreadControllerImpl(name, isDaemon, routine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,132 @@
|
|||||||
|
package ru.landgrafhomyak.multitasking_0.threads
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
import java.lang.Thread as jThread
|
||||||
|
|
||||||
|
@Suppress("JoinDeclarationAndAssignment")
|
||||||
|
public class ThreadControllerImpl : ThreadController {
|
||||||
|
public override val thread: Thread
|
||||||
|
private val _sync: ReentrantLock = ReentrantLock()
|
||||||
|
private var _isExplicitlyDestroyed: Boolean = false
|
||||||
|
private var _uncaughtException: Throwable? = null
|
||||||
|
private val _threadFinishedWaiter: CountDownLatch
|
||||||
|
|
||||||
|
internal constructor(name: String, isDaemon: Boolean, routine: ThreadRoutine) {
|
||||||
|
this._threadFinishedWaiter = CountDownLatch(1)
|
||||||
|
|
||||||
|
this.thread = Thread(jThread.ofPlatform().name(name).daemon(isDaemon).unstarted(this.Kernel(routine)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Kernel(private val _routine: ThreadRoutine) : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
try {
|
||||||
|
Thread._tl_currentThread.set(this@ThreadControllerImpl.thread)
|
||||||
|
this._routine.runThread(this@ThreadControllerImpl.thread)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
this@ThreadControllerImpl._sync.withLock {
|
||||||
|
this@ThreadControllerImpl._uncaughtException = t
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this@ThreadControllerImpl._threadFinishedWaiter.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "<controller of platform native thread '${this.thread._nativeThread.name}'>"
|
||||||
|
|
||||||
|
public override val isDaemon: Boolean
|
||||||
|
get() {
|
||||||
|
this._sync.withLock {
|
||||||
|
if (this._isExplicitlyDestroyed)
|
||||||
|
throw IllegalStateException("Thread destroyed")
|
||||||
|
}
|
||||||
|
return this.thread._nativeThread.isDaemon
|
||||||
|
}
|
||||||
|
|
||||||
|
public override val state: ThreadController.State
|
||||||
|
get() {
|
||||||
|
this._sync.withLock {
|
||||||
|
if (this._isExplicitlyDestroyed)
|
||||||
|
return ThreadController.State.DESTROYED
|
||||||
|
|
||||||
|
when (this.thread._nativeThread.state) {
|
||||||
|
jThread.State.NEW -> return ThreadController.State.NEW
|
||||||
|
jThread.State.RUNNABLE -> return ThreadController.State.STARTED
|
||||||
|
jThread.State.BLOCKED -> return ThreadController.State.STARTED
|
||||||
|
jThread.State.WAITING -> return ThreadController.State.STARTED
|
||||||
|
jThread.State.TIMED_WAITING -> return ThreadController.State.STARTED
|
||||||
|
jThread.State.TERMINATED -> {
|
||||||
|
if (this._uncaughtException == null)
|
||||||
|
return ThreadController.State.FINISHED_SUCCESSFULLY
|
||||||
|
else
|
||||||
|
return ThreadController.State.FINISHED_WITH_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override val uncaughtException: Throwable
|
||||||
|
get() {
|
||||||
|
this._sync.withLock {
|
||||||
|
if (this._isExplicitlyDestroyed)
|
||||||
|
throw IllegalStateException("Thread destroyed")
|
||||||
|
|
||||||
|
val e = this._uncaughtException
|
||||||
|
if (e != null)
|
||||||
|
return e
|
||||||
|
|
||||||
|
when (this.thread._nativeThread.state) {
|
||||||
|
jThread.State.NEW, jThread.State.RUNNABLE, jThread.State.BLOCKED, jThread.State.WAITING, jThread.State.TIMED_WAITING ->
|
||||||
|
throw IllegalStateException("Thread not finished yet")
|
||||||
|
|
||||||
|
jThread.State.TERMINATED -> throw IllegalStateException("Thread finished without uncaught exceptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override fun start() {
|
||||||
|
this._sync.withLock {
|
||||||
|
if (this._isExplicitlyDestroyed)
|
||||||
|
throw IllegalStateException("Thread already was started, finished and destroyed")
|
||||||
|
// stdlib's errors aren't verbose
|
||||||
|
when (this.thread._nativeThread.state) {
|
||||||
|
jThread.State.NEW -> {}
|
||||||
|
jThread.State.TERMINATED -> throw IllegalStateException("Thread already was started and finished")
|
||||||
|
jThread.State.RUNNABLE, jThread.State.BLOCKED, jThread.State.WAITING, jThread.State.TIMED_WAITING ->
|
||||||
|
throw IllegalStateException("Thread already was started")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.thread._nativeThread.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun join() {
|
||||||
|
this._sync.withLock {
|
||||||
|
if (this._isExplicitlyDestroyed)
|
||||||
|
throw IllegalStateException("Can't join on destroyed thread")
|
||||||
|
if (this.thread._nativeThread.state == jThread.State.NEW)
|
||||||
|
throw IllegalStateException("Can't join on thread that isn't started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdlib's join uses Object.wait which may block caller virtual thread, so let wait in way virtual threads support
|
||||||
|
this._threadFinishedWaiter.await()
|
||||||
|
this.thread._nativeThread.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun releaseResources() {
|
||||||
|
this._sync.withLock {
|
||||||
|
if (this._isExplicitlyDestroyed)
|
||||||
|
throw IllegalStateException("Thread already destroyed")
|
||||||
|
when (this.thread._nativeThread.state) {
|
||||||
|
jThread.State.NEW -> {}
|
||||||
|
jThread.State.TERMINATED -> {}
|
||||||
|
jThread.State.RUNNABLE, jThread.State.BLOCKED, jThread.State.WAITING, jThread.State.TIMED_WAITING ->
|
||||||
|
throw IllegalStateException("Thread already was started")
|
||||||
|
}
|
||||||
|
this._isExplicitlyDestroyed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,77 +8,12 @@ import ru.landgrafhomyak.multitasking_0.fibers.Fiber
|
|||||||
@ExpectRefinement
|
@ExpectRefinement
|
||||||
@Suppress("EXPECT_ACTUAL_IR_INCOMPATIBILITY")
|
@Suppress("EXPECT_ACTUAL_IR_INCOMPATIBILITY")
|
||||||
public expect class Thread {
|
public expect class Thread {
|
||||||
/**
|
|
||||||
* Name of thread for debug purposes.
|
|
||||||
*
|
|
||||||
* After [releaseResources()][Thread.releaseResources] become inaccessible.
|
|
||||||
*/
|
|
||||||
public val name: String
|
|
||||||
override fun toString(): String
|
override fun toString(): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start execution of [routine][ThreadRoutine] passed to [create()][Thread.create].
|
* Name of thread for debug purposes.
|
||||||
*
|
|
||||||
* Doesn't define when any resources used by underlying functionality (e.g., stack) must be allocated.
|
|
||||||
*/
|
*/
|
||||||
public fun start()
|
public val name: String
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks caller thread or fiber until [releaseResources()][Thread.releaseResources] call allowed.
|
|
||||||
*
|
|
||||||
* Calling this method before thread [started][Thread.start] is error.
|
|
||||||
*/
|
|
||||||
public fun join()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirms that all resources used by underlying functionality are released.
|
|
||||||
* Can be called only once.
|
|
||||||
* Can be called before thread [started][Thread.start] or after thread [finishes][Thread.join].
|
|
||||||
*/
|
|
||||||
public fun releaseResources()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State of thread. Always accessible.
|
|
||||||
*/
|
|
||||||
public val state: State
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception that wasn't uncaught by [routine][ThreadRoutine] and terminated thread.
|
|
||||||
*
|
|
||||||
* Accessible only in [FINISHED_WITH_ERROR][Thread.State.FINISHED_WITH_ERROR] state,
|
|
||||||
* in other [states][Thread.state] will throw error, even after [releaseResources()][Thread.releaseResources].
|
|
||||||
*/
|
|
||||||
public val uncaughtException: Throwable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns `false` if thread prevents process from being finished while thread is running.
|
|
||||||
*
|
|
||||||
* After [releaseResources()][Thread.releaseResources] become inaccessible.
|
|
||||||
*/
|
|
||||||
public val isDaemon: Boolean
|
|
||||||
|
|
||||||
public enum class State {
|
|
||||||
/**
|
|
||||||
* Thread [created][Thread.create] but not [started][Thread.start] yet.
|
|
||||||
*/
|
|
||||||
NEW,
|
|
||||||
/**
|
|
||||||
* Thread [started][Thread.start], but not finished yet.
|
|
||||||
*/
|
|
||||||
STARTED,
|
|
||||||
/**
|
|
||||||
* Thread finished ([routine][ThreadRoutine] returned) normally.
|
|
||||||
*/
|
|
||||||
FINISHED_SUCCESSFULLY,
|
|
||||||
/**
|
|
||||||
* Thread [finished with error][Thread.uncaughtException], that wasn't caught by [routine][ThreadRoutine].
|
|
||||||
*/
|
|
||||||
FINISHED_WITH_ERROR,
|
|
||||||
/**
|
|
||||||
* All resources allocated by this [Thread] object are released and reference to descriptor can be lost.
|
|
||||||
*/
|
|
||||||
DESTROYED
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
/**
|
/**
|
||||||
@ -101,7 +36,7 @@ public expect class Thread {
|
|||||||
* @param isDaemon should thread prevent process from being finished while thread is running.
|
* @param isDaemon should thread prevent process from being finished while thread is running.
|
||||||
* @param routine
|
* @param routine
|
||||||
*/
|
*/
|
||||||
public fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): Thread
|
public fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): ThreadController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
package ru.landgrafhomyak.multitasking_0.threads
|
||||||
|
|
||||||
|
public interface ThreadController {
|
||||||
|
/**
|
||||||
|
* Thread controlled by this instance.
|
||||||
|
*/
|
||||||
|
public val thread: Thread
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start execution of [routine][ThreadRoutine] passed to [create()][Thread.create].
|
||||||
|
*
|
||||||
|
* Doesn't define when any resources used by underlying functionality (e.g., stack) must be allocated.
|
||||||
|
*/
|
||||||
|
public fun start()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks caller thread or fiber until [releaseResources()][ThreadController.releaseResources] call allowed.
|
||||||
|
*
|
||||||
|
* Calling this method before thread [started][ThreadController.start] is error.
|
||||||
|
*/
|
||||||
|
public fun join()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms that all resources used by underlying functionality are released.
|
||||||
|
* Can be called only once.
|
||||||
|
* Can be called before thread [started][ThreadController.start] or after thread [finishes][ThreadController.join].
|
||||||
|
*/
|
||||||
|
public fun releaseResources()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of thread. Always accessible.
|
||||||
|
*/
|
||||||
|
public val state: State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that wasn't uncaught by [routine][ThreadRoutine] and terminated thread.
|
||||||
|
*
|
||||||
|
* Accessible only in [FINISHED_WITH_ERROR][ThreadController.State.FINISHED_WITH_ERROR] state,
|
||||||
|
* in other [states][ThreadController.state] will throw error, even after [releaseResources()][ThreadController.releaseResources].
|
||||||
|
*/
|
||||||
|
public val uncaughtException: Throwable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `false` if thread prevents process from being finished while thread is running.
|
||||||
|
*
|
||||||
|
* After [releaseResources()][ThreadController.releaseResources] become inaccessible.
|
||||||
|
*/
|
||||||
|
public val isDaemon: Boolean
|
||||||
|
|
||||||
|
public enum class State {
|
||||||
|
/**
|
||||||
|
* Thread [created][Thread.create] but not [started][ThreadController.start] yet.
|
||||||
|
*/
|
||||||
|
NEW,
|
||||||
|
/**
|
||||||
|
* Thread [started][ThreadController.start], but not finished yet.
|
||||||
|
*/
|
||||||
|
STARTED,
|
||||||
|
/**
|
||||||
|
* Thread finished ([routine][ThreadRoutine] returned) normally.
|
||||||
|
*/
|
||||||
|
FINISHED_SUCCESSFULLY,
|
||||||
|
/**
|
||||||
|
* Thread [finished with error][ThreadController.uncaughtException], that wasn't caught by [routine][ThreadRoutine].
|
||||||
|
*/
|
||||||
|
FINISHED_WITH_ERROR,
|
||||||
|
/**
|
||||||
|
* All resources allocated by this [ThreadController] object are released and reference to descriptor can be lost.
|
||||||
|
*/
|
||||||
|
DESTROYED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user