9

Как правильно уменьшить размер стека для .NET приложения? Возможно есть какие либо директивы или опции в Visual Studio.

andreycha
  • 25,167
  • 4
  • 46
  • 82
Alexis
  • 3,476
  • Хм. А зачем? Для чего такое вдруг понадобилось? – VladD Oct 14 '15 at 19:54
  • Для увеличения числа потоков. По умолчанию поднимается в районе 2000 потоков. Согласно статье Раймонда Чена: http://blogs.msdn.com/b/oldnewthing/archive/2005/07/29/444912.aspx ему удалось преодолеть этот порог увеличив стек. Но там пример для плюсов, не знаю как его применить к Task (с опцией шедулера LongRunning). – Alexis Oct 14 '15 at 19:57
  • Везде предлагаются решения либо для C++ либо софт для реверсинга, с помощью которого ковыряют .exe. Считаю это несколько неправильным. – Alexis Oct 14 '15 at 19:58
  • 1
    Так, уже теплее. А зачем вам 2000 потоков? Может быть, лучше перейти на async/await? – VladD Oct 14 '15 at 19:58
  • @VladD, серверное многопоточное TCP приложение. Этим все сказано. – Alexis Oct 14 '15 at 19:59
  • 1
    (1) Для увеличения количества потоков нужно по идее не увеличить, а уменьшить стек. (2) Всё равно непонятно, почему не async/await. TCP-сервер вполне можно писать на async/await. Выделять на каждый запрос по потоку — слишком дорогой вариант. – VladD Oct 14 '15 at 20:01
  • Затем, чтобы обойти лимит на адресное пространство, лучше по идее компилироваться под x64. – VladD Oct 14 '15 at 20:02
  • Приложение уже x64. Спасибо что поправили по уменьшению размера стека. Но остается открытым вопрос - как манипулировать размером стека не прибегая к реверсингу .exe? – Alexis Oct 14 '15 at 20:04
  • 2
    Но всё же, мне кажется, вы идёте неверным путём. Выделять отдельный поток каждому реквесту очень накладно, и вы тем самым налагаете жёсткое ограничение на количество одновременных запросов. Вас можно будет легко заДОСить, просто открыв 20000 соединений и медленно отвечая на запросы. Почему всё же на async/await? – VladD Oct 14 '15 at 20:08
  • @VladD, я не пишу сервер. Я пишу клиент. – Alexis Oct 14 '15 at 20:08
  • Хорошо, клиент. Но вопрос остаётся. – VladD Oct 14 '15 at 20:11
  • Возьмем к примеру обычный TCP сканер портов. Почему же для увеличения производительности в них не используют Async\Await? – Alexis Oct 14 '15 at 20:16
  • Например, потому, что он написан давно? Мой коллега, который писал очень много сетевого кода ещё на C++, не пользовался отдельными потоками из-за неэффективности, а строил state machine вручную. На C# 5+ за вас её построит за кулисами async-метод. – VladD Oct 14 '15 at 20:21
  • Для сканера портов проблем вообще нету. Вы запускаете одновременно n Task'ов, каждый из которых стучится в порт. Всё будет летать по идее даже на одном потоке, если вы не будете пользоваться блокирующими функциями. – VladD Oct 14 '15 at 20:24
  • 1
    Попробуйте сами ConnectAsync, вы удивитесь :) Потоки не нужны. – VladD Oct 14 '15 at 20:25
  • @VladD, вы привели дефолтный метод TcpClient-а. Я просто привел сканер портов как пример. Возьмем же приложение посложнее, с TCP\TLS соединениями, в отдельной сборке. Причем часть приложения в нативном коде. В данный момент менять потоки на async\await нет смысла. Тем более в части приложения используется работа с разными доменами. – Alexis Oct 14 '15 at 20:29
  • 3
    Мне сложно говорить по поводу конкретно вашего приложения, но для хорошего сетевого кода очень нетипична синхронная обработка в отдельных потоках. Я бы не советовал идти этим путём, а вместо этого «асинхронизировать» ваше приложение. Но вам, как архитектору проекта, в любом случае виднее. – VladD Oct 14 '15 at 20:32

1 Answers1

9

Вы можете, например, воспользоваться конструктором Thread с указанием максимального размера стека.


