LockSłowo kluczowe “lock” oznacza część krytyczną kodu, która zablokuje przepływ kodu dla innych wątku ,aż do jego zwolnienia.

Wewnątrz części krytycznej zabezpieczam kod, który mógłby zachowywać się nie przewidywalnie, gdyby wiele wątków naraz próbowało wykonać jedną i tą samą operację.

Do prezentacji tego problemu będziemy potrzebować wątków działający asynchronicznie. W tym celu postanowiłem użyć Task-ów bez nowości z C# 5.0 i .NET 4.5.

Oto kod aplikacji konsolowej.

Mam listę sześciu taksów, które wykonają asynchronicznie statyczną metodę “SaveLockMessage”. W pętli po kolej będę uruchamiał te zadanie. Gdy wszystkie zadania się skończą konsola wyświetli napis “Skończone…”.

static void Main(string[] args)
{
    List<Task> list = new List<Task>();
    ReadClass2.HowMuchWillBeDone = 6;
    list.Add(new Task(ReadClass.SaveLockMessage));
    list.Add(new Task(ReadClass.SaveLockMessage));
    list.Add(new Task(ReadClass.SaveLockMessage));
    list.Add(new Task(ReadClass.SaveLockMessage));
    list.Add(new Task(ReadClass.SaveLockMessage));
    list.Add(new Task(ReadClass.SaveLockMessage));

    foreach (var task in list)
    {
        task.Start();
    }

    while (!ReadClass.CheckIsDone())
    { }

    Console.WriteLine("\n Skończone...");
            
    Console.ReadKey();
}

Co robi metoda “SaveLockMessage”. Zapisuje ona do pliku datę i godzinę ,a po jednej sekundzie zapisuje ona numer wykonywanego zadania.

Jak widać jest tutaj zastosowane wyrażenie “lock”.

Zmienna“SyncObject” służy tylko jako klucz do blokady i nie spełnia żadnego innego celu.

public class ReadClass
{
    private static readonly object SyncObject = new object();
    private static TextWriter W;

    public static void SaveLockMessage()
    {
        Console.WriteLine("Przed Lockiem");

        lock (SyncObject)
        {
            i++;
            W = new StreamWriter("log.txt", true);
            Console.WriteLine("\t Start zapisu do pliku: " + i);
            W.WriteLine("Otrzymana: {0} {1}", 
		DateTime.Now.ToLongTimeString(),
		DateTime.Now.ToLongDateString());
            Thread.Sleep(1000);
            
            Console.WriteLine("\t Skończyłem: " + i);
            W.Close();
        }
    }

    private static int i = 0;

    public static int HowMuchWillBeDone = 6;

    public static bool CheckIsDone()
    {
        return i >= HowMuchWillBeDone;
    }
}

Jak ona działa w praktyce? O to wydruk konsoli.

Lock C Sharp

Jak widać 4 z 6 zadań na początku napisało w konsoli napis “Przed Lockiem”. Jedno z tych zadań przeszło do zapisu pliku. W tym samym czasie pozostał 2 taski wydrukowały napis “Przed Lockiem”.

Ze względu na naturę asynchroniczności cały ten proces może wyglądać za każdym razem inaczej. Tym razem postanowiłem też zmienić napis “Przed Lockiem” na “Przed Lock” bo brzmiało to moim zdaniem zbyt głupio.

Lock C Sharp Another Example

Jak widać tylko jedno asynchroniczne zadnie przeszło do zapisu pliku pozostałe zadania czekają ,aż on odblokuję tą część kodu.

Gdy to się stanie następny “losowy” task wykonuje ten blok kodu ,a reszta na niego czeka.

Cały proces trwa do momentu wykonania wszystkich tasków.

Treść zapisana do pliku wygląda tak:

Otrzymana: 20:17:16 3 kwietnia 2013
Otrzymana: 20:17:17 3 kwietnia 2013
Otrzymana: 20:17:18 3 kwietnia 2013
Otrzymana: 20:17:19 3 kwietnia 2013
Otrzymana: 20:17:20 3 kwietnia 2013
Otrzymana: 20:17:21 3 kwietnia 2013

Wszystko wydaje się w porządku. Co jednak się stanie, gdy wiele zdań spróbuje otworzyć plik?

public static void SendLockMessage2()
{
    W = new StreamWriter("log.txt", true);
    W.WriteLine("Otrzymana: {0} {1}",
	DateTime.Now.ToLongTimeString(), 
	DateTime.Now.ToLongDateString());
    Thread.Sleep(1000);
    i++;
    W.Flush();
    W.Close();
}

Wyskoczy oczywiście wyjątek informujący nas o tym ,że plik nie może być otwarty przez inny proces, jeśli jest on już otwarty. Wyrażenie “lock” jest więc w pewnym sensie użyteczne.

exception

A gdyby tak otworzyć plik raz i poczekać na zadania by zapisały zawartość asynchronicznie.

public class ReadClass2
{
    private static readonly object SyncObject = new object();
    private static TextWriter W = new StreamWriter("log.txt", true);

    public static void SaveLockMessage()
    {
        i++;
        Console.WriteLine("\t Start zapisu do pliku: " + i);
        W.WriteLine("Otrzymana: {0} {1}",
	DateTime.Now.ToLongTimeString(), 
	DateTime.Now.ToLongDateString());
        Thread.Sleep(1000);
        W.Flush();
        Console.WriteLine("\t Skończyłem: " + i);
    }

    private static int i = 0;
}

Tym razem nie wyskoczy nam wyjątek jednak nie możemy mieć żadnej pewności o tym jak treści zostaną zapisana do pliku.

without sync

Zapis jednej linii jest atomowy więc dużego dramatu tutaj nie ma. Poza brakiem kolejności w zapisach nie widać tutaj żadnych błędów.

Otrzymana: 21:12:08 3 kwietnia 2013
Otrzymana: 21:12:07 3 kwietnia 2013
Otrzymana: 21:12:07 3 kwietnia 2013
Otrzymana: 21:12:07 3 kwietnia 2013
Otrzymana: 21:12:07 3 kwietnia 2013
Otrzymana: 21:12:07 3 kwietnia 2013
Otrzymana: 21:12:08 3 kwietnia 2013

Przy dużej liczbie zadań widać jednak ,że zadania nachodzą na siebie i czasem spowalniają się nawzajem (magia procesora) co przy nie atomowych operacjach doprowadziło by do błędów aplikacji,

Na koniec przerobiłem też kod by pokazać jeszcze inny problem związany z błędnym użycie lock-a.

Tym razem do metody “SendLockMessage” przekazuje parametr “k”. Ten parametr zostanie wydrukowany do pliku.

Jak myślisz jaka wartość parametru “k” zostanie wydrukowana.

Otrzymana: 21:25:22 3 kwietnia 2013
Numer zadania: 6
Otrzymana: 21:25:38 3 kwietnia 2013
Numer zadania: 6
Otrzymana: 21:25:39 3 kwietnia 2013
Numer zadania: 6
Otrzymana: 21:25:40 3 kwietnia 2013
Numer zadania: 6
Otrzymana: 21:25:41 3 kwietnia 2013
Numer zadania: 6
Otrzymana: 21:25:42 3 kwietnia 2013

Zadania niby stoją ,ale parametr “k” ma wartość 6, ponieważ nie została ona zabezpieczona przed wielowątkowością.