Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Globalizacja aplikacji i wątki

Natknąłem się na pewne zachowanie .Net Frameworka, które było zupełnie odmienne od tego, którego się spodziewałem. Problem dotyczy globalizacji i wątków. Okazuje się, że mając aplikację, która jest zlokalizowana na wiele języków musimy zwrócić szczególną uwagę za każdym razem gdy korzystamy z wątków.

Ustawienia dotyczące kultury są właściwościami wątku. W systemie Windows przy starcie wątku ustawienia kultury pobierane są z ustawień systemowych. Zatem jeżeli uruchamiamy aplikację, jej watek otrzymuje ustawienia użytkownika. Działanie to jest jak najbardziej oczekiwane. Okazuje się jednak, że tworzone w aplikacji kolejne watki otrzymują ustawienia kulturowe w taki sam sposób jak wątek pierwszy. Może nie było by w tym nic złego, ale osobiście oczekiwałem zachowania odwrotnego – czyli że nowy wątek otrzyma takie same ustawienia jak wątek główny. Natomiast gdy w międzyczasie zmienimy ustawienia kulturowe pierwszego wątku to pojawiają się dodatkowe problemy.

Wyobraźmy sobie aplikację, która przed uruchomieniem wyświetla okno, w którym możemy wybrać język w jakim chcemy, żeby się uruchomiła. Wybierając język inny niż ten, który znajduje się w ustawieniach systemu, będziemy działać swobodnie dopóki nie aplikacja nie zacznie korzystać z wątków.

Ja, będąc niedoświadczonym programistą, założyłem, że skoro aplikacja ma określone ustawienia kulturowe w pierwszym wątku, to tworząc nowe wątki ustawienia te zostaną zachowane (skopiowane z wątku, który tworzy nowe watki). Okazuje się jednak, że CLR nie zawiera żadnego mechanizmu kontrolowania ustawień kulturowych wątków tworzonych w danym procesie. Dlatego każdy tworzony wątek będzie miał ustawienia te pobrane z systemu Windows.

Dobrze, skoro już wiemy o tych niuansach, to trzeba się do tego przystosować. Jeżeli ręcznie tworzymy wątki to nie ma z tym żadnego problemu. Klasa Thread ma odpowiednie właściwości, dzięki którym możemy ustawienia kulturowe ustawić według własnego uznania. Ja jednak korzystam zwykle z puli wątków (klasa ThreadPool). Jak się szybko okazało klasa ta nie ma żadnego bezpośredniego wsparcia dla uruchamiania wątków z innymi ustawieniami kulturowymi. A szkoda. Skoro reguły są jasne, to powinniśmy chociaż dostać jakieś przeciążenie metody QueueUserWorkItem pozwalające określić ustawienia kulturowe uruchamianego wątku. A tak trzeba się tym ręcznie bawić i określać ustawienia kulturowe już w metodzie, uruchomionej w osobnym wątku, co moim zdaniem tylko wprowadza niepotrzebne zamieszanie w kodzie. Uważam, że można było tego uniknąć udostępniając odpowiednie API.

Podsumowując okazuje się, że pracując z aplikacją, która ma być lokalizowana na inne języki musimy szczególną uwagę zwrócić na obsługę wątków i odpowiednio ustawiać im właściwości kulturowe. Niemniej jednak zachowanie to jest dla mnie zgoła nie intuicyjne. Bo przecież zwykle chcemy, aby cała aplikacja działała z tymi samymi ustawieniami kulturowymi, a przypadki odwrotne są raczej sporadyczne. A może jest jakieś dobre uzasadnienie tego stanu rzeczy?

Opublikowane 2 kwietnia 2008 14:23 przez nuwanda
Filed under:

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

# re: Globalizacja aplikacji i wątki

2 kwietnia 2008 14:55 by dario-g

Uzasadnienia nie znam, ale właśnie pracuję nad częścią aplikacji wielojęzycznej i wątkami :)

Dzięki za tip :)

# re: Globalizacja aplikacji i wątki

2 kwietnia 2008 15:10 by nuwanda

Bardzo proszę ;). Mam nadzieję, że się przyda.

# re: Globalizacja aplikacji i wątki

2 kwietnia 2008 17:04 by apl

Z drugiej strony wyobraźmy sobie sytuację, w której wątek przy tworzeniu byłby inicjalizowany ustawieniami wątku, powiedzmy, "rodzica" (co jest lekkim semantycznym nadużyciem, gdyż wszystkie wątki w CLR są równorzędne). Problem z pulą wątków byłby nadal nierozwiązany, gdyż mogłaby ona utrzymywać przy życiu wątki utworzone wcześniej, nawet z poziomu innych wątków, i delegować je do wykonania nowych zadań.

Co do rozwiązania problemu, proponuję następujące podejście:

public static class ThreadExtensions

{

  public static WaitCallback CreateCallback(this Thread thread, WaitCallback callback)

  {

     return CreateCallback(thread, callback, thread.CurrentCulture, thread.CurrentUICulture);

  }

  public static WaitCallback CreateCallback(this Thread thread, WaitCallback callback, CultureInfo culture, CultureInfo uiCulture)

  {

     if (thread == null) {

        throw new ArgumentNullException("thread");

     }

     if (callback == null) {

        throw new ArgumentNullException("callback");

     }

     return (object state) =>

     {

        CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture;

        CultureInfo originalUICulture = Thread.CurrentThread.CurrentUICulture;

        SetThreadCulture(culture, uiCulture);

        callback(state);

        SetThreadCulture(originalCulture, originalUICulture);

     };

  }

  private static void SetThreadCulture(CultureInfo culture, CultureInfo uiCulture)

