Bir konsol uygulamasının 'Ana' yönteminde 'zaman uyumsuz' değiştirici belirtilemiyor


445

asyncDeğiştirici ile eşzamansız programlamada yeniyim. MainBir konsol uygulaması yöntemimin asenkron olarak çalıştığından nasıl emin olacağımı anlamaya çalışıyorum .

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = bs.GetList();
    }
}

public class Bootstrapper {

    public async Task<List<TvChannel>> GetList()
    {
        GetPrograms pro = new GetPrograms();

        return await pro.DownloadTvChannels();
    }
}

Bunun "üstten" senkronize olmayan bir şekilde çalışmadığını biliyorum. Yöntemde asyncdeğiştiriciyi belirtmek mümkün olmadığından Main, kodu maineşzamansız olarak nasıl çalıştırabilirim ?


23
Bu artık C # 7.1'de geçerli değildir. Ana yöntemler
asenkron

2
İşte C # 7.1 blog yazısı duyurusu . Async Main başlıklı bölüme bakın .
styfle

Yanıtlar:


382

Bildiğiniz gibi, VS11'de derleyici bir async Mainyönteme izin vermeyecektir . Bu, Async CTP ile VS2010'da izin verildi (ancak asla tavsiye edilmedi).

Özellikle async / await ve asenkron konsol programları ile ilgili son blog yayınlarım var . Giriş yazısından bazı arka plan bilgileri:

"Bekliyor", beklemenin tamamlanmadığını görürse, eşzamansız olarak hareket eder. Tamamlandığında yöntemin geri kalanını çalıştırmayı bekler ve ardından zaman uyumsuz yöntemden döner . Await, yöntemin geri kalanını beklenebilir duruma geçirdiğinde mevcut bağlamı da yakalayacaktır .

Daha sonra, beklenen işlem tamamlandığında, async yönteminin geri kalanını (yakalanan bağlam dahilinde) yürütür.

Konsol programlarında aşağıdakilere neden olan bir sorundur async Main:

Giriş yazımızdan, bir zaman uyumsuz yöntemin tamamlanmadan önce arayanına döneceğini unutmayın . Bu, UI uygulamalarında (yöntem yalnızca UI olay döngüsüne geri döner) ve ASP.NET uygulamalarında (yöntem iş parçacığını döndürür ancak isteği canlı tutar) mükemmel çalışır. Konsol programları için pek işe yaramaz: Ana işletim sistemine döner - böylece programınız kapanır.

Çözümlerden biri, kendi içeriğinizi sağlamaktır - konsol programınız için zaman uyumsuz uyumlu bir "ana döngü".

Eğer zaman uyumsuz CTP ile bir makine varsa, kullanabilirsiniz GeneralThreadAffineContextgelen Belgelerim \ Microsoft Visual Studio Async CTP \ Numuneler (C # Test) Birim Testi \ AsyncTestUtilities . Alternatif olarak, kullanabilirsiniz AsyncContextdan benim Nito.AsyncEx Nuget paketinde .

İşte bir örnek AsyncContext; GeneralThreadAffineContextneredeyse aynı kullanıma sahiptir:

using Nito.AsyncEx;
class Program
{
    static void Main(string[] args)
    {
        AsyncContext.Run(() => MainAsync(args));
    }

    static async void MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

Alternatif olarak, zaman uyumsuz çalışmanız tamamlanana kadar ana Konsol iş parçacığını engelleyebilirsiniz:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }

    static async Task MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

Kullanımı GetAwaiter().GetResult(); bu, veya AggregateExceptionkullanırsanız gerçekleşen sargıyı önler .Wait()Result

Güncelleme, 2017/11/30: Visual Studio 2017 Güncelleme 3 (15.3) itibariyle dil artık destekler async Main- sürece döner olarak Taskveya Task<T>. Şimdi bunu yapabilirsiniz:

class Program
{
    static async Task Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

Anlambilim GetAwaiter().GetResult(), ana iş parçacığını engelleme stiliyle aynı görünmektedir . Bununla birlikte, C # 7.1 için henüz bir dil spesifikasyonu yoktur, bu yüzden bu sadece bir varsayımdır.


30
Sen edebilir bir basit kullanabilir Waitveya Resultve hiçbir şey yanlış bununla var. Ancak iki önemli farkın olduğunu unutmayın: 1) tüm asyncdevamlar ana iş parçacığı yerine iş parçacığı havuzunda çalışır ve 2) tüm istisnalar bir AggregateException.
Stephen Cleary

