Как распоряжаться асинхронно? - PullRequest
19 голосов
/ 30 декабря 2008

Допустим, у меня есть класс, который реализует интерфейс IDisposable . Примерно так:

http://www.flickr.com/photos/garthof/3149605015/

MyClass использует некоторые неуправляемые ресурсы, поэтому метод Dispose () из IDisposable освобождает эти ресурсы. MyClass следует использовать так:

using ( MyClass myClass = new MyClass() ) {
    myClass.DoSomething();
}

Теперь я хочу реализовать метод, который вызывает DoSomething () асинхронно. Я добавляю новый метод к MyClass :

http://www.flickr.com/photos/garthof/3149605005/

Теперь, со стороны клиента, MyClass должен использоваться следующим образом:

using ( MyClass myClass = new MyClass() ) {
    myClass.AsyncDoSomething();
}

Однако, если я больше ничего не сделаю, это может завершиться ошибкой, так как объект myClass может быть удален до вызова DoSomething () (и выдаст неожиданный ObjectDisposedException ). Таким образом, вызов метода Dispose () (явного или неявного) следует отложить до завершения асинхронного вызова DoSomething () .

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

Спасибо.

ПРИМЕЧАНИЕ. Для простоты я не вписывал детали того, как реализован метод Dispose (). В реальной жизни я обычно следую шаблону Dispose .


ОБНОВЛЕНИЕ: Большое спасибо за ваши ответы. Я ценю ваши усилия. Поскольку chakrit прокомментировал , мне нужно, чтобы можно было сделать несколько вызовов асинхронной DoSomething . В идеале что-то вроде этого должно работать нормально:

using ( MyClass myClass = new MyClass() ) {

    myClass.AsyncDoSomething();
    myClass.AsyncDoSomething();

}

Я изучу счетный семафор, кажется, что я ищу. Это также может быть проблемой дизайна. Если мне это будет удобно, я поделюсь с вами некоторыми подробностями реального случая и тем, что на самом деле делает MyClass .

Ответы [ 6 ]

11 голосов
/ 30 декабря 2008

Похоже, что вы используете асинхронный шаблон на основе событий ( см. Здесь для получения дополнительной информации об асинхронных шаблонах .NET ), так что обычно у вас есть событие в классе, которое срабатывает при асинхронная операция завершена с именем DoSomethingCompleted (обратите внимание, что AsyncDoSomething действительно нужно назвать DoSomethingAsync, чтобы правильно следовать шаблону). С этим событием вы можете написать:

var myClass = new MyClass();
myClass.DoSomethingCompleted += (sender, e) => myClass.Dispose();
myClass.DoSomethingAsync();

Другой альтернативой является использование шаблона IAsyncResult, где вы можете передать делегату, который вызывает метод dispose, параметр AsyncCallback (более подробную информацию об этом шаблоне также можно найти на странице выше). В этом случае у вас будут методы BeginDoSomething и EndDoSomething вместо DoSomethingAsync, и вы будете называть это что-то вроде ...

var myClass = new MyClass();
myClass.BeginDoSomething(
    asyncResult => {
                       using (myClass)
                       {
                           myClass.EndDoSomething(asyncResult);
                       }
                   },
    null);        

Но каким бы способом вы это ни делали, вам нужно, чтобы вызывающий абонент был уведомлен о завершении асинхронной операции, чтобы он мог избавиться от объекта в нужное время.

5 голосов
/ 30 декабря 2008

Асинхронные методы обычно имеют обратный вызов, позволяющий вам выполнить некоторые действия после завершения Если это ваш случай, это будет что-то вроде этого:

// The async method taks an on-completed callback delegate
myClass.AsyncDoSomething(delegate { myClass.Dispose(); });

Другой способ - асинхронная оболочка:

ThreadPool.QueueUserWorkItem(delegate
{
    using(myClass)
    {
        // The class doesn't know about async operations, a helper method does that
        myClass.DoSomething();
    }
});
2 голосов
/ 19 января 2013

Я сожалею, что в рамках контракта IDisposable Microsoft не требовала, чтобы реализации позволяли вызывать Dispose из любого контекста потоков, так как не существует нормального способа, которым создание объекта может привести к продолжению существование контекста потоков, в котором он был создан. Можно спроектировать код так, чтобы поток, который создает объект, каким-то образом наблюдал, как объект устарел, и может Dispose по своему удобству, и чтобы когда поток больше не нужен ни для чего другого, он оставался до тех пор, пока все не станет подходящим. объекты были Dispose d, но я не думаю, что есть стандартный механизм, который не требует специального поведения со стороны потока, создающего Dispose.

Ваша лучшая ставка, вероятно, состоит в том, чтобы все объекты, представляющие интерес, создавались в общем потоке (возможно, в потоке пользовательского интерфейса), пытаться гарантировать, что поток будет оставаться в течение всей жизни объектов, представляющих интерес, и использовать что-то вроде Control.BeginInvoke запросить распоряжение объектами. При условии, что ни создание объекта, ни очистка не будут блокироваться в течение любого промежутка времени, это может быть хорошим подходом, но если любая операция может заблокировать другой подход, может потребоваться другой подход [возможно, откройте скрытую фиктивную форму со своим собственным потоком, так что можно используйте Control.BeginInvoke там].