Если вы планируете запускать Task на этом потоке, имеет смысл реализовать TaskScheduler, который перекинет Task в этот поток.

Или можно воспользоваться готовым scheduler'ом, например, WPF.


Пример кода:

При помощи этого метода можно «перебросить» async-метод в поток, на котором бежит данный WPF-диспетчер:

static class AsyncHelper
{
    public static DispatcherRedirector RedirectTo(Dispatcher d)
    {
        return new DispatcherRedirector(d);
    }
}

// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx
public struct DispatcherRedirector : INotifyCompletion
{
    public DispatcherRedirector(Dispatcher dispatcher)
    {
        this.dispatcher = dispatcher;
    }

    #region awaiter
    public DispatcherRedirector GetAwaiter()
    {
        // combined awaiter and awaitable
        return this;
    }
    #endregion

    #region awaitable
    public bool IsCompleted
    {
        get
        {
            // true means execute continuation inline
            return dispatcher.CheckAccess();
        }
    }

    public void OnCompleted(Action continuation)
    {
        dispatcher.BeginInvoke(continuation);
    }

    public void GetResult() { }
    #endregion

    Dispatcher dispatcher;
}

Теперь вам нужен поток, в котором бежит диспетчер.

public class DispatcherThread : IDisposable
{
    public Dispatcher Dispatcher { get; private set; }
    public TaskScheduler TaskScheduler { get; private set; }

    Thread thread;

    public DispatcherThread(int maxStackSize)
    {
        using (var barrier = new AutoResetEvent(false))
        {
            thread = new Thread(() =>
            {
                Dispatcher = Dispatcher.CurrentDispatcher;
                barrier.Set();
                Dispatcher.Run();
            }, maxStackSize);

            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
            barrier.WaitOne();
        }

        TaskScheduler = Get(() => TaskScheduler.FromCurrentSynchronizationContext());
    }

    // ---------------------------------------------
    // остальные функции вам не нужны для вашей задачи, но могут пригодиться впоследствии
    public void Execute(Action a)
    {
        if (Dispatcher.CheckAccess())
            a();
        else
            Dispatcher.Invoke(a);
    }

    public void FireAndForget(Action a)
    {
        Dispatcher.BeginInvoke(a);
    }

    public T Get<T>(Func<T> getter)
    {
        if (Dispatcher.CheckAccess())
            return getter();
        else
        {
            T t = default(T);
            Dispatcher.Invoke((Action)(() => { t = getter(); }));
            return t;
        }
    }

    public Task<T> GetAsync<T>(Func<T> getter)
    {
        return Dispatcher.InvokeAsync(getter).Task;
    }

    public Task StartNewTask(Action action)
    {
        return Task.Factory.StartNew(
                    action: action,
                    cancellationToken: CancellationToken.None,
                    creationOptions: TaskCreationOptions.None,
                    scheduler: TaskScheduler);
    }

    public Task<T> StartNewTask<T>(Func<T> function)
    {
        return Task.Factory.StartNew(
                    function: function,
                    cancellationToken: CancellationToken.None,
                    creationOptions: TaskCreationOptions.None,
                    scheduler: TaskScheduler);
    }

    public void Dispose()
    {
        Dispatcher.InvokeShutdown();
        if (thread != Thread.CurrentThread)
            thread.Join();
    }
}

Этим можно пользоваться, например, так:

using (var t = new DispatcherThread(maxStackSize))
{
    await AsyncHelper.RedirectTo(t.Dispatcher);
    // остаток метода
}

Обновление: совсем забыл, надо же по идее сбежать из умирающего потока! Например, в thread pool.

static class AsyncHelper
{
    public static ThreadPoolRedirector RedirectToThreadPool()
    {
        return new ThreadPoolRedirector();
    }
}

public struct ThreadPoolRedirector : INotifyCompletion
{
    #region awaiter
    public ThreadPoolRedirector GetAwaiter()
    {
        // combined awaiter and awaitable
        return this;
    }
    #endregion

    #region awaitable
    public bool IsCompleted
    {
        get
        {
            // true means execute continuation inline
            return Thread.CurrentThread.IsThreadPoolThread;
        }
    }

    public void OnCompleted(Action continuation)
    {
        ThreadPool.QueueUserWorkItem(o => continuation());
    }