2
Şimdiye kadar (ve blog yayınınız) bunu çözerken gerçek bir sorun yaşıyordum. Bu, bu sorunu çözmek için en kolay yöntemdir ve paketi nuget konsoluna sadece bir "install-package Nito.Asyncex" ile yükleyebilirsiniz ve işiniz bitti.
ConstantineK

1
@StephenCleary: Hızlı yanıt için teşekkürler Stephen. Herkes neden anlamıyorum olmaz bir istisnası atılır zaman ayıklayıcı kırmak istiyorum. Hata ayıklama ve bir null başvuru özel durumu üzerinde çalıştırmak, doğrudan kod rahatsız edici satırına gitmek tercih gibi görünüyor. VS eşzamanlı kod için "kutunun dışında" gibi çalışır, ancak async / await için çalışmaz.
Greg

6
C # 7.1 şimdi bir async ana var, büyük cevabınıza eklemeye değer olabilir, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii

3
VS 2017'de C # 7.1 sürümünü kullanıyorsanız, projenin burada gösterildiği<LangVersion>latest</LangVersion> gibi csproj dosyasına ekleyerek dilin en son sürümünü kullanacak şekilde yapılandırıldığından emin olmalıydım .
Liam

359

Bunu bu basit yapı ile çözebilirsiniz:

class Program
{
    static void Main(string[] args)
    {
        Task.Run(async () =>
        {
            // Do any async anything you need here without worry
        }).GetAwaiter().GetResult();
    }
}

Bu, ThreadPool'a yaptığınız her şeyi istediğiniz yere koyacaktır (böylece başlattığınız / beklediğiniz diğer Görevler, yapmamaları gereken bir Konuya yeniden katılmaya çalışmaz) ve Konsol uygulamasını kapatmadan önce her şeyin bitmesini bekleyin. Özel döngülere veya dışarıdaki kütüphanelere gerek yoktur.

Düzenleme: Yakalanmamış İstisnalar için Andrew çözümünü dahil edin.


3
Bu yaklaşım çok açıktır, ancak istisnaları sarmaya eğilimlidir, bu yüzden şimdi daha iyi bir yol arıyorum.
abatishchev

2
@abatishchev Kodunuzda, en azından Görev'in içinde try / catch kullanmalısınız. Daha ayrıntılı değilse, İstisnaların Göreve akmasına izin vermeyin. Başarısız olabilecek şeylerin etrafına deneme / yakalama yaparak sarma sorununu önleyeceksiniz.
Chris Moschini

54
Eğer değiştirirseniz Wait()ile GetAwaiter().GetResult()size kaçınmak gerekir AggregateExceptionşeyler atmak sarmalayıcı.
Andrew Arnott

7
Bu async mainyazıdan itibaren C # 7.1'de bu şekilde tanıtıldı.
user9993

@ user9993 Bu teklife göre, bu tam olarak doğru değil.
Sinjai

90

Bunu, harici kütüphanelere ihtiyaç duymadan, aşağıdakileri yaparak da yapabilirsiniz:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var getListTask = bs.GetList(); // returns the Task<List<TvChannel>>

        Task.WaitAll(getListTask); // block while the task completes

        var list = getListTask.Result;
    }
}

7
Akılda Ayı getListTask.Resultzamanda bir engelleme çağrıdır ve bu nedenle yukarıdaki kod olmadan yazılmış olabilir Task.WaitAll(getListTask).
do0g

