SwitchToThread/Thread.Yield против Thread.Sleep(0) против Thead.Sleep(1)

Я пытаюсь написать окончательный метод "Выход", чтобы привести текущий временной интервал к другим потокам. До сих пор я обнаружил, что есть несколько различных способов заставить поток выдавать выделенный ему временной интервал. Я просто хочу убедиться, что я правильно их интерпретирую, поскольку документация не очень понятна. Итак, из того, что я прочитал о stackru, MSDN и различных постах в блоге, существуют следующие варианты, которые имеют разные преимущества / недостатки:

SwitchToThread [win32] / Thread.Yield [.NET 4 Beta 1]: уступает любому потоку на том же процессоре

  • Преимущество: примерно в два раза быстрее Thread.Sleep(0)
  • Недостаток: уступает только потокам на одном процессоре

Thread.Sleep(0) : уступает любому потоку с тем же или более высоким приоритетом на любом процессоре

  • Преимущество: быстрее чем Thread.Sleep(1)
  • Недостаток: уступает только потокам с одинаковым или более высоким приоритетом

Thread.Sleep(1): уступает любому потоку на любом процессоре

  • Преимущество: уступает любому потоку на любом процессоре
  • Недостаток: самый медленный вариант (Thread.Sleep(1) обычно приостанавливает поток примерно на 15 мс, если timeBeginPeriod / timeEndPeriod [win32] не используются)

Как насчет Thread.SpinWait? Можно ли это использовать для получения отрезка времени потока? Если нет, то для чего он используется?

Я там что-то еще пропустил или неверно истолковал. Я был бы признателен, если бы вы могли исправить / дополнить мое понимание.

Вот так выглядит мой метод Yield:

public static class Thread
{
    [DllImport("kernel32.dll")]
    static extern bool SwitchToThread();

    [DllImport("winmm.dll")]
    internal static extern uint timeBeginPeriod(uint period);

    [DllImport("winmm.dll")]
    internal static extern uint timeEndPeriod(uint period);

    /// <summary>  yields time slice of current thread to specified target threads </summary>
    public static void YieldTo(ThreadYieldTarget threadYieldTarget)
    {
        switch (threadYieldTarget) {
            case ThreadYieldTarget.None: 
                break; 
            case ThreadYieldTarget.AnyThreadOnAnyProcessor:
                timeBeginPeriod(1); //reduce sleep to actually 1ms instead of system time slice with is around 15ms
                System.Threading.Thread.Sleep(1); 
                timeEndPeriod(1); //undo
                break;
            case ThreadYieldTarget.SameOrHigherPriorityThreadOnAnyProcessor:
                System.Threading.Thread.Sleep(0); 
                break;
            case ThreadYieldTarget.AnyThreadOnSameProcessor:
                SwitchToThread();
                break;
            default: throw new ArgumentOutOfRangeException("threadYieldTarget");
        }
    }
}

public enum ThreadYieldTarget
{
    /// <summary>  Operation system will decide when to interrupt the thread </summary>
    None,
    /// <summary>  Yield time slice to any other thread on any processor </summary>
    AnyThreadOnAnyProcessor,
    /// <summary>  Yield time slice to other thread of same or higher piority on any processor </summary>
    SameOrHigherPriorityThreadOnAnyProcessor,
    /// <summary> Yield time slice to any other thread on same processor </summary>
    AnyThreadOnSameProcessor
}

4 ответа

SpinWait полезен на многопоточных процессорах. Благодаря гиперпоточности на одном физическом процессоре могут выполняться несколько запланированных потоков ОС, которые совместно используют ресурсы процессора. SpinWait указывает процессору, что вы не выполняете никакой полезной работы и что он должен запускать код из другого логического процессора. Как следует из названия, он обычно используется, когда вы вращаетесь.

Предположим, у вас есть такой код:

while (!foo) {} // Spin until foo is set.

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

Изменяя на:

while (!foo) {Thread.SpinWait(1);} 

Мы указываем процессору выделить некоторые ресурсы другому потоку.

SpinWait не влияет на планирование потоков в ОС.

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

Статья Джеффа Мозера "Как блокирует блокировку" ( http://www.moserware.com/2008/09/how-do-locks-lock.html) может немного рассказать о механике SpinWait. Чтобы процитировать документ:

Что именно он делает? Глядя на clr/src/vm/comsynchronizable.cpp Ротора, мы получаем реальность:

FCIMPL1 (void, ThreadNative:: SpinWait, int iterations) {WRAPPER_CONTRACT; STATIC_CONTRACT_SO_TOLERANT;

for(int i = 0; i < iterations; i++)
    YieldProcessor();

} FCIMPLEND

Дальнейшее погружение показывает, что "YieldProcessor" - это макрос:

#define YieldProcessor () __asm ​​{ rep nop }

Это инструкция по повторению без операции. Он также известен в руководстве по набору инструкций Intel как "PAUSE - Spin Loop Hint". Это означает, что процессор знает об ожидании вращения, которое мы хотим выполнить.

Связанный: http://msdn.microsoft.com/en-us/library/ms687419(VS.85).aspx http://www.moserware.com/2008/09/how-do-locks-lock.html

SpinWait предназначен для ожидания, не уступая текущему временному интервалу

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

У меня было впечатление, что Thread.Yield(x) для любого значения x <квант потока был эквивалентен, включая ноль, хотя у меня нет эталонных тестов на этот счет.

В дополнение к другим ответам, вот некоторые цифры профилирования.

(!) Не принимайте это профилирование слишком серьезно! Сделано только для иллюстрации приведенных выше ответов в цифрах и приблизительного сравнения величин.

static void Profile(Action func)
    {
        var sw = new Stopwatch();
        var beginTime = DateTime.Now;
        ulong count = 0;
        while (DateTime.Now.Subtract(beginTime).TotalSeconds < 5)
        {
            sw.Start();
            func();
            sw.Stop();
            count++;
        }
        Console.WriteLine($"Made {count} iterations in ~5s. Total sleep time {sw.ElapsedMilliseconds}[ms]. Mean time = {sw.ElapsedMilliseconds/(double) count} [ms]");
    }

        Profile(()=>Thread.Sleep(0));
        Profile(()=>Thread.Sleep(1));
        Profile(()=>Thread.Yield());
        Profile(()=>Thread.SpinWait(1));

Результаты вращающихся петель за ~5 с:

Function   | CPU % | Iters made |  Total sleep  | Invoke 
           |       |            |  time [ms]    | time [ms]
===================================================================== 
Sleep(0)   | 100 0 | 2318103    | 482           | 0.00020
Sleep(1)   |  6  0 | 4586       | 5456          | 1.08971 
Yield()    | 100 0 | 2495220    | 364           | 0.00010
SpinWait(1)| 100 0 | 2668745    | 81            | 0.00003

Сделано с Mono 4.2.3 x86_64

Другие вопросы по тегам