В качестве альтернативы, если у вас есть контроль над реализациями IDisposable, спроектируйте их так, чтобы они могли безопасно запускаться асинхронно. Во многих случаях это будет «просто работать» при условии, что никто не пытается использовать предмет, когда он утилизируется, но это вряд ли является данностью. В частности, для многих типов IDisposable существует реальная опасность того, что несколько экземпляров объекта могут манипулировать общим внешним ресурсом [например, объект может содержать List<> созданных экземпляров, добавлять экземпляры в этот список при их создании и удалять экземпляры в Dispose; если операции со списком не синхронизированы, асинхронный Dispose может повредить список, даже если располагаемый объект не используется иным образом.

Кстати, полезный шаблон - объекты, позволяющие асинхронное удаление во время их использования, с ожиданием, что такое удаление приведет к тому, что любые выполняющиеся операции вызовут исключение при первой удобной возможности. Такие вещи, как сокеты, работают таким образом. Для операции чтения может быть невозможным преждевременное завершение операции, не оставляя свой сокет в бесполезном состоянии, но если сокет никогда не будет использоваться в любом случае, нет смысла для чтения продолжать ждать данных, если другой поток определил, что это должно сдаться. ИМХО, именно так все IDisposable объекты должны стараться вести себя, но я не знаю ни одного документа, призывающего к такой общей схеме.

2 голосов
/ 30 декабря 2008

Вы можете добавить механизм обратного вызова и передать функцию очистки в качестве обратного вызова.

var x = new MyClass();

Action cleanup = () => x.Dispose();

x.DoSomethingAsync(/*and then*/cleanup);

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

Один из способов - реализовать простой счетный семафор с классом Семафор для подсчета количества запущенных асинхронных заданий.

Добавьте счетчик в MyClass и при каждом вызове Async любой счетчик увеличивает счетчик при выходе из него. Когда семафор равен 0, класс готов к удалению.

var x = new MyClass();

x.DoSomethingAsync();
x.DoSomethingAsync2();

while (x.RunningJobsCount > 0)
    Thread.CurrentThread.Sleep(500);

x.Dispose();

Но я сомневаюсь, что это был бы идеальный способ. Я чувствую запах дизайнерской проблемы. Может быть, переосмысление дизайна MyClass могло бы избежать этого?

Не могли бы вы рассказать о реализации MyClass? Что он должен делать?

2 голосов
/ 30 декабря 2008

Я бы не стал каким-либо образом изменять код, чтобы разрешить асинхронное удаление. Вместо этого я бы удостоверился, что при вызове AsyncDoSomething у него будет копия всех данных, необходимых для выполнения. Этот метод должен отвечать за очистку всего, если его ресурсы.

1 голос
/ 30 декабря 2008

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

public class MyClass : IDisposable {

    private delegate void AsyncDoSomethingCaller();
    private delegate void AsyncDoDisposeCaller();

    private int pendingTasks = 0;

    public DoSomething() {
        // Do whatever.
    }

    public AsyncDoSomething() {
        pendingTasks++;
        AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller();
        caller.BeginInvoke( new AsyncCallback( EndDoSomethingCallback ), caller);
    }

    public Dispose() {
        AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller();
        caller.BeginInvoke( new AsyncCallback( EndDoDisposeCallback ), caller);
    }

    private DoDispose() {
        WaitForPendingTasks();

        // Finally, dispose whatever managed and unmanaged resources.
    }

    private void WaitForPendingTasks() {
        while ( true ) {
            // Check if there is a pending task.
            if ( pendingTasks == 0 ) {
                return;
            }

            // Allow other threads to execute.
            Thread.Sleep( 0 );
        }
    }

    private void EndDoSomethingCallback( IAsyncResult ar ) {
        AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState;
        caller.EndInvoke( ar );
        pendingTasks--;
    }

    private void EndDoDisposeCallback( IAsyncResult ar ) {
        AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState;
        caller.EndInvoke( ar );
    }
}

Некоторые проблемы могут возникать, если два или более потоков пытаются одновременно прочитать / записать переменную pendingTasks , поэтому для предотвращения условий гонки следует использовать ключевое слово lock :

public class MyClass : IDisposable {

    private delegate void AsyncDoSomethingCaller();
    private delegate void AsyncDoDisposeCaller();

    private int pendingTasks = 0;
    private readonly object lockObj = new object();

    public DoSomething() {
        // Do whatever.
    }

    public AsyncDoSomething() {
        lock ( lockObj ) {
            pendingTasks++;
            AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller();
            caller.BeginInvoke( new AsyncCallback( EndDoSomethingCallback ), caller);
        }
    }

    public Dispose() {
        AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller();
        caller.BeginInvoke( new AsyncCallback( EndDoDisposeCallback ), caller);
    }

    private DoDispose() {
        WaitForPendingTasks();

        // Finally, dispose whatever managed and unmanaged resources.
    }

    private void WaitForPendingTasks() {
        while ( true ) {
            // Check if there is a pending task.
            lock ( lockObj ) {
                if ( pendingTasks == 0 ) {
                    return;
                }
            }

            // Allow other threads to execute.
            Thread.Sleep( 0 );
        }
    }

    private void EndDoSomethingCallback( IAsyncResult ar ) {
        lock ( lockObj ) {
            AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState;
            caller.EndInvoke( ar );
            pendingTasks--;
        }
    }

    private void EndDoDisposeCallback( IAsyncResult ar ) {
        AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState;
        caller.EndInvoke( ar );
    }
}

Я вижу проблему с этим подходом. Поскольку освобождение ресурсов выполняется асинхронно, может сработать что-то подобное:

MyClass myClass;

using ( myClass = new MyClass() ) {
    myClass.AsyncDoSomething();
}

myClass.DoSomething();

Когда ожидаемое поведение должно запускать ObjectDisposedException , когда DoSomething () вызывается вне , используя предложение . Но я не нахожу это достаточно плохим, чтобы переосмыслить это решение.

...