В вашем примере кода вы реализуете PostWithAsyncReply
в терминах PostWithReply
. Это не идеально, потому что это означает, что когда вы вызываете PostWithAsyncReply
, и субъекту требуется некоторое время, чтобы обработать его, на самом деле связаны два потока: один исполняющий субъект и второй ожидающий его завершения. Было бы лучше иметь один поток, выполняющий актер, а затем вызывать обратный вызов в асинхронном случае. (Очевидно, что в синхронном случае нельзя избежать связывания двух потоков).
Обновление:
Подробнее об этом: вы создаете актера с аргументом, указывающим, сколько потоков запустить. Для простоты предположим, что каждый актер работает с одним потоком (на самом деле это довольно хорошая ситуация, поскольку у актеров может быть внутреннее состояние без блокировки, так как только один поток обращается к нему напрямую).
Актер A вызывает актера B, ожидая ответа. Чтобы обработать запрос, субъект B должен вызвать субъекта C. Таким образом, теперь только потоки A и B ожидают, а C - единственный, который фактически дает процессору какую-либо работу. Так много для многопоточности! Но это то, что вы получите, если будете постоянно ждать ответов.
Хорошо, вы можете увеличить количество потоков, которые вы запускаете в каждом актере. Но вы начинаете их, чтобы они могли сидеть и ничего не делать. Стек занимает много памяти, а переключение контекста может быть дорогим.
Так что лучше отправлять сообщения асинхронно с механизмом обратного вызова, чтобы вы могли получить готовый результат. Проблема с вашей реализацией заключается в том, что вы извлекаете другой поток из пула потоков, просто чтобы сидеть и ждать. Таким образом, вы в основном применяете обходной путь увеличения количества потоков. Вы выделяете поток для задачи , которая никогда не выполняется .
Было бы лучше реализовать PostWithReply
в терминах PostWithAsyncReply
, то есть наоборот. Асинхронная версия является низкоуровневой. Основываясь на моем примере на основе делегатов (потому что он требует меньше ввода кода!):
private bool InsertCoinImpl(int value)
{
// only accept dimes/10p/whatever it is in euros
return (value == 10);
}
public void InsertCoin(int value, Action<bool> accepted)
{
Submit(() => accepted(InsertCoinImpl(value)));
}
Так что частная реализация возвращает bool. Открытый асинхронный метод принимает действие, которое получит возвращаемое значение; как частная реализация, так и действие обратного вызова выполняются в одном потоке.
Надеюсь, необходимость синхронного ожидания будет делом меньшинства. Но когда вам это нужно, он может быть предоставлен вспомогательным методом, полностью общего назначения и не привязанным к какому-либо конкретному субъекту или типу сообщения:
public static T Wait<T>(Action<Action<T>> activity)
{
T result = default(T);
var finished = new EventWaitHandle(false, EventResetMode.AutoReset);
activity(r =>
{
result = r;
finished.Set();
});
finished.WaitOne();
return result;
}
Так что теперь в каком-то другом актере мы можем сказать:
bool accepted = Helpers.Wait<bool>(r => chocMachine.InsertCoin(5, r));
Аргумент типа для Wait
может быть ненужным, не пытался скомпилировать ничего из этого. Но Wait
в основном фокусирует обратный вызов для вас, так что вы можете передать его какому-то асинхронному методу, и снаружи вы просто возвращаете все, что было передано в обратный вызов, в качестве возвращаемого значения. Обратите внимание, что лямбда, которую вы передаете Wait
, все еще фактически выполняется в том же потоке, который вызвал Wait
.
Теперь мы вернемся к нашей обычной программе ...
Что касается фактической проблемы, о которой вы спрашивали, вы отправляете актеру сообщение, чтобы заставить его что-то сделать. Делегаты полезны здесь. Они позволяют эффективно заставить компилятор генерировать вам класс с некоторыми данными, конструктор, который вам даже не требуется вызывать явно, а также метод. Если вам нужно написать кучу маленьких классов, переключитесь на делегатов.
abstract class Actor
{
Queue<Action> _messages = new Queue<Action>();
protected void Submit(Action action)
{
// take out a lock of course
_messages.Enqueue(action);
}
// also a "run" that reads and executes the
// message delegates on background threads
}
Теперь конкретный производный актер следует этому шаблону:
class ChocolateMachineActor : Actor
{
private void InsertCoinImpl(int value)
{
// whatever...
}
public void InsertCoin(int value)
{
Submit(() => InsertCoinImpl(value));
}
}
Итак, чтобы отправить сообщение актеру, вы просто вызываете публичные методы. Частный метод Impl
делает настоящую работу. Не нужно писать кучу классов сообщений вручную.
Очевидно, я упустил материал об ответах, но это можно сделать с помощью большего количества параметров. (См. Обновление выше).