    public void GetResult() { }
    #endregion
}

и использовать как

using (var t = new DispatcherThread(maxStackSize))
{
    await AsyncHelper.RedirectTo(t.Dispatcher);
    // остаток метода
    await AsyncHelper.RedirectToThreadPool();
}

Хотя может быть это и не нужно, InvokeShutdown не убивает поток немедленно. Но тем не менее.


Более современная версия DispatcherThread — в этом ответе.

VladD
  • 206,799
  • А как быть для Task? Я не использую Thread. – Alexis Oct 14 '15 at 20:12
  • 1
    @z668: Окей, тогда вам нужен собственный TaskScheduler, который будет выполнять Task на указанном вами потоке или потоках. – VladD Oct 14 '15 at 20:22
  • Благодарю, думаю стоит добавить это в ответ. Пошел ковырять исходники LongRunning. – Alexis Oct 14 '15 at 20:31
  • 1
    @z668: LongRunning просто запускает Task на thread pool'е. – VladD Oct 14 '15 at 20:33
  • Искренне благодарен, данным примером кода вы сэкономили мне ночь без сна. – Alexis Oct 14 '15 at 20:52
  • 1
    @z668: Вот и хорошо :) Не стоит благодарности, информация должна быть свободной! – VladD Oct 14 '15 at 20:53
  • @VladD Интересный способ, спасибо! Ваша любовь к таскам безгранична ;D – ApInvent Oct 14 '15 at 21:21
  • @ApInvent: Способ с await RedirectTo(...) кажется мне очень изящным, я прожужжал им все уши коллегам. :) – VladD Oct 14 '15 at 21:39
  • Только вот увеличить размер стека потока можно только имея full trust-привилегии. Без них лимит - стандартный мегабайт. А вот уменьшить можно всегда. – Qwertiy Oct 14 '15 at 21:47
  • @Qwertiy: Ну, в вопросе идёт речь именно об уменьшении. Кроме того, если уж в проекте есть нативный код... – VladD Oct 14 '15 at 21:48
  • Эм.. Я как-то не так читаю заголовок темы? "Как правильно увеличить размер стека?" – Qwertiy Oct 14 '15 at 21:49
  • @Qwertiy: Почитайте комментарии под вопросом :) – VladD Oct 14 '15 at 21:49
  • Ага, уже прочитал. Только, как бы это, эм.. Стек потока - это 1 мегабайт. В x64 каких-либо сильных ограничений на память нет. Так сколько же потоков надо насоздавать, чтобы не влезть в имеющуюся память? Кстати, я не уверен, не потребляет ли поток ещё какие ресурсы помимо памяти под стек, которые сыграют более значительную роль в лимите на число потоков. – Qwertiy Oct 14 '15 at 21:54
  • @Qwertiy: Вроде ничего особенно большого кроме стека у потока нету. Странно, что x64 упирается в проблему, да. – VladD Oct 14 '15 at 21:55
  • А как насчёт хендла потока? У него случайно нет ограничения 65535? – Qwertiy Oct 14 '15 at 21:59
  • Не обязательно большого. Возможно, именно лимитированного другими механизмами. – Qwertiy Oct 14 '15 at 22:00
  • @Qwertiy: Ну, в вопросе как раз ссылка на http://blogs.msdn.com/b/oldnewthing/archive/2005/07/29/444912.aspx – VladD Oct 14 '15 at 22:10
  • Ну там про 32-битные и лимит в 2000. Кстати, а зачем в том коде CloseHandle(h);? Я так понимаю, поток продолжает жить после этого. Т. е. что эта штука вообще должна сделать с потоком? – Qwertiy Oct 14 '15 at 22:15
  • @Qwertiy: Ну, h — это хэндл, по которому можно дождаться окончания работы потока. Если не закрыть его, то надо хранить где-то и закрыть в конце, иначе будет resource leak. – VladD Oct 14 '15 at 22:21
  • Но ведь поток жив. Или только мне кажется странным, что хендл закрывается, а ресурс, к которому он привязан, остаётся существовать? – Qwertiy Oct 14 '15 at 22:29
  • Но это же просто хэндл, по которому можно ожидать его окончания. Он не представляет собой поток. – VladD Oct 15 '15 at 09:44