Шаблонный метод в многопоточном контексте

Допустим, у нас есть шаблонный метод, который выглядит так

abstract class Worker
{
  public void DoJob()
  {
    BeforJob()
    DoRealJob();
    AfterJob();
  }

  abstract void DoRealJob();
}

подклассы, которые наследуются от класса Wroker, должны реализовывать метод DoRealJob(), когда реализация выполняется в том же потоке, все в порядке, три части метода DoJob() выполняются в этом порядке.

  1. Перед работой()
  2. ВыполняйНастоящуюРаботу()
  3. После работы ()

но когда DoRealJob() выполняется в другом потоке, AfterJob() может быть выполнено до завершения DoRealJob()

мое фактическое решение состоит в том, чтобы позволить подклассам вызывать AfterJob(), но это не мешает подклассу забыть вызвать его, и мы теряем преимущества шаблонного метода.

есть ли другие способы получить согласованный порядок вызовов, несмотря на то, что DoRealJob() блокируется или нет?


person anouar.bagari    schedule 06.07.2014    source источник
comment
Я не думаю, что есть какой-то способ добиться этого. Просто напишите четкую документацию вашего SDK, чтобы разработчики знали, как его использовать. Независимо от того, что вы делаете, разработчик может даже запустить неуправляемый код C++ внутри этой функции, которая выполняется в неуправляемом потоке, и вы не можете абсолютно ничего сделать, чтобы убедиться, что его код завершил выполнение до вызова метода AfterJob. Четко укажите в своей документации, что метод DoRealJob должен быть синхронным, чтобы ваш SDK работал правильно. В противном случае вы можете предоставить альтернативный класс Async, который принимает обратный вызов.   -  person Darin Dimitrov    schedule 06.07.2014


Ответы (2)


Вы не можете получить как простое наследование (подпись и перехват), так и поддержку асинхронных операций в своем коде.

Эти две цели взаимоисключающие.

Наследники должны знать о механизмах обратного вызова либо в прямом (Задачи, асинхронно) или косвенно (события, функции обратного вызова, Auto(Manual)ResetEvents или другие конструкции синхронизации). Некоторые из них новые, некоторые старые. И сложно сказать, какой из них будет лучше для конкретного случая использования.

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

So:

  • Если вы действительно считаете, что ваш класс будет использоваться в качестве основы для какого-то асинхронного работника, то вы должны спроектировать его соответствующим образом.
  • Если нет - не переусердствуйте. Вы не можете рассматривать любой возможный сценарий. Просто задокументируйте свой класс достаточно хорошо, и я сомневаюсь, что кто-нибудь попытается реализовать DoRealJob асинхронно, особенно если вы назовете его DoRealJobSynchronously. Если кто-то попытается это сделать, то в этом случае ваша совесть может быть первозданно чиста.

ИЗМЕНИТЬ:

Как вы думаете, было бы правильно, если бы я предоставил обе версии, синхронную и асинхронную, DoRealJob и флаг IsAsynchronous, чтобы я мог решить, какую из них вызывать?

Как я уже сказал, я не знаю ваших реальных сценариев использования. И нереально считать, что конструкция сможет эффективно справиться со всеми ними.

Также есть два очень важных вопроса, которые относятся к вашему общему Worker классу и его DoJob методу:

1) Вы должны определить, хотите ли вы, чтобы метод DoJob был синхронным или асинхронным, или вы хотите иметь как синхронную, так и асинхронную версии? Это не имеет прямого отношения к вашему вопросу, но по-прежнему является очень важным проектным решением, поскольку оно окажет большое влияние на вашу объектную модель. Этот вопрос можно было бы перефразировать так:

Вы хотите, чтобы метод DoJob блокировал любые действия после его вызова, пока он не выполнит свою работу, или вы хотите вызвать его как метод StartJob, который просто запустит реальную обработку, но другие механизмы должны уведомить вас, когда задание завершено (или остановить его вручную):

        //----------------Sync worker--------------------------
        SyncWorker syncWorker = CreateSyncStringWriter("The job is done");
        Console.WriteLine("SyncWorker will be called now");

        syncWorker.DoJob(); // "The job is done" is written here

        Console.WriteLine("SyncWorker call ended");


        //----------------Async worker--------------------------
        Int32 delay = 1000;
        AsyncWorker asyncWorker = CreateAsyncStringWriter("The job is done", delay);
        Console.WriteLine("AsyncWorker will be called now");

        asyncWorker.StartDoJob(); // "The job is done" won't probably be written here

        Console.WriteLine("AsyncWorker call ended");
        // "The job is done" could be written somewhere here.

2) Если вы хотите, чтобы DoJob был асинхронным (или имел асинхронную версию), вам следует подумать, хотите ли вы иметь какие-то механизмы, которые будут уведомлять, когда DoJob завершит обработку - Шаблоны асинхронного программирования , либо для вас абсолютно неважно, когда он заканчивается и заканчивается ли вообще.

Итак:

У вас есть ответы на эти два вопроса?

  • Если да - то хорошо.
  • Если нет - доработайте и учтите ваши требования.
  • Если вы все еще не уверены, придерживайтесь простых методов синхронизации.

