От Java до Kotlin: синхронизация и шаблон блокировки / ожидания / уведомления - PullRequest
0 голосов
/ 26 сентября 2019

Исходный контекст

Вернувшись при разработке приложений для Android / Java, я создал простой класс, который позволял бы мне синхронизировать такты вместо использования логики очереди и / или шаблона обратных вызовов.

Действительно, это позволило бы мне выполнять ряд задач линейным образом (при взгляде на код).В некоторых случаях это более полезно и намного удобнее в обслуживании и отладке.

Причина этого заключается в том, что, когда мне нужно работать с API-интерфейсами, которые предоставляют асинхронную логику с обратными вызовами для уведомления о результате или сбое,такие как Android Bluetooth LE API или Glide (чтобы назвать несколько), я хочу иметь возможность синхронизировать мой код при использовании асинхронных API.

Вот что я придумал:

Lock.java

import android.support.annotation.Nullable;
import android.util.Log;


/**
 * This class is a small helper to provide feedback on lock wait and notify process
 */
public final class Lock<T> {
    private final static String TAG = Lock.class.getSimpleName();

    private T result = null;
    private boolean isWaiting = false;
    private boolean wasNotified = false;


    /**
     * Releases the lock on this instance to notify the thread that was waiting and to provide the result
     * @param result    the result of the asynchronous operation that was waited
     */
    public synchronized final void setResultAndNotify(@Nullable final T result) {
        this.result = result;
        if (this.isWaiting) {
            synchronized (this) {
                this.notify();
            }
        } else
            this.wasNotified = true;
    }


    /**
     * This method locks on the current instance and wait for the duration specified in milliseconds
     * @param timeout the duration, in milliseconds, the thread will wait if it does not get notify
     */
    @Nullable
    public synchronized final T waitAndGetResult(final long timeout) {
        if (this.wasNotified) { // it might happen that the notify was performed even before the wait started!
            this.wasNotified = false;
            return this.result;
        }

        try {
            synchronized (this) {
                this.isWaiting = true;
                if (timeout < 0) {
                    this.wait();
                } else {
                    this.wait(timeout);
                }
                this.isWaiting = false;
                return this.result;
            }
        } catch (final InterruptedException e) {
            Log.e(TAG, "Failed to wait, Thread got interrupted -> " + e.getMessage());
            Log.getStackTraceString(e);
        }
        return null;
    }


    /**
     * This method locks on the current instance and wait as long necessary (no timeout)
     */
    @Nullable
    public synchronized final T waitAndGetResult() {
        return waitAndGetResult(-1)
    }


    /**
     * Tells whether this instance is currently waiting or not
     * @return <code>true</code> if waiting, <code>false</code> otherwise
     */
    public final boolean isWaiting() {
        return this.isWaiting;
    }
}

Я уверен, что это не лучшее решение, но, в некотором смысле, оно помогает в некоторых редких случаях, когда notifyможет быть вызван раньше, чем wait.Я думаю, что мог бы использовать AtomicBoolean, но пока проблем нет.

Вот как это можно использовать:

public final boolean performLongRunningTasks() {
    this.asynchronousTaskA.startAsync();
    final Result taskAResult = this.taskALock.waitAndGetResult(); // wait forever

    this.asynchronousTaskB.startAsync();
    final Result taskBResult = this.taskBLock.waitAndGetResult(5000L); // wait up to 5 seconds

    return taskAResult != null && taskBResult != null;
}


// callback
private void onTaskACompleted(@Nullable Result result) {
    this.taskALock.setResultAndNotify(result);
}


// callback
private void onTaskBCompleted(@Nullable Result result) {
    this.taskBLock.setResultAndNotify(result);
}

Полезно, верно?Конечно, ожидание вечности не является идеальным.

Миграция в Kotlin

