溫故之.NET 異步

這篇文章包含以下內(nèi)容

  • 異步基礎(chǔ)
  • 基于任務(wù)的異步模式
  • 部分 API 介紹

異步基礎(chǔ)

所謂異步蠢涝,對(duì)于計(jì)算密集型的任務(wù)派阱,是以線程為基礎(chǔ)的,而在具體使用中揩环,使用線程池里面的線程還是新建獨(dú)立線程搔弄,取決于具體的任務(wù)量;對(duì)于 I/O 密集型任務(wù)的異步丰滑,是以 Windows 事件為基礎(chǔ)的

.NET 提供了執(zhí)行異步操作的三種方式:

  • 異步編程模型 (APM) 模式(也稱 IAsyncResult 模式):在此模式中異步操作需要 BeginEnd 方法(比如用于異步寫入操作的 BeginWriteEndWrite)顾犹。不建議新的開發(fā)使用此模式
  • 基于事件的異步模式 (EAP):這種模式需要一個(gè)或多個(gè)事件、事件處理程序委托類型和 EventArg 派生類型褒墨,以便在工作完成時(shí)觸發(fā)炫刷。不建議新的開發(fā)使用這種模式
  • 基于任務(wù)的異步模式 (TAP):它是在 .NET 4 中引入的。C# 中的 asyncawait 關(guān)鍵字為 TAP 提供了語(yǔ)言支持郁妈。這是推薦使用方法

由于異步編程模型 (APM) 模式與基于事件的異步模式 (EAP)在新的開發(fā)中已經(jīng)不推薦使用浑玛。故在此處我們就不介紹了,以下僅介紹基于任務(wù)的異步模式(TAP

基于任務(wù)的異步模式(TAP)

任務(wù)是工作的異步抽象噩咪,而不是線程的抽象锄奢。即當(dāng)一個(gè)方法返回了 TaskTask<T>,我們不應(yīng)該認(rèn)為它一定創(chuàng)建了一個(gè)線程剧腻,而是開始了一個(gè)任務(wù)拘央。這對(duì)于我們理解 TAP 是非常重要的。

TAPTaskTask<T> 為基礎(chǔ)书在。它把具體的任務(wù)抽象成了統(tǒng)一的使用方式灰伟。這樣,不論是計(jì)算密集型任務(wù),還是 I/O 密集型任務(wù)栏账,我們都可以使用 async 帖族、await 關(guān)鍵字來(lái)構(gòu)建更加簡(jiǎn)潔易懂的代碼

任務(wù)分為 計(jì)算密集型任務(wù)I/O密集型任務(wù)任務(wù)兩種

  • 計(jì)算密集型任務(wù):當(dāng)我們 await 一個(gè)操作時(shí),該操作會(huì)通過 Task.Run 方法啟動(dòng)一個(gè)線程來(lái)處理相關(guān)的工作
    工作量大的任務(wù)挡爵,通過為 Task.Factory.StartNew 指定 TaskCreateOptions.LongRunning選項(xiàng) 可以使新的任務(wù)運(yùn)行于獨(dú)立的線程上竖般,而非使用線程池里面的線程
  • I/O 密集型任務(wù):當(dāng)我們 await 一個(gè)操作時(shí),它將返回 一個(gè) TaskTask<T>茶鹃。
    值得注意的是涣雕,這兒并不會(huì)啟動(dòng)一個(gè)線程

雖然計(jì)算密集型任務(wù)和 I/O 密集型任務(wù)在使用方式上沒有多大的區(qū)別,但其底層實(shí)現(xiàn)卻大不相同闭翩。

那我們?nèi)绾螀^(qū)分 I/O 密集型任務(wù)和計(jì)算密集型任務(wù)呢挣郭?

比如網(wǎng)絡(luò)操作,需要從服務(wù)器下載我們所需的資源疗韵,它就是屬于 I/O 密集型的操作兑障;比如我們通過排序算法對(duì)一個(gè)數(shù)組排序時(shí),這時(shí)的任務(wù)就是計(jì)算密集型任務(wù)蕉汪。

簡(jiǎn)而言之流译,判斷一個(gè)任務(wù)是計(jì)算型還是 I/O 型,就看它占用的 CPU 資源多者疤,還是 I/O 資源多就可以了福澡。