Однако, если вы считаете, что вам нужна некоторая асинхронная инфраструктура, то, учитывая, что это C# 3.0, вам следует использовать Модель асинхронного программирования.

Почему этот, а не основанный на событии? Поскольку интерфейс IAsyncResult, несмотря на его громоздкость, довольно общий и может быть легко использоваться в модель, упрощающая будущий переход на более поздние версии .NET.

Это будет что-то вроде:

 /// <summary>
 /// Interface for both  the sync and async job.
 /// </summary>
 public interface IWorker
{
    void DoJob();

    IAsyncResult BeginDoJob(AsyncCallback callback);

    public void EndDoJob(IAsyncResult asyncResult);
}

/// <summary>
/// Base class that provides DoBefore and DoAfter methods
/// </summary>
public abstract class Worker : IWorker
{
    protected abstract void DoBefore();
    protected abstract void DoAfter();


    public IAsyncResult BeginDoJob(AsyncCallback callback)
    {
        return new Action(((IWorker)this).DoJob)
            .BeginInvoke(callback, null);
    }
    //...
}

public abstract class SyncWorker : Worker
{
    abstract protected void DoRealJobSync();

    public void DoJob()
    {
        DoBefore();
        DoRealJobSync();
        DoAfter();
    }
}

public abstract class AsyncWorker : Worker
{
    abstract protected IAsyncResult BeginDoRealJob(AsyncCallback callback);
    abstract protected void EndDoRealJob(IAsyncResult asyncResult);

    public void DoJob()
    {
        DoBefore();
        IAsyncResult asyncResult = this.BeginDoRealJob(null);
        this.EndDoRealJob(asyncResult);
        DoAfter();
    }
}

P.S. Этот пример неполный и не проверен.
P.P.S. Вы также можете рассмотреть возможность использования делегатов вместо абстрактных (виртуальных) методов для выражения своих заданий:

public class ActionWorker : Worker
{
    private Action doRealJob;
    //...

    public ActionWorker(Action doRealJob)
    {
        if (doRealJob == null)
            throw new ArgumentNullException();

        this.doRealJob = doRealJob;
    }

    public void DoJob()
    {
        this.DoBefore();
        this.doRealJob();
        this.DoAfter();
    }
}

DoBefore и DoAfter могут быть выражены аналогичным образом.
PPPS: делегат Action — это конструкция версии 3.5, поэтому вам, вероятно, придется определить собственный делегат, который принимает нулевые параметры и возвращает значение void.

public delegate void MyAction()
person Eugene Podskal    schedule 06.07.2014
comment
Как вы думаете, будет ли правильно, если я предоставлю обе версии, sync и async, DoRealJob и флаг IsAsynchronous, чтобы я мог решить, какую из них вызывать? - person anouar.bagari; 07.07.2014
comment
Спасибо за этот подробный ответ. - person anouar.bagari; 08.07.2014

Рассмотрите возможность изменения DoRealJob на DoRealJobAsync и присвойте ему возвращаемое значение Task. Таким образом, вы можете дождаться возможного асинхронного результата.

Таким образом, ваш код будет выглядеть так

abstract class Worker
{
  public void DoJob()
  {
    BeforJob()
    await DoRealJobAsync();
    AfterJob();
  }

  abstract Task DoRealJob();
}

Если у вас нет .net 4.0 и вы не хотите использовать старую 3.0 CTP асинхронности, вы можете использовать базовый стиль задачи normale:

  abstract class Worker
    {
      public void DoJob()
      {
        BeforJob()
        var task = DoRealJobAsync();
         .ContinueWith((prevTask) =>
              {
                  AfterJob()
               });
      }

      abstract Task DoRealJob();
    }
person Boas Enkler    schedule 06.07.2014
comment
Я забыл упомянуть, что использую С# 3 - person anouar.bagari; 06.07.2014
comment
Была асинхронная версия ctp, которая могла работать с С# 3. Также там вы могли сделать что-то похожее на Task.Wait(myTask) Таким образом, ваш метод также мог возвращать задачу, которую можно ждать. - person Boas Enkler; 06.07.2014
comment
Я также добавил пример, чтобы сделать это без Async CTP. - person Boas Enkler; 06.07.2014
comment
Класс System.Threading.Tasks.Task был введен в C# 4.0, поэтому я действительно не понимаю, откуда эти вещи ContinueWith появятся в C# 3.0. - person Darin Dimitrov; 06.07.2014
comment
упс извините за ошибку. Я думал, что это было в 3.0. Тогда вашим решением будут обратные вызовы/делегеты, которые передаются в качестве параметров функции RealJob, тогда функция realjob может вызвать этот делегат, чтобы продолжить подобное поведение. - person Boas Enkler; 06.07.2014
comment
Но как бы вы заставили разработчика функции DoRealJob когда-либо вызывать какой-либо делегат, передаваемый в качестве параметра? А если он не позвонит? - person Darin Dimitrov; 06.07.2014
comment
даже если это С# 4, он не будет работать, если DoRealJob запускает код в другом потоке - person anouar.bagari; 06.07.2014