27
Ayrıca, GetListatarsa , AggregateExceptionatılan gerçek istisnayı belirlemek için bir istisnayı yakalamanız ve sorgulamanız gerekir. Ancak, çağırabilir GetAwaiter()almak TaskAwaiteriçin Taskve çağrı GetResult(), yani bu konuda var list = getListTask.GetAwaiter().GetResult();. TaskAwaiter(Aynı zamanda bir engelleme çağrısından) sonuç alındığında, atılan herhangi bir istisna bir AggregateException.
do0g

1
GetGult, ihtiyacım olan cevaptı. Bu, yapmaya çalıştığım şey için mükemmel çalışıyor. Muhtemelen bunu başka yerlerde de kullanacağım.
Deathstalker

78

C # 7.1'de uygun bir async Main yapabileceksiniz . Yöntem için uygun imzalar aşağıdakilere Maingenişletildi:

public static Task Main();
public static Task<int> Main();
public static Task Main(string[] args);
public static Task<int> Main(string[] args);

Örneğin;

static async Task Main(string[] args)
{
    Bootstrapper bs = new Bootstrapper();
    var list = await bs.GetList();
}

Derleme zamanında, zaman uyumsuz giriş noktası yöntemi çağrıya çevrilir GetAwaitor().GetResult().

Ayrıntılar: https://blogs.msdn.microsoft.com/mazhou/2017/05/30/c-7-series-part-2-async-main

DÜZENLE:

C # 7.1 dil özelliklerini etkinleştirmek için, projeyi sağ tıklayıp "Özellikler" i tıklamanız ve "Oluştur" sekmesine gitmeniz gerekir. Orada, alttaki gelişmiş düğmeyi tıklayın:

resim açıklamasını buraya girin

Dil sürümü açılır menüsünden "7.1" (veya daha yüksek bir değer) seçin:

resim açıklamasını buraya girin

Varsayılan, konsol uygulamalarında zaman uyumsuz ana işlevi desteklemeyen (bu yazı yazıldığı sırada) C # 7.0'ı değerlendiren "en son ana sürüm" dür.


2
FWIW, şu anda buradan beta / önizleme sürümü olarak kullanılabilen Visual Studio 15.3 ve üstü sürümlerde mevcuttur: visualstudio.com/vs/preview
Mahmoud Al-Qudsi

Bir dakika ... Tamamen güncelleştirilmiş bir yükleme yapıyorum ve en son seçeneğim 7.1 ... Mayıs ayında 7,2’yi nasıl elde ettin?

Cevap benim oldu. Ekim düzenlemesi, 7.2 (önizleme?) Yayınlanmış olabileceğini düşündüğüm zamana kadar başka biri tarafından yapıldı.
nawfal

1
Uyarı - sadece bunu yaptığınızda hata ayıklamak için değil, tüm yapılandırmalarda olduğunu kontrol edin!
user230910

1
@ user230910 teşekkürler. C # ekibi tarafından en garip seçimlerden biri.
nawfal

74

Diğer tüm cevapların göz ardı ettiği önemli bir özelliği ekleyeceğim: iptal.

TPL'deki en önemli şeylerden biri iptal desteğidir ve konsol uygulamalarında yerleşik bir iptal yöntemi vardır (CTRL + C). Onları birbirine bağlamak çok basit. Tüm zaman uyumsuz konsol uygulamalarımı şu şekilde yapılandırıyorum:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();

    System.Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = true;
        cts.Cancel();
    };

    MainAsync(args, cts.Token).Wait();
}

static async Task MainAsync(string[] args, CancellationToken token)
{
    ...
}

İptal jetonuna da geçilmeli Wait()mi?
Siewers

5
Hayır, çünkü zaman uyumsuz kodun iptal işlemini zarif bir şekilde işleyebilmesini istiyorsunuz. Eğer adresine Wait()iletirseniz, zaman uyumsuz kodun bitmesini beklemez - beklemeyi durdurur ve işlemi hemen bitirir.
Cory Nelson