對(duì)于I/O密集型的應(yīng)用,它們是以 Windows 事件為基礎(chǔ)的宛渐,因此不需要新建一個(gè)線程或使用線程池里面的線程來(lái)執(zhí)行具體工作。但我們?nèi)匀豢梢允褂?async眯搭、await 來(lái)進(jìn)行異步處理窥翩,這得益于 .Net 為我們提供了一個(gè)統(tǒng)一的使用方式: TaskTask<T>

舉個(gè)例子,對(duì)于 I/O 密集型任務(wù)鳞仙,使用方式如下

// 這是在 .NET 4.5 及以后推薦的網(wǎng)絡(luò)請(qǐng)求方式
HttpClient httpClient = new HttpClient();
var result = await httpClient.GetStringAsync("https://www.baidu.com");

// 而不是以下這種方式(雖然得到的結(jié)果相同寇蚊,但性能卻不一樣,并且在.NET 4.5及以后都不推薦使用)
WebClient webClient = new WebClient();
var resultStr = Task.Run(() => {
    return webClient.DownloadString("https://www.baidu.com");
});

對(duì)于計(jì)算密集型應(yīng)用棍好,使用方式如下

Random random = new Random();
List<int> data = new List<int>();
for (int i = 0; i< 50000000; i++) {
    data.Add(random.Next(0, 100000));
}
// 這兒會(huì)啟動(dòng)一個(gè)線程仗岸,來(lái)執(zhí)行排序這種計(jì)算型任務(wù)
await Task.Run(() => {
    data.Sort();
});

異步方法返回 TaskTask<TResult>,具體取決于相應(yīng)方法返回的是 void 還是類型 TResult借笙。如果返回的是 void扒怖,則使用 Task,如果是 TResult业稼,則使用 Task<TResult>

不應(yīng)該使用 outref 的方式來(lái)返回值盗痒,因?yàn)檫@可能產(chǎn)生意料之外的結(jié)果。因此低散,我們應(yīng)該盡可能的使用 Task<TResult> 中的 TResult 來(lái)組合多個(gè)返回值

另外俯邓,await不能用在返回值為 void 的方法上骡楼,否則會(huì)有編譯錯(cuò)誤

針對(duì) TAP 的編碼建議

  • asyncawait 應(yīng)該搭配使用。即它們要么都出現(xiàn)稽鞭,要么都不出現(xiàn)
  • 僅在異步方法(即被 async 修飾的方法)中使用 await鸟整。否則會(huì)有編譯器錯(cuò)誤
  • 如果一個(gè)方法內(nèi)部,沒有使用 await朦蕴,則該方法不應(yīng)該使用 async 來(lái)修飾篮条,否則會(huì)有編譯器警告
  • 如果一個(gè)方法為異步方法(被 async 修飾),則它應(yīng)該以 Async 結(jié)尾
  • 我們應(yīng)該使用非阻塞的方式來(lái)編寫等待任務(wù)結(jié)果的代碼:
    使用 await梦重、await Task.WhenAny兑燥、 await Task.WhenAllawait Task.Delay 去等待后臺(tái)任務(wù)的結(jié)果琴拧。
    而不是 Task.Wait 降瞳、Task.ResultTask.WaitAny蚓胸、Task.WaitAll挣饥、Thread.Sleep,因?yàn)檫@些方式會(huì)阻塞當(dāng)前線程沛膳。

    即如果需要等待或暫停扔枫,我們應(yīng)該使用 .NET 4.5 提供的 await 關(guān)鍵字,而不是使用 .NET 4.5 之前的版本提供的方式
  • 如果是計(jì)算密集型任務(wù)锹安,則應(yīng)該使用 Task.Run 來(lái)執(zhí)行任務(wù)短荐;如果是耗時(shí)比較長(zhǎng)的任務(wù),則應(yīng)該使用 Task.Factory.StartNew 并指定 TaskCreateOptions.LongRunning 選項(xiàng)來(lái)執(zhí)行任務(wù)
  • 如果是 I/O 密集型任務(wù)叹哭,不應(yīng)該使用 Task.Run忍宋。
    因?yàn)?Task.Run 會(huì)在一個(gè)單獨(dú)的線程中運(yùn)行(線程池或者新建一個(gè)獨(dú)立線程),而對(duì)于 I/O 任務(wù)來(lái)說风罩,啟用一個(gè)線程意義不大糠排,反而會(huì)浪費(fèi)線程資源

