Подтвердить что ты не робот

Как распоряжаться асинхронно?

Скажем, у меня есть класс, который реализует интерфейс 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 pattern.


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

using ( MyClass myClass = new MyClass() ) {

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

}

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

4b9b3361

Ответ 1

Итак, моя идея состоит в том, чтобы сохранить, сколько 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, поэтому ключевое слово блокировка должно использоваться для предотвращения условий гонки:

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() вызывается вне с использованием. Но я не считаю это достаточно плохим, чтобы переосмыслить это решение.

Ответ 2

Похоже, вы используете шаблон async на основе событий (см. здесь для получения дополнительной информации об асинхронных шаблонах .NET), так что вы Обычно d это событие в классе, который запускается, когда операция async завершается с именем 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);        

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

Ответ 3

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

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

Другим способом это оболочка async:

ThreadPool.QueueUserWorkItem(delegate
{
    using(myClass)
    {
        // The class doesn't know about async operations, a helper method does that
        myClass.DoSomething();
    }
});

Ответ 4

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

Ответ 5

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

var x = new MyClass();

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

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

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

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

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

var x = new MyClass();

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

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

x.Dispose();

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

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

Ответ 6

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

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

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

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