Bundan emin misin? Ben sadece denedim ve Wait()yöntem aynı belirteci geçti bile, iptal isteği en derin düzeyde işleniyor gibi görünüyor . Söylemeye çalıştığım şey, herhangi bir fark yaratmadığı.
Siewers

4
Eminim. Operatörün kendisini iptal etmek istiyorsunuz, op'un bitmesini beklemek değil. Temizleme kodunun sonlandırılmasını veya sonucunu önemsemediğiniz sürece.
Cory Nelson

1
Evet, sanırım anladım, kodumda herhangi bir fark yaratmadı. Beni kurtaracak başka bir şey, iptal etmeyi destekleyen bekleme yöntemi hakkında kibar bir ReSharper ipucu idi;) İlk başta anlayamadığım bir OperationCancelledException oluşturacağı için örneğe bir deneme yakalaması eklemek isteyebilirsiniz
Siewers

22

C # 7.1 (2017 güncelleme 3'e karşı) asenkron ana sistemi tanıtıyor

Yazabilirsin:

   static async Task Main(string[] args)
  {
    await ...
  }

Daha fazla bilgi için C # 7 Serisi, Bölüm 2: Async Main

Güncelleme:

Bir derleme hatası alabilirsiniz:

Program, bir giriş noktasına uygun statik bir 'Ana' yöntem içermiyor

Bu hata, vs2017.3'ün varsayılan olarak c # 7.1 değil c # 7.0 olarak yapılandırılmasından kaynaklanır.

C # 7.1 özelliklerini ayarlamak için projenizin ayarını açıkça değiştirmeniz gerekir.

C # 7.1'i iki yöntemle ayarlayabilirsiniz:

Yöntem 1: Proje ayarları penceresini kullanma:

  • Projenizin ayarlarını açın
  • Oluştur sekmesini seçin
  • Gelişmiş düğmesini tıklayın
  • Aşağıdaki şekilde gösterildiği gibi istediğiniz sürümü seçin:

resim açıklamasını buraya girin

Yöntem2: .csproj özelliği PropertyGroup'u el ile değiştirme

Bu mülkü ekle:

    <LangVersion>7.1</LangVersion>

misal:

    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <PlatformTarget>AnyCPU</PlatformTarget>
        <DebugSymbols>true</DebugSymbols>
        <DebugType>full</DebugType>
        <Optimize>false</Optimize>
        <OutputPath>bin\Debug\</OutputPath>
        <DefineConstants>DEBUG;TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
        <Prefer32Bit>false</Prefer32Bit>
        <LangVersion>7.1</LangVersion>
    </PropertyGroup>    

20

C # 7.1 veya üstünü kullanıyorsanız, nawfal'ın cevabına gidin ve Ana yönteminizin dönüş türünü Taskveya olarak değiştirin Task<int>. Eğer değilseniz:

  • Var bir async Task MainAsync Johan gibi söyledi .
  • Çağrısını .GetAwaiter().GetResult()yatan durum yakalamak için do0g dediğim gibi .
  • Cory'nin dediği gibi destek iptali .
  • Bir saniye CTRL+Csüreci derhal sonlandırmalıdır. (Teşekkürler binki !)
  • Tanıtıcı OperationCancelledException- uygun bir hata kodu döndürün.

Son kod şöyle görünür:

private static int Main(string[] args)
{
    var cts = new CancellationTokenSource();
    Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = !cts.IsCancellationRequested;
        cts.Cancel();
    };

    try
    {
        return MainAsync(args, cts.Token).GetAwaiter().GetResult();
    }
    catch (OperationCanceledException)
    {
        return 1223; // Cancelled.
    }
}

private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
{
    // Your code...

    return await Task.FromResult(0); // Success.
}

1
Birçok güzel program CancelKeyPress'i yalnızca ilk kez iptal eder, böylece ^ C tuşuna basarsanız zarif bir kapanış elde edersiniz, ancak sabırsızsanız ikinci bir ^ C nezaketsizce sona erer. Bu çözümle, e.Cancel = truekoşulsuz olduğu için CancellationToken'ı onurlandırmazsa programı el ile öldürmeniz gerekir .
binki