  {

     Thread.CurrentThread.CurrentCulture = culture;

     Thread.CurrentThread.CurrentUICulture = uiCulture;

  }

}

Zadanie do wykonania w wątku z puli możemy teraz zlecić w ten sposób:

private void Action(object state)

{

  // Zrób coś.

  ...

}

...

ThreadPool.QueueUserWorkItem(Thread.CurrentThread.CreateCallback(Action));

# re: Globalizacja aplikacji i wątki

2 kwietnia 2008 17:29 by nuwanda

Twoje rozwiązanie z metodami rozszerzającymi bardzo mi się podoba. Muszę przyznać, że nie pomyślałem o tym, ale problem, o którym pisałem dotknął mnie w pracy, a tam pracuję z C# 2.0. Co do puli wątków to oczywiście chodziło mi o to, by przy pobieraniu wątku z puli można było ustawić mu właściwości dotyczące kultury, a nie żeby ona automatycznie nadawała te ustawienia. Podobne rozwiązanie przedstawia Twoja metoda rozszerzająca.

Z drugiej strony czy możemy dodawać metody rozszerzające o takich samych nazwach jak te już istniejące tylko z inną listą parametrów? Jeżeli tak to twoje rozwiązanie można by było przenieść do klasy ThreadPool i zrobić tak: bool QueueUserWorkItem(WaitCallback callback, object state,  CultureInfo culture, CultureInfo uiCulture). Tego właśnie bym oczekiwał.

# re: Globalizacja aplikacji i wątki

2 kwietnia 2008 18:13 by apl

W C# 2.0 można z powodzeniem użyć tego samego podejścia, rezygnując wyłącznie ze składniowych smaczków:

public static class WaitCallbackUtility

{

  public static WaitCallback CreateCallback(Thread thread, WaitCallback callback)

  {

     if (thread == null) {

        throw new ArgumentNullException("thread");

     }

     return CreateCallback(thread, callback, thread.CurrentCulture, thread.CurrentUICulture);

  }

  public static WaitCallback CreateCallback(WaitCallback callback, CultureInfo culture, CultureInfo uiCulture)

  {

     if (callback == null) {

        throw new ArgumentNullException("callback");

     }

     return delegate (object state)

     {

        CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture;

        CultureInfo originalUICulture = Thread.CurrentThread.CurrentUICulture;

        SetThreadCulture(culture, uiCulture);

        callback(state);

        SetThreadCulture(originalCulture, originalUICulture);

     };

  }

  private static void SetThreadCulture(CultureInfo culture, CultureInfo uiCulture)

  {

     Thread.CurrentThread.CurrentCulture = culture;

     Thread.CurrentThread.CurrentUICulture = uiCulture;

  }

}

...

private void Action(object state)

{

  // Zrób coś.

  ...

}

...

ThreadPool.QueueUserWorkItem(WaitCallbackUtility.CreateCallback(Thread.CurrentThread, Action));

Metody rozszerzające oczywiście można przeciążać, lecz klasa ThreadPool jest statyczna, więc nie bardzo możemy wykorzystać ten mechanizm.

# re: Globalizacja aplikacji i wątki

2 kwietnia 2008 22:16 by Wojciech Gebczyk

Z tego co pamietam to nie wszystkie watki sa rownorzedne bo dziela sie przynajmniej ba glowne i poboczne/tla (Foreground, Background). I ich obsluga jest czasami inna.

Nie wiedzialem o takim zachowaniu, myslalem ze glowny/poczatkowy/pierwszy watek rzeczywiscie przekaze CultureInfo. Ale zerkajac reflectorem okazje sie ze jak sie nie ustawi to ustanienia "defaultowe" sa brane...

# re: Globalizacja aplikacji i wątki

3 kwietnia 2008 13:13 by apl

Mówisz o klasyfikacji wątków, mi zaś chodziło o relacje między nimi. Konkretnie miałem na myśli to, że niezależnie w jakich warunkach tworzone są wątki, nie ma między nimi żadnej hierarchicznej zależności typu rodzic-dziecko.

Jeszcze jeden haczyk:

class Test

{

static void ShowCulture()

{

Console.WriteLine("Thread ID={0}; Culture={1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.CurrentCulture.Name);

}

static void Main()

{

ThreadStart showCulture = ShowCulture;

Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");

Console.WriteLine("Main Thread ID=" + Thread.CurrentThread.ManagedThreadId);

showCulture.Invoke();

showCulture.EndInvoke(showCulture.BeginInvoke(null, null));

}

}

Przykład pokazuje, że zależnie od tego, jak wywołaliśmy delegat - synchronicznie czy asynchronicznie - w ciele metody otrzymamy inne ustawienia kulturowe. Zachowanie takie wynika z tego, że pod maską BeginInvoke korzysta z puli wątków. Jest to o tyle kłopotliwe, że mechanizm asynchronicznych delegatów często wykorzystywany jest w .NET niejawnie, np. przez komponent BackgroundWorker.

# re: Globalizacja aplikacji i wątki

3 kwietnia 2008 13:17 by apl

Przepraszam za rozjechany kod, ale w edytorze komentarzy naprawdę ciężko nad tym zapanować. Jeszcze raz:

class Test

{

  static void ShowCulture()

  {

     Console.WriteLine("Thread ID={0}; Culture={1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.CurrentCulture.Name);

  }

  static void Main()

  {

     ThreadStart showCulture = ShowCulture;

     Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");

     Console.WriteLine("Main Thread ID=" + Thread.CurrentThread.ManagedThreadId);

     showCulture.Invoke();

     showCulture.EndInvoke(showCulture.BeginInvoke(null, null));

  }

}

Co o tym myślisz?

(wymagane) 
wymagane 
(wymagane) 

  
Wprowadź kod: (wymagane)