Сейчас я развиваюсь в Kotlin и я начал читать о параллелизме и сопрограммах.В какой-то момент была одна статья, конвертировавшая шаблон синхронизации / блокировки / ожидания / уведомления Java в Kotlin, который помог мне перевести мой оригинальный класс в такой:

Lock.kt

import android.util.Log

class Lock<T> {

    private var result : T? = null

    private var isWaiting = false
    private var wasNotified = false

    private val lock = Object() // Java object, Urgh...

    fun setResultAndNotify(value: T?) {
        result = value
        if (isWaiting) {
            synchronized(lock) {
                lock.notify()
            }
        } else {
            wasNotified = true
        }
    }


    fun waitAndGetResult(timeout: Long) : T? {
        if (wasNotified) {
            wasNotified = false
            return result
        }

        try {
            synchronized(lock) {
                isWaiting = true
                if (timeout < 0) lock.wait() else lock.wait(timeout)
                isWaiting = false
            }
        } catch (e: InterruptedException) {
            Log.getStackTraceString(e)
        }

        return this.result
    }


    fun waitAndGetResult() = waitAndGetResult(-1)
}

Инет проблем здесь это работает ... Но это Котлин!Конечно, я мог бы сделать лучше?Каким было бы ваше решение?

1 Ответ

0 голосов
/ 26 сентября 2019

Kotlin в Android компилируется в том же Java-эквиваленте, так что вы можете без проблем использовать свой старый код в java с kotlin.Так что вам не нужно переписывать все классы в kotlin.Используйте их как есть.

Еще один вопрос: какой подход лучше?Что касается того, я бы сказал, что это сопрограммы kotlin!Они будут работать как ваш код, но без каких-либо блокировок и другого стандартного кода.Использование очень простое.

1) Добавить зависимость:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

2) Создайте функции приостановки, добавив модификатор suspend к функции:

suspend fun someBigTask(param: Param): Result {
   //...
}

3) Создайте свою область видимости и вспомогательные методы.Думайте об этом как о теме, в которой будет представлен ваш результат.Мне также нравится создавать еще одну область для фоновых задач:

val ioScope by lazy { CoroutineScope(Dispatchers.IO) } // background scope

// For background processing
@Suppress("SuspendFunctionOnCoroutineScope")
private suspend inline fun <T> CoroutineScope.await(crossinline block: suspend Job.() -> T): T {
    val deferred = CompletableDeferred<T>()
    launch {
        try {
            val result = deferred.block()
            deferred.complete(result)
        } catch (throwable: Throwable) {
            deferred.completeExceptionally(throwable)
        }
    }

    return deferred.await()
}

suspend inline fun <T> doAsync(crossinline block: suspend () -> T): T {
    return ioScope.await { block() }
}

4) Теперь вы готовы запустить свои долгосрочные задачи:

val job = SupervisorJob() // the job containing your performing tasks. You can use it to cancel your tasks when needed.
val mainScope = CoroutineScope(Dispatchers.Main) // scope of the main thread

fun launchMyJob(block: suspend () -> Unit) {
   mainScope.launch(job) {
       try {
           block()
       } catch(e: JobCancellationException) {
           // is thrown when you call job.cancel() method.
           // ignore it
       }
   }
}

fun performLongRunningTasks(param1: Param1, param2: Param2) {
   launchMyJob {
       val result1 = doAsync { someBigTask(param1) /* This operation is running in background thread */ }
       // result1 will be written only when someBigTask() is finished and coroutine will continue
       val result2 = doAsync { someBigTask(result1, param2) }

       // Here you can access views because you have result on the main thread.
       textView.text = result2.toString() 
   }
}

5) Отменить задание:

job.cancel()

Будьте осторожны при использовании этого метода, потому что после его вызова задание не может быть использовано снова, и ваша сопрограмма выдаст JobCancellationException, который вы должны обработать.

Даже если это выглядит как синхронныйКод в реальности все будет выполняться на разных потоках.Это красота котлинских сопрограмм.

...