19

Henüz bu kadar ihtiyaç duymadım, ancak Hızlı testler için konsol uygulamasını kullandığımda ve async gerektirdiğimde bunu şu şekilde çözdüm:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).Wait();
    }

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}

Görevi geçerli bağlama göre zamanlamanız ve beklemeniz gerektiğinde bu örnek yanlış çalışır (örneğin, ConfigureAwait (false) eklemeyi unutabilirsiniz; bu nedenle, dönüş yöntemi, Bekleme işlevindeki ana iş parçacığına zamanlanır. ). Geçerli iş parçacığı bekleme durumunda olduğundan, bir kilitlenme alırsınız.
Manushin Igor

6
Doğru değil, @ManushinIgor. En azından bu önemsiz örnekte, SynchronizationContextana iş parçacığı ile ilişkili yoktur . Böylece kilitlenmeyecektir, çünkü onsuz bile ConfigureAwait(false)tüm devamlar threadpool'da yürütülecektir.
Andrew Arnott


4

Main'de GetList çağrısını şu şekilde değiştirmeyi deneyin:

Task.Run(() => bs.GetList());

4

C 5. CTP tanıtıldığında, kesinlikle olabilir ile Main işaretlemekasync ... genellikle bunu yapmak için iyi bir fikir olmamasına rağmen. Bunun VS 2013'ün piyasaya sürülmesiyle bir hata haline geldiğine inanıyorum.

Başka bir ön plan iş parçacığı Mainbaşlatmadıysanız, arka plan çalışması başlatılmış olsa bile programınız tamamlandığında çıkacaktır .

Gerçekten ne yapmaya çalışıyorsun? GetList()Yöntemin şu anda gerçekten zaman uyumsuz olması gerekmediğini unutmayın - gerçek bir sebep olmadan ekstra bir katman ekliyor. Mantıksal olarak eşdeğerdir (ancak daha karmaşıktır):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}

2
Jon, listedeki öğeleri eşzamansız olarak almak istiyorum, o zaman neden eşzamansız bu GetList yönteminde uygun değil? Listenin kendisinin değil async 'öğesindeki öğeleri toplamam gerektiği için mi? Ana yöntemi zaman uyumsuz olarak işaretlemeye çalıştığımda "statik bir Ana yöntem içermiyor ..."
danielovich

@danielovich: Ne DownloadTvChannels()dönüyor? Muhtemelen bir döndürür Task<List<TvChannel>>değil mi? Değilse, beklemeniz pek olası değildir. (Mümkün, awaiter desen, ama muhtemel verilen.) Gelince Mainyöntemiyle - bu hala vermedi ... statik olması gerekir yerinistatic ile değiştirici asyncbelki değiştirici?
Jon Skeet

evet, dediğin gibi bir Görev <..> döndürür. Async'i Main yöntem imzasına nasıl koymaya çalışsam da hata atar. VS11 önizleme bitleri üzerinde oturuyorum!
danielovich

@danielovich: Boş dönüş türüyle bile mi? Sadece public static async void Main() {}mi? Ancak DownloadTvChannels()zaten bir döndürürse Task<List<TvChannel>>, muhtemelen zaten eşzamansızdır - bu nedenle başka bir katman eklemenize gerek yoktur. Bunu dikkatlice anlamaya değer.
Jon Skeet

1
@nawfal: Geriye dönüp baktığımda, VS2013 piyasaya sürülmeden önce değiştiğini düşünüyorum. C # 7'nin bunu değiştirip değiştirmeyeceğinden emin değilim ...
Jon Skeet

4

C # - Yeni sürüm C # 7.1 zaman uyumsuz konsol uygulaması oluşturmanıza olanak sağlar. Projede C # 7.1'i etkinleştirmek için VS'nizi en az 15.3'e yükseltmeniz ve C # sürümünü C# 7.1veya olarak değiştirmeniz gerekir.C# latest minor version . Bunu yapmak için Proje özellikleri -> Derleme -> Gelişmiş -> Dil sürümü'ne gidin.

Bundan sonra, aşağıdaki kod çalışacaktır:

internal class Program
{
    public static async Task Main(string[] args)
    {
         (...)
    }

3

MSDN'de, Task.Run Yöntemi (Eylem) belgeleri, bir yöntemin eşzamansız olarak nasıl çalıştırılacağını gösteren bu örneği sağlar main:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        ShowThreadInfo("Application");

        var t = Task.Run(() => ShowThreadInfo("Task") );
        t.Wait();
    }

    static void ShowThreadInfo(String s)
    {
        Console.WriteLine("{0} Thread ID: {1}",
                          s, Thread.CurrentThread.ManagedThreadId);
    }
}
// The example displays the following output:
//       Application thread ID: 1
//       Task thread ID: 3

Örneği izleyen bu ifadeye dikkat edin:

Örnekler, zaman uyumsuz görevin ana uygulama iş parçacığından farklı bir iş parçacığında yürütüldüğünü gösterir.

Bu nedenle, bunun yerine görevin ana uygulama iş parçacığında çalışmasını istiyorsanız, @StephenCleary tarafından verilen cevaba bakın . .

Ve görevin çalıştığı konu ile ilgili olarak, Stephen'ın cevabı hakkındaki yorumuna da dikkat edin :

Sen edebilir bir basit kullanabilir Waitveya Resultve hiçbir şey yanlış bununla var. Ancak, iki önemli fark olduğunu unutmayın: 1) tüm asyncdevamlar ana iş parçacığı yerine iş parçacığı havuzunda çalışır ve 2) tüm istisnalar bir AggregateException.

(Bkz İstisna Yönetimi (Görev) Kitaplığı Paralel bir başa ele istisna dahil etmek nasıl AggregateException.)


Son olarak, MSDN'de Task.Delay Yöntemi (TimeSpan) belgelerinden , bu örnek, bir değer döndüren zaman uyumsuz bir görevin nasıl çalıştırılacağını gösterir:

using System;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        var t = Task.Run(async delegate
                {
                    await Task.Delay(TimeSpan.FromSeconds(1.5));
                    return 42;
                });
        t.Wait();
        Console.WriteLine("Task t Status: {0}, Result: {1}",
                          t.Status, t.Result);
    }
}
// The example displays the following output:
//        Task t Status: RanToCompletion, Result: 42

Yerine geçme Not delegateiçin Task.Run, bunun yerine böyle bir lambda fonksiyonu geçirebilirsiniz:

var t = Task.Run(async () =>
        {
            await Task.Delay(TimeSpan.FromSeconds(1.5));
            return 42;
        });

1

Arama yığınının aşağısında, geçerli iş parçacığını yeniden birleştirmeye çalışan (bir Beklemede sıkışmış) bir işlevi çağırdığınızda donmayı önlemek için aşağıdakileri yapmanız gerekir:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        List<TvChannel> list = Task.Run((Func<Task<List<TvChannel>>>)bs.GetList).Result;
    }
}

(oyuncu kadrosu yalnızca belirsizliği çözmek için gereklidir)


Teşekkürler; Task.Run GetList () kilitlenmesine neden olmaz. Bekleyin, bu cevap daha fazla oy verecek ...
Stefano d'Antonio

1

Benim durumumda, ana yöntemimden zaman uyumsuz olarak çalıştırmak istediğim işlerin bir listesi vardı, bunu bir süredir üretimde kullanıyor ve iyi çalışıyor.

static void Main(string[] args)
{
    Task.Run(async () => { await Task.WhenAll(jobslist.Select(nl => RunMulti(nl))); }).GetAwaiter().GetResult();
}
private static async Task RunMulti(List<string> joblist)
{
    await ...
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.