創(chuàng)建任務(wù)

要?jiǎng)?chuàng)建一個(gè)計(jì)算密集型任務(wù),在 .NET 4.5 及以后超升,可采用 Task.Run 的方式來(lái)快速創(chuàng)建入宦;如果需要對(duì)任務(wù)有更多的控制權(quán),則可以使用 .NET 4.0 提供的 Task.Factory.StartNew 來(lái)創(chuàng)建一個(gè)任務(wù)室琢。
對(duì)于 I/O 密集型任務(wù)乾闰,我們可以通過將 await 作用于對(duì)應(yīng)的 I/O 操作方法上即可

取消任務(wù)

TAP 中,任務(wù)是可以取消的盈滴。通過 CancellationTokenSource 來(lái)管理汹忠。需要支持取消的任務(wù),必須持有 CancellationTokenSource.Token (令牌),以便該任務(wù)可以通過 CancellationTokenSource.Cancel() 的方式來(lái)取消宽菜。

使用 CancellationTokenSource 來(lái)取消任務(wù)谣膳,有以下優(yōu)點(diǎn)

  • 可以將令牌傳遞給多個(gè)任務(wù),這樣可以同時(shí)取消多個(gè)任務(wù)铅乡。類似于一個(gè)老師继谚,可以管理多個(gè)學(xué)生。
  • 可以通過 CancellationTokenSource.Token.Register 來(lái)監(jiān)聽任務(wù)的取消阵幸。這樣我們可以在任務(wù)取消之后做一些其他的工作

任務(wù)處理進(jìn)度

我們可以通過 IProgress<T> 接口監(jiān)聽進(jìn)度花履,如下所示

public Task ReadAsync(byte[] buffer, int offset, int count, IProgress<long> progress)

.NET 4.5 提供單個(gè) IProgress<T> 實(shí)現(xiàn):Progress<T>Progress<T> 類的聲明方式如下:

// Progress<T> 類的聲明
public class Progress<T> : IProgress<T> {  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T> ProgressChanged;  
}   

舉個(gè)例子挚赊,假設(shè)我們需要獲取并顯示下載進(jìn)度诡壁,則可以按以下方式書寫

private async void btnDownload_Click(object sender, RoutedEventArgs e) {  
    btnDownload.IsEnabled = false;  
    try {  
        txtResult.Text = await DownloadStringAsync(txtUrl.Text, new Progress<int>(p => pbDownloadProgress.Value = p));  
    }  
    finally { 
        btnDownload.IsEnabled = true; 
    }  
} 

部分 API 介紹

Task.WhenAll

此方法可以幫助我們同時(shí)等待多個(gè)任務(wù),所有任務(wù)結(jié)束(正常結(jié)束荠割、異常結(jié)束)后返回

這里需要注意的是妹卿,如果單個(gè)任務(wù)有異常產(chǎn)生,這些異常會(huì)合并到 AggregateException 中蔑鹦。我們可以通過 AggregateException.InnerExceptions 來(lái)得到異常列表夺克;也可以使用 AggregateException.Handle 來(lái)對(duì)每個(gè)異常進(jìn)行處理,示例代碼如下

public static async void EmailAsync() {
    List<string> addrs = new List<string>();
    IEnumerable<Task> asyncOps = addrs.Select(addr => SendMailAsync(addr));
    try {
        await Task.WhenAll(asyncOps);
    } catch (AggregateException ex) {
        // 可以通過 InnerExceptions 來(lái)得到內(nèi)部返回的異常
        var exceptions = ex.InnerExceptions;
        // 也可以使用 Handle 對(duì)每個(gè)異常進(jìn)行處理
        ex.Handle(innerEx => {
            // 此處的演示僅僅為了說明 ex.Handle 可以對(duì)異常進(jìn)行單獨(dú)處理
            // 實(shí)際項(xiàng)目中不一定會(huì)拋出此異常

            if (innerEx is OperationCanceledException oce) {
                // 對(duì) OperationCanceledException 進(jìn)行單獨(dú)的處理
                return true;
            } else if (innerEx is UnauthorizedAccessException uae) {
                // 對(duì) UnauthorizedAccessException 進(jìn)行單獨(dú)處理
                return true;
            }
            return false;
        });
    }
}

但嚎朽,如果我們需要對(duì)每個(gè)任務(wù)進(jìn)行更加詳細(xì)的管理铺纽,則可以使用以下方式來(lái)處理

public static async void EmailAsync() {
    List<string> addrs = new List<string>();
    IEnumerable<Task> asyncOps = addrs.Select(addr => SendMailAsync(addr));
    try {
        await Task.WhenAll(asyncOps);
    } catch (AggregateException ex) {
        // 此處可以針對(duì)每個(gè)任務(wù)進(jìn)行更加具體的管理
        foreach (Task<string> task in asyncOps) {
            if (task.IsCanceled) {
            }else if (task.IsFaulted) {
            }else if (task.IsCompleted) {
            }
        }
    }
}

這樣,就應(yīng)該基本上足夠應(yīng)對(duì)我們工作中的大部分的異常處理了

Task.WhenAny

Task.WhenAll 不同哟忍,Task.WhenAny 返回的是已完成的任務(wù)(可能只是所有任務(wù)中的幾個(gè)任務(wù))

舉個(gè)例子狡门,比如我們開發(fā)了一個(gè)圖片類App。我們可能需要在打開這個(gè)頁(yè)面時(shí),同時(shí)下載并展示多張圖片。但我們希望無(wú)論是哪一張圖片,只要下載完成,就展示出來(lái)吆倦,而不是所有的圖片都下載完了之后再展示。示例代碼如下

List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl)).ToList();
// 如果我們需要對(duì)圖片做一些處理(比如灰度化)米愿,可以使用以下代碼
// List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl).ContinueWith(task => ConvertToGray(task.Result)).ToList();
while(imageTasks.Count > 0) {  
    try {  
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        // 移除已經(jīng)下載完成的任務(wù)
        imageTasks.Remove(imageTask);  
        // 同時(shí)將該任務(wù)的圖片威始,在UI上呈現(xiàn)出來(lái)
        Bitmap image = await imageTask;  
        panel.AddImage(image);  
    } catch{}  
}

Task.Delay

此方法用于暫停當(dāng)前任務(wù)的執(zhí)行,在指定時(shí)間之后繼續(xù)運(yùn)行肤寝。

它可以與 Task.WhenAnyTask.WhenAll 結(jié)合当辐,實(shí)現(xiàn)任務(wù)的超時(shí),如下

public async void btnDownload_Click(object sender, EventArgs e) {  
    btnDownload.Enabled = false;  
    try {  
        Task<Bitmap> download = GetBitmapAsync(url); 
        // 以下的這行代碼表示鲤看,如果在 3s 之內(nèi)沒有下載完成缘揪,則認(rèn)為超時(shí)
        if (download == await Task.WhenAny(download, Task.Delay(3000))) {  
            Bitmap bmp = await download;  
            pictureBox.Image = bmp;  
            status.Text = "Downloaded";  
        } else {  
            pictureBox.Image = null;  
            status.Text = "Timed out";  
            var ignored = download.ContinueWith(t => Trace("Task finally completed"));
        }  
    } finally { 
      btnDownload.Enabled = true; 
    }  
}  

通過這種方式,也可以監(jiān)聽使用 Task.WhenAll 時(shí)多個(gè)任務(wù)的超時(shí),如下

Task<Bitmap[]> downloads = Task.WhenAll(from url in urls select GetBitmapAsync(url));  
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000))) {  
    foreach(var bmp in downloads) 
        panel.AddImage(bmp);  
    status.Text = "Downloaded";  
} else {
    status.Text = "Timed out";  
    downloads.ContinueWith(t => Log(t));  
}



另外找筝,提供兩個(gè)有用的函數(shù)蹈垢,以方便我們?cè)陧?xiàng)目中使用

RetryOnFail

定義如下所示

// 如果下載資源失敗后,我們希望重新下載時(shí)可以使用此方法
// 我們可以指定失敗之后袖裕,間隔多長(zhǎng)時(shí)間才重試曹抬。
// 也可以將 retryWhen 指定為 null,以便在失敗之后立即重試
public static async Task<T> RetryOnFail<T>(Func<Task<T>> function, int maxTries, Func<Task> retryWhen) {
    for (int i = 0; i < maxTries; i++) {
        try {
            return await function().ConfigureAwait(false);
        } catch {
            if (i == maxTries - 1) throw;
        }
        if (retryWhen != null)
            await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

使用方式如下急鳄,這在失敗之后谤民,暫停 1s,然后再重試

string pageContents = await RetryOnFail(() => DownloadStringAsync(url), 3, () => Task.Delay(1000)); 

或者如下疾宏,這將在失敗之后立即重試

string pageContents = await RetryOnFail(() => DownloadStringAsync(url), 3, null); 

NeedOnlyOne

定義如下

public static async Task<T> NeedOnlyOne<T>(params Func<CancellationToken, Task<T>>[] functions) {
    var cts = new CancellationTokenSource();
    var tasks = functions.Select(func => func(cts.Token));
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach (var task in tasks) {
        var ignored = task.ContinueWith(t => Trace.WriteLine(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return await completed;
}

對(duì)于前面我們提到的下載電影的例子:獲取到速度最快的渠道之后张足,立即取消其他的任務(wù)。現(xiàn)在我們可以這樣做

var line = await NeedOnlyOne(
            token => DetectSpeedAsync("line_1", movieName, cts.Token),
            token => DetectSpeedAsync("line_2", movieName, cts.Token),
            token => DetectSpeedAsync("line_3", movieName, cts.Token)
            );



以上提供的這兩個(gè)方法坎藐,在實(shí)際項(xiàng)目中會(huì)非常有用为牍,在需要時(shí)可以將它們用起來(lái)。當(dāng)然顺饮,通過對(duì) Task 的靈活運(yùn)用吵聪,可以組合出更多方便的方法出來(lái)。在具體項(xiàng)目中多多使用即可

關(guān)于 Task 的一些基本的用法就介紹到這兒了


至此兼雄,本節(jié)內(nèi)容講解完畢吟逝。下一篇文章我們將講解 .NET 中的并行編程。歡迎關(guān)注公眾號(hào)【嘿嘿的學(xué)習(xí)日記】赦肋,所有的文章块攒,都會(huì)在公眾號(hào)首發(fā),Thank you~

公眾號(hào)二維碼.jpg

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末佃乘,一起剝皮案震驚了整個(gè)濱河市囱井,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趣避,老刑警劉巖庞呕,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異程帕,居然都是意外死亡住练,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門愁拭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)讲逛,“玉大人,你說我怎么就攤上這事岭埠≌祷欤” “怎么了蔚鸥?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)许赃。 經(jīng)常有香客問我止喷,道長(zhǎng),這世上最難降的妖魔是什么混聊? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任启盛,我火速辦了婚禮,結(jié)果婚禮上技羔,老公的妹妹穿的比我還像新娘僵闯。我一直安慰自己,他們只是感情好藤滥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布鳖粟。 她就那樣靜靜地躺著,像睡著了一般拙绊。 火紅的嫁衣襯著肌膚如雪向图。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天标沪,我揣著相機(jī)與錄音榄攀,去河邊找鬼。 笑死金句,一個(gè)胖子當(dāng)著我的面吹牛檩赢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播违寞,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼贞瞒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了趁曼?” 一聲冷哼從身側(cè)響起军浆,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挡闰,沒想到半個(gè)月后乒融,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摄悯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年赞季,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片射众。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡碟摆,死狀恐怖晃财,靈堂內(nèi)的尸體忽然破棺而出叨橱,到底是詐尸還是另有隱情典蜕,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布罗洗,位于F島的核電站愉舔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏伙菜。R本人自食惡果不足惜轩缤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贩绕。 院中可真熱鬧火的,春花似錦、人聲如沸淑倾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)娇哆。三九已至湃累,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碍讨,已是汗流浹背治力。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勃黍,地道東北人宵统。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像覆获,于是被迫代替她去往敵國(guó)和親榜田。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容