Thread.Yield vs Thread.Sleep(0) vs Thread.Sleep(1)

Dziś kolejny mechanizm potrzebny do zrozumienia struktury SpinWait, którą opiszę w następnym poście. Thread.Yield oddaje wątek z powrotem do CPU. Wywołanie mówi, że nie mam nic więcej do roboty i jeśli CPU ma coś lepszego do zrobienia to niech to zrobi a po tym dopiero wątek macierzysty zostanie wznowiony. Innymi słowy jeśli wątek A wywołuje Thread.Yield wtedy CPU zawiesza go, wykonuje inny i potem z powrotem powraca do wątku A. Jednak należy zaznaczyć, że w przypadku Yield, może zostać wybrany wątek wyłącznie wykonywany na tym samym procesorze. Nie można ustąpić miejsca na rzez wątku znajdującego się na innym rdzeniu.

Thread.Sleep (0) oraz Thread.Sleep(1) wykonują to samo z tym, że:

Metoda Opis
Thread.Sleep( 0 )

Ustąpienie miejsca wątkowi o wyższym lub takim samym  priorytecie, który może zostać wykonany na dowolnym procesorze. Wolniejszy sposób niż Thread.Yield ale szybszy niż Thread.Sleep(1).

Thread.Sleep (1)

Ustąpienie miejsca wątkowi o dowolnym priorytecie, wykonywanym na dowolnym procesorze. Najwolniejsza z 3 metod.

Thread.Yield()

Ustąpienie miejsca wątkowi o wyższym\takim samym priorytecie, wykonywanym na tym samym procesorze. Bardzo szybka metoda. Zwraca true gdy znaleziono kandydata na rzez którego należy ustąpić miejsce

Powyższe metody przydatne są w programowaniu współbieżnym bez stosowania blokad. W następnym poście przedstawię stos działający w środowisku wielowątkowym ale nie wykorzystujący lock’ow. Oczywiście Thread.Yield nie spowoduje, że inny wątek zostanie w całości wykonany. Jest to wyłącznie oddanie trochę czasu.  Task scheduling to skomplikowany mechanizm i nigdy nie wiadomo, kiedy wątek zostanie wstrzymany.

Thread.SpinWait() a Thread.Sleep()

Dziś kolejny post z ostatniego cyklu o o wielowątkowości.  Myślę, że Thread.Sleep jest znany każdemu – służy po prostu do uśpienia wątku. Na przykład:

for(int i=0;i<20;i++)
    Thread.Sleep(100);

Kod spowoduje uśpienie wątku na 100 milisekund czyli razem, uwzględniając pętle  >2 sekundy. Wywołanie SpinWait jest podobne:

Thread.SpinWait(100000000)

Pierwsza różnica to parametr wejściowy, który nie jest czasem w milisekundach a liczbą iteracji do przeczekania. Czym się zatem różni przeczekanie od uśpienia? SpinWait nie powoduje zmiany kontekstu. Jak pisałem w poprzednich postach, zmiana kontekstu jest kosztowna ponieważ wymaga przywrócenia stanu procesora. W niektórych przypadkach lepiej tego uniknąć. Generalnie należy się sporo zastanowić nad użyciem SpinWait. Jeśli wiemy, że operacja powinna być wznowiona bardzo szybko wtedy lepiej użyć Spinwait, który nie usuwa wątku a po prostu dosłownie blokuje wątek. Przez ten czas wątek wciąż zużywa zasoby ale jest to lepsze niż task scheduling, zmiana kontekstu itp. Pod warunkiem jeśli nie musimy czekać zbyt długo. W innych przypadkach, lepiej wątek oddać innym zadaniom.

MSDN nie definiuje co to jest liczba iteracji. Osobiście myślę, że jest to jakaś liczba cyklów procesora\liczba wykonanych instrukcji a co za tym czas zależy od konkretnego procesora i jego wydajności. SpinWait może służyć do zaawansowanej synchronizacji. Jeśli wiemy, że jakiś zasób będzie dostępny za parę milisekund to wtedy użycie SpinWait zamiast lock czy co gorsza Sleep jest wydajniejsze. Ale jak pisałem, jest to zdecydowanie mechanizm dla zaawansowanych algorytmów. Zły scenariusz dla Spinwait spowoduje wysokie zużycie procesora i blokadę zasobów, które w przypadku Sleep mogłyby zostać oddane innym zadaniom.

Synchronizacja za pomocą CountdownEvent

W ostatnich postach opisuje różne mechanizmy synchronizacji w c#. Jak widać, jest tego na prawdę wiele i sama znajomość słowa lock nie wystarcza jeśli poważnie myśli się o algorytmach współbieżnych. Dziś kolejna metoda synchronizacji a mianowicie klasa CountdownEvent. Ostatnie posty oprócz wprowadzenia do wspomnianych klas stanowią wstęp do kolekcji współbieżnych, które zamierzam omówić wkrótce.

Konstruktor CountdownEvent przyjmuje liczbę całkowita (zwykle większą od 0), która stanowi wartość początkową wewnętrznego licznika. Następnie podczas wywołania metody Wait, wątek jest blokowany dopóki licznik nie osiągnie wartości zero. Licznik jest zmniejszany przez metodę Signal, która ma dwa przeładowania. Jedno zmniejsza licznik po prostu o 1 a drugi o wartość przekazaną w parametrze:

var countDownEvent = new CountdownEvent(20);
countDownEvent.Signal(15); 

Jedna bardzo ważna uwaga. CountdownEvent używa mechanizmu Spin opisanego kilka postów wcześniej. Z tego względu, wykorzystanie klasy jest wydajne jednak wyłącznie gdy wiemy, że “blokada” została nałożona na krótki czas. W innych scenariuszach, może się okazać, że wątek blokuje zasoby i lepiej aby został uśpiony ponieważ będzie musiał poczekać trochę na swoją kolej. Polecam przeczytanie tego posta aby w pełni zrozumieć zasadę działania Spin i czym ona się różni od klasycznych blokad.

Analogiczną metodą jest AddCount, która zwiększa licznik:

var countDownEvent = new CountdownEvent(20);
countDownEvent.AddCount(15); 

Metoda Wait blokuje wątek i czeka aż licznik zostanie ustawiony na zero (np. poprzez wysyłanie sygnału za pomocą Signal):

_countdownEvent.Wait();
// jakas logika tutaj

Ponadto można przekazać timeout w milisekundach:

_countdownEvent.Wait(5000);

Z kolei metoda Reset ustawia CurrentCount do InitialCount:

_countdownEvent.Reset();

Aby przekonać się, że CountDownEvent używa SpinWait, sprawdźmy implementację Signal np. w Reflector:

public bool Signal(int signalCount)
{
    int num;
    if (signalCount <= 0)
    {
        throw new ArgumentOutOfRangeException("signalCount");
    }
    this.ThrowIfDisposed();
    SpinWait wait = new SpinWait();
Label_001D:
    num = this.m_currentCount;
    if (num < signalCount)
    {
        throw new InvalidOperationException(Environment.GetResourceString("CountdownEvent_Decrement_BelowZero"));
    }
    if (Interlocked.CompareExchange(ref this.m_currentCount, num - signalCount, num) != num)
    {
        wait.SpinOnce();
        goto Label_001D;
    }
    if (num == signalCount)
    {
        this.m_event.Set();
        return true;
    }
    return false;
}

Signal korzysta z klasy SpinWait, którą omówię w następnym poście ale mechanizm jest analogiczny do SpinLock opisanego w jednym z poprzednich postów – nie wymaga zmiany kontekstu.

Zasada działania CountdownEvent jest podobna do ManualResetEvent z tym, że mamy do dyspozycji licznik liczb całkowitych a nie zwykły boolean. Przykłady zastosowań są również podobne np. synchronizacja zdarzeń.  Podsumowując najważniejsze metody to Wait, AddCount, Signal oraz Reset. Wszystkie poza Wait zmniejszają lub zwiększają wewnętrzny licznik a  Wait blokuje wątek dopóki licznik nie osiągnie wartości zero.

Method group

Dziś krótki post o ciekawej konwersji, która ma miejsce podczas przekazywania delegat. Rozważmy kod:

List<int> numbers = new List<int>(new[] {3, 5, 464});
numbers.ForEach(n => Console.WriteLine(n));

ForEach to przykład metody, która jako parametr wejściowy przyjmuje wskaźnik na funkcję:

public void ForEach(Action<T> action)

Okazuje się, że powyższą linię można zapisać nieco prościej a mianowicie:

List<int> numbers = new List<int>(new[] {3, 5, 464});
numbers.ForEach(Console.WriteLine);

Na pierwszy rzut oka wydaje się to dziwne – WriteLine wygląda jak właściwość a nie metoda\delegata. Jak to działa? C# po prostu przeszukuje wszystkie przeładowania WriteLine i dopasowuje je do definicji delegaty. Foreach jako parametr przyjmuje typ Action<T>, zatem kompilator będzie starał się znaleźć odpowiednie przeładowanie WriteLine a następnie przekaże za nas parametry (w tym przypadku liczbę całkowitą).

Nie jest to żadna “specjalna konwersja”. Jeśli napiszemy własną metodę przyjmującą delegatę, podobnie będziemy mogli przekazywać po prostu nazwy metod. Nie jest to funkcjonalność ograniczona wyłącznie do ForEach.

Jak sprawa wygląda od strony dobrych praktyk? Niektórym może się nie spodobać, że wywołanie nie wygląda jak lambda ale jak przekazanie zwykłej właściwości. Moim zdaniem to nazwa powinna określać czy jest to właściwość czy metoda – właściwości nie mogą być czasownikami a metody z kolei muszą być (a dokładniej w trybie rozkazującym). Niestety gorzej sprawa wygląda z parametrami. Jeśli jest ich kilka warto opisać je za pomocą nazwy. Podanie samej metody nic nie mówi jakie parametry przekazujemy a to z kolei może ułatwić nam zrozumienie logiki. Wszystko zależy więc od konkretnego przykładu.

Semafor w c#

Dziś kolejny post o synchronizacji w c#. Semafor to bardzo popularna forma limitowania liczby wątków, które mogą mieć dostęp do danego kodu. Nie chcę omawiać tutaj podstaw semafora, ale myślę, że warto przypomnieć ogólną zasadę. Pseudokod (źródło Wikipedia):

procedure V (S : Semaphore);
 begin
   (* Operacja atomowa: inkrementacja semafora *)
   S := S + 1;
 end;
 
   (* Operacja atomowa: dekrementacja semafora *)
 procedure P (S : Semaphore);
 begin
   (* Cała operacja jest atomowa *)
   repeat
     Wait();
   until S > 0;
   S := S - 1;
 end;

Semafor składa się z dwóch metod. Jedna zwiększa pewien licznik, druga z kolei zmniejsza. Wejście do sekcji krytycznej jest możliwe tylko wtedy gdy licznik jest większy od zera.  Zatem jeśli na początku licznik ma wartość 1 i zostanie wywołana metoda P to licznik będzie miał wartość 0 a tym samym następnie wywołanie P będzie musiało poczekać. Innymi słowy, przed wejściem zmniejszamy licznik a a przy wyjściu z sekcji krytycznej  zwiększamy go z powrotem. Jeśli początkowa wartość licznika to 1 wtedy tylko jeden wątek może wejść do takiej sekcji. Semafor może dopuścić wiele wątków jednocześnie -  to od nas zależy początkowa wartość licznika.

W .NET semafor jest realizowany za pomocą jednej z dwóch klas. Pierwszą z nich jest Semaphore:

internal class Program
{
   private static int _counter;

   private static void Main(string[] args)
   {
       for (int i = 0; i < 10000; i++)
       {
           var task = new Task(IncreaseValue);
           task.Start();
       }
       Thread.Sleep(2000);
       Console.WriteLine(_counter);

   }
   private static readonly Semaphore _semaphore=new Semaphore(1,1);

   private static void IncreaseValue()
   {
       _semaphore.WaitOne();
       _counter++;
       _semaphore.Release();
   }
}

Za pomocą konstruktora można określić ile wątków chcemy dopuścić do danej sekcji ( w naszym przypadku jest to jeden wątek). WaitOne zmniejsza wspomniany licznik z kolei release zwiększa. Po szczegóły API odsyłam do MSDN. W poście chciałem jednak pokazać drugą klasę odpowiedzialną za semafor: SemaphoreSlim:

internal class Program
{
    private static int _counter;
    
    private static void Main(string[] args)
    {
        for (int i = 0; i < 10000; i++)
        {
            var task = new Task(IncreaseValue);
            task.Start();
        }
        Thread.Sleep(2000);
        Console.WriteLine(_counter);
    }
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    private static void IncreaseValue()
    {
        _semaphore.Wait();
        _counter++;
        _semaphore.Release();
    }
}

Jak widać, sposób użycia jest bardzo podobny. Istnieje jednak kilka bardzo ważnych szczegółów:

  1. Używaj SemaphoreSlim dla krótkich operacji.
  2. Semaphore może służyć do synchronizacji wątków miedzy procesami. Wtedy należy skorzystać z tzw. named semaphores.  W takiej sytuacji warto zwrócić szczególną uwagę na bezpieczeństwo aby złośliwe oprogramowanie nie spowodowało deadlock.
  3. SemaphoreSlim to odchudzona wersja przeznaczona do synchronizacji wątków znajdujących się w tym samym procesie. Semaphore z kolei to wrapper na systemowy, niezarządzany semafor, który jest potężniejszy ale znacząco wolniejszy.

Synchronizacja za pomocą SpinLock

W .NET istnieje wiele sposobów synchronizacji pracy wątków. O dużej części z nich pisałem już na blogu (z ciekawszych np. klasa Barrier ). Najpopularniejszym i najłatwiejszym sposobem jest użycie słowa kluczowego lock. W wielu przypadkach jest to najlepszy i najbezpieczniejszy wybór. SpinLock to zupełnie inne podejście.

W przypadku lock, wątek jest usypiany i budzony gdy przyjdzie na niego kolej. Ma to kilka poważnych wad. Wiążą się one z szeregowaniem oraz zmianą kontekstu. Zmiana kontekstu jest dość czasochłonna ponieważ należy zapisać stan CPU (rejestry itp.) Usypianie więc wątku jest dość skomplikowane ponieważ należy dokonać pewnego rodzaju serializacji.

SpinLock działa zupełnie inaczej – wątek nigdy nie jest usypiany. Po prostu wątek działa i odpytuje czy może już uzyskać dostęp. Jeśli tak to od razu wykonuje operacje bez zbędnego szeregowania czy zmiany kontekstu. SpinLock to bardzo prosty algorytm, wykonujący pętle i sprawdzający jakąś flagę. W pseudokodzie można to zapisać następująco:


while (IsLockAlreadyTaken)
{
    // do nothing        
}

 

Rozważmy więc klasyczny problem zwiększania pewnej współdzielonej zmiennej:

internal class Program
{
private static int _counter;
private static SpinLock _spinLock=new SpinLock();

private static void Main(string[] args)
{
  for (int i = 0; i < 10000; i++)
  {
      var task = new Task(IncreaseValue);
      task.Start();
  }
  Thread.Sleep(7000);
  Console.WriteLine(_counter);
}
private static void IncreaseValue()
{
  bool lockTaken = false;

  try
  {
      _spinLock.Enter(ref lockTaken);
      _counter++;
  }
  finally
  {          
      if (lockTaken)
          _spinLock.Exit();
  }
 }
}

Korzystanie z SpinLock jest łatwe, wystarczy wywołać metodę Enter a potem Exit.  Należy jednak zwrócić uwagę na kilka niesłychanie ważnych szczegółów:

  1. SpinLock to struktura a nie klasa! Z tego względu jeśli chcemy przekazać jako parametr musimy zrobić to przez referencję bo inaczej będą to całkowicie niezależne Spin’y.
  2. Przed wywołaniem metody SpinLock.Enter flaga musi być ustawiona na false.
  3. Spinlock nie jest “reentrant” czyli nie można wywołać metody Enter dwa razy. W przypadku gdy jest ustawiona właściwość  IsThreadOwnerTrackingEnabled zostanie wyrzucony wyjątek a w przeciwnym wypadku spowoduje to zakleszczenie.

Jakie są wady SpinLock? Największa to fakt, że wątek wciąż zużywa zasoby,wykonuje cykle. Dla operacji krótkotrwałych jest to lepsze niż zmiana kontekstu. W przypadku długich operacji zasoby systemowe są niepotrzebnie zużywane. Należy pamiętać, aby korzystać tylko z SpinLock dla prostych operacji, których wykonanie zajmie kilka cykli CPU – wtedy lepiej poczekać niż zmieniać kontekst. Jeszcze gorszy scenariusz, co w przypadku gdy czekamy na wątek, który został uśpiony ponieważ CPU miał coś ważniejszego do wykonania (interruption)? Im dłuższa operacja tym większe ryzyko. SpinLock jest odradzany dla procesorów jednordzeniowych gdzie trzymanie wątków nic nie robiących jest dużo bardziej kosztowne.

Klasa Tuple

W .NET 4.0 wprowadzoną klasę Tuple służącą do owijania kilku wartości w jeden obiekt. Tuple to nic innego jak obiekt zawierający w sobie jakieś dane w postaci właściwości. Do dyspozycji jest 8 statycznych  metod służących do stworzenia Tuple:

  • Create(T1)
  • Create(T1,T2)
  • Create(T1,T2,T3)
  • Create(T1,T2,T3,T4)
  • Create(T1,T2,T3,T4,T5)
  • Create(T1,T2,T3,T4,T5,T6)
  • Create(T1,T2,T3,T4,T5,T6,T7)
  • Create(T1,T2,T3,T4,T5,T6,T7,T8)

Ponadto istnieje możliwość stworzenia tuple za pomocą jednego z konstruktorów:

  • Tuple<T1>
  • Tuple<T1,T2>
  • Tuple<T1,T2,T3>
  • Tuple<T1,T2,T3,T4>
  • Tuple<T1,T2,T3,T4,T5>
  • Tuple<T1,T2,T3,T4,T5,T6>
  • Tuple<T1,T2,T3,T4,T5,T6,T7>
  • Tuple<T1,T2,T3,T4,T5,T6,T7,TRest>

Załóżmy, że chcemy stworzyć obiekt zawierający dwa pola: jedno string drugie int:

Tuple<string, int> tuple = Tuple.Create("Piotr", 26);
string firstName = tuple.Item1;
int age = tuple.Item2;

Należy zaznaczyć, że Tuple jest obiektem immutable i zmiana pól po utworzeniu jest już niemożliwa:

Tuple<string, int> tuple = Tuple.Create("Piotr", 26);
tuple.Item1 = "Piotr"; // niemozliwe - readonly
tuple.Item2 = 30; // niemozliwe - readonly

Oczywiście nic nie szkodzi na przeszkodzie aby stworzyć zagnieżdżone obiekty:

var tuple = Tuple.Create(Tuple.Create("Piotr", "Zielinski"), 26);
string firstName = tuple.Item1.Item1;
string lastName = tuple.Item1.Item2;
int age = tuple.Item2;

Korzystanie z metody statycznej Create myślę, że jest już jasne. Jak jednak wspomniałem, istnieje możliwość tworzenia tuple za pomocą konstruktorów:

var tuple = new Tuple<string, string>("Piotr", "Zielinski");
string firstName = tuple.Item1;
string lastName = tuple.Item2;

Najbardziej interesującym konstruktorem jest “Tuple<T1,T2,T3,T4,T5,T6,T7,TRest>”, który pozwala na zdefiniowanie więcej niż 8 wartości:

var tuple = new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>(1, 2, 3, 4, 5, 6, 7,
                                                                                      new Tuple<int, int>(8, 9));
int item1 = tuple.Item1;
int item7 = tuple.Rest.Item1;
int item8 = tuple.Rest.Item2; 

Tuple to nie kolekcja – nie ma enumeratora. Kiedy warto tego używać? Moim zdaniem nigdy! Część programistów używa jednak tuple w wewnętrznych klasa np. przekazując parametry do wątku. Wszyscy natomiast odradzają eksponowania Tuple jako publiczna właściwość czy wartość zwracana przez funkcje. Osobiście nie używam tego tworu nawet w wewnętrznych klasach jako tymczasowe zmienne. Preferuje używać własne klasy, które zawierają właściwości z nazwami odzwierciedlającymi prawdziwe przeznaczenie. Wyjątkiem są aplikacje tymczasowe np. na szybko tworzone prototypy – uważam, że prototyp przede wszystkim musi zostać zaimplementowany szybko bo od niego zależy wiele ważnych decyzji.

Zasada Command-query separation (CQS)

Dziś znów powrót do podstaw inżynierii oprogramowania. Przedstawianie podstawowej zasady może wydawać się śmieszne ale mimo wszystko programista dobrze jak wie, że taka zasada ma swoją nazwę i naprawdę powinno się tego przestrzegać.  Za pewne wiele programistów nie zna nazw tych reguł ale i tak postępuje zgodnie z nimi. Post ma jednak uświadomić, że takie praktyki są dobrze udokumentowane i są na naprawdę dobrym zwyczajem a nie tylko intuicją doświadczonego programisty.

Wzorzec dotyczy konstrukcji metod – każda metoda powinna być “komendą (command)” lub “zapytaniem (query)” ale nigdy jednocześnie i komendą i zapytaniem. Pozostaje wyjaśnić czym się różnią te dwa twory? Komenda to polecenie zrobienia czegoś – np. zapisu danych do bazy danych. Zapytanie to zwrócenie danych. Innymi słowy, nie powinniśmy tworzyć metod, które zarówno wykonują jakaś logikę, zmieniając tym samym stan obiektu, bazy danych jak i które zwracają na końcu dane. Według CQS lepszym podejściem jest rozdzielenie tego na dwie metody.

Uważam, że zasada bardzo dobra ale czasami jest wręcz anty-wzorcem. Pierwszy przykład to programowanie współbieżne, gdzie należy unikać częstych lock’ow i z tego względu lepiej skorzystać z bardziej rozbudowanej ale jednej metody. Drugi przykład to warstwa usług, która eksponuje warstwę biznesową.  Fasada sama z definicji gromadzi szeroką funkcjonalność w jednej metodzie. Tworząc usługę sieciową lepiej aby jedno zapytanie zrobiło tyle co trzeba zamiast wysyłać kilka pojedynczych zapytań – interfejs warstwy usług nie powinien być “chatty”. Operacja na stosie POP (zdjęcie obiektu) jest klasycznym przykładem łamiącym CQS ale mimo wszystko jest to dobre podejście.

CQS przede wszystkim daje przejrzyste API – wiadomo, które metody zmieniają stan obiektu i prawdopodobnie kilkakrotne ich wywołanie może spowodować jakieś efekty uboczne.

Visual Studio 11 – kompatybilność wstecz oraz Solution Explorer

W dzisiejszym poście o dwóch nowościach. Pierwsza z nich to ulepszona kompatybilność. W VS 11 można otwierać solucje utworzone w Visual Studio 2010 bez znanego “Upgrade”. Oznacza to, że można jednocześnie na tym samym projekcie pracować zarówno w VS 11 jak i VS 2010. Projekt otworzony w VS 11 nie powoduje zmiany  formatu i później wciąż może być otwierany w VS 2010. Moim zdaniem znaczącą ułatwia to migrację. Niestety projekt utworzony od nowa w VS 11 nie może być już otwarty w 2010 ale wydaje się to naturalne.

Druga sprawa to ulepszony Solution Explorer. Screen:

image

 

Jak widać oprócz nazw plików mamy również listę klas oraz metod, właściwości itp. Klikając na którąś z pozycji otrzymujemy następujące menu:

image

Menu umożliwia sprawdzenie gdzie funkcja jest wywoływana, używana itp. Dla klas ponadto można sprawdzić hierarchię:

image

Visual Studio 11– QuickLaunch

W ostatnim poście o nowościach w VS 11 pisałem o szarych ikonach, co w większości osobom nie przypadło do gustu. Z tego co obserwuję, zdecydowana większość jest rozczarowana nowymi ikonami. Dzisiaj jednak chciałbym zaprezentować praktyczniejszą funkcję – nowy QuickLaunch. Na początek PrintScren:

image

Chodzi mi oczywiście o QuickLaunch znajdujący się w prawym górnym rogu. Wpisując jakiś tekst, zostanie przeszukanych kilka typów informacji jak:

  1. Otwarte dokumenty (przeszukuje po ich nazwie).
  2. Item’y w menu – bardzo przydatne gdy nie wiemy gdzie dokładnie jest coś w menu a pamiętamy tylko nazwę.
  3. Ostatnio używane operacje.
  4. Opcje w menu. Aby szybko przejść do opcji o nazwie VB Specific wystarczy:

image

Ponadto można ustawiać filtry. Jeśli chcemy przeszukać wyłącznie ostatnio wykonywane operacje wtedy zapytanie poprzedzamy @mru, otwarte dokumenty – @doc, item’y w menu – @menu, z kolei opcje – @opt.

image

Aby przełączyć się np. z edytora kodu do QuickLaunch wystarczy wcisnąć CTRL+Q a następnie możemy już wpisywać nasze zapytanie. Moim zdaniem przydatne gdy nie odrywając rąk od klawiatury chcemy wykonać jakaś operację a w końcu każdy programista VS powinien unikać używania myszki.