1. 程式人生 > >記UWP開發——多線程操作/並發操作中的坑

記UWP開發——多線程操作/並發操作中的坑

app cover ESS ret googl 加載 神奇 view 不存在

一切都要從新版風車動漫UWP的圖片緩存功能說起。

起因便是風車動漫官網的番劇更新都很慢,所以圖片更新也非常慢。在開發新版的過程中,我很簡單就想到了圖片多次重復下載導致的資源浪費問題。

所以我給app加了一個緩存機制:

創建一個用戶控件CoverView,將首頁GridView.ItemTemplate裏的Image全部換成CoverView

CoverView一旦接到ImageUrl的修改,就會自動向後臺的PictureHelper申請指定Url的圖片

PictureHelper會先判斷本地是否有這個Url的圖片,沒有的話從風車動漫官網下載一份,保存到本地,然後返回給CoverView

關鍵就是PictureHelper的GetImageAsync方法

本地緩存圖片的代碼片段:

    //緩存文件名以MD5的形式保存在本地
    string name = StringHelper.MD5Encrypt16(Url);


    if (imageFolder == null)
        imageFolder = await cacheFolder.CreateFolderAsync("imagecache", CreationCollisionOption.OpenIfExists);
    StorageFile file;
    IRandomAccessStream stream = null;
    if (File.Exists(imageFolder.Path + "
\\" + name)) { file = await imageFolder.GetFileAsync(name); stream = await file.OpenReadAsync(); } //文件不存在or文件為空,通過http下載 if (stream == null || stream.Size == 0) { file = await imageFolder.CreateFileAsync(name, CreationCollisionOption.ReplaceExisting); stream
= await file.OpenAsync(FileAccessMode.ReadWrite); IBuffer buffer = await HttpHelper.GetBufferAsync(Url); await stream.WriteAsync(buffer); } //...

嗯...一切都看似很美好....

但是運行之後,發現了一個很嚴重的偶發Exception

技術分享圖片

查閱google良久後,得知了發生這個問題的原因:

主頁GridView一次性加載了幾十個Item後,幾十個Item中的CoverView同時調用了PictureHelper的GetImageAsync方法

幾十個PictureHelper的GetImageAsync方法又同時訪問緩存文件夾,導致了非常嚴重的IO鎖死問題,進而引發了大量的UnauthorizedAccessException

技術分享圖片

有=又查閱了許久之後,終於找到了解決方法:

SemaphoreSlim異步鎖

使用方法如下:

        private static SemaphoreSlim asyncLock = new SemaphoreSlim(1);//1:信號容量,即最多幾個異步線程一起執行,保守起見設為1

        public async static Task<WriteableBitmap> GetImageAsync(string Url)
        {
            if (Url == null)
                return null;
            try
            {
                await asyncLock.WaitAsync();

                //緩存文件名以MD5的形式保存在本地
                string name = StringHelper.MD5Encrypt16(Url);


                if (imageFolder == null)
                    imageFolder = await cacheFolder.CreateFolderAsync("imagecache", CreationCollisionOption.OpenIfExists);
                StorageFile file;
                IRandomAccessStream stream = null;
                if (File.Exists(imageFolder.Path + "\\" + name))
                {
                    file = await imageFolder.GetFileAsync(name);
                    stream = await file.OpenReadAsync();
                }

                //文件不存在or文件為空,通過http下載
                if (stream == null || stream.Size == 0)
                {
                    file = await imageFolder.CreateFileAsync(name, CreationCollisionOption.ReplaceExisting);
                    stream = await file.OpenAsync(FileAccessMode.ReadWrite);
                    IBuffer buffer = await HttpHelper.GetBufferAsync(Url);
                    await stream.WriteAsync(buffer);
                }

                //...

            }
            catch(Exception error)
            {
                Debug.WriteLine("Cache image error:" + error.Message);
                return null;
            }
            finally
            {
                asyncLock.Release();
            }
        }
    

成功解決了並發訪問IO的問題

但是在接下來的Stream轉WriteableBitmap的過程中,問題又來了....

技術分享圖片

這個問題比較好解決

                BitmapDecoder bitmapDecoder = await BitmapDecoder.CreateAsync(stream);
                WriteableBitmap bitmap = null;
                await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async delegate
                {
                    bitmap = new WriteableBitmap((int)bitmapDecoder.PixelWidth, (int)bitmapDecoder.PixelHeight);
                    stream.Seek(0);
                    await bitmap.SetSourceAsync(stream);
                });
                stream.Dispose();
                return bitmap;

使用UI線程來跑就ok了

然後!問題又來了

WriteableBitmap到被return為止,都很正常

但是到接下來,我在CoverView裏做其他一些bitmap的操作時,出現了下面這個問題

技術分享圖片

又找了好久,最後回到bitmap的PixelBuffer一看,擦,全是空的?

雖然bitmap成功的new了出來,PixelHeight/Width啥的都有了,當時UI線程中的SetSourceAsync壓根沒執行完,所以出現了內存保護的神奇問題

明明await了啊?

最後使用這樣一個奇技淫巧,最終成功完成

                BitmapDecoder bitmapDecoder = await BitmapDecoder.CreateAsync(stream);
                WriteableBitmap bitmap = null;
                TaskCompletionSource<bool> task = new TaskCompletionSource<bool>();
                await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async delegate
                {
                    bitmap = new WriteableBitmap((int)bitmapDecoder.PixelWidth, (int)bitmapDecoder.PixelHeight);
                    stream.Seek(0);
                    await bitmap.SetSourceAsync(stream);
                    task.SetResult(true);
                });
                await task.Task;

關於TaskCompletionSource,請參閱

https://www.cnblogs.com/loyieking/p/9209476.html

最後總算是完成了....

        public async static Task<WriteableBitmap> GetImageAsync(string Url)
        {
            if (Url == null)
                return null;
            try
            {
                await asyncLock.WaitAsync();

                //緩存文件名以MD5的形式保存在本地
                string name = StringHelper.MD5Encrypt16(Url);


                if (imageFolder == null)
                    imageFolder = await cacheFolder.CreateFolderAsync("imagecache", CreationCollisionOption.OpenIfExists);
                StorageFile file;
                IRandomAccessStream stream = null;
                if (File.Exists(imageFolder.Path + "\\" + name))
                {
                    file = await imageFolder.GetFileAsync(name);
                    stream = await file.OpenReadAsync();
                }

                //文件不存在or文件為空,通過http下載
                if (stream == null || stream.Size == 0)
                {
                    file = await imageFolder.CreateFileAsync(name, CreationCollisionOption.ReplaceExisting);
                    stream = await file.OpenAsync(FileAccessMode.ReadWrite);
                    IBuffer buffer = await HttpHelper.GetBufferAsync(Url);
                    await stream.WriteAsync(buffer);
                }

                BitmapDecoder bitmapDecoder = await BitmapDecoder.CreateAsync(stream);
                WriteableBitmap bitmap = null;
                TaskCompletionSource<bool> task = new TaskCompletionSource<bool>();
                await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async delegate
                {
                    bitmap = new WriteableBitmap((int)bitmapDecoder.PixelWidth, (int)bitmapDecoder.PixelHeight);
                    stream.Seek(0);
                    await bitmap.SetSourceAsync(stream);
                    task.SetResult(true);
                });
                await task.Task;
                stream.Dispose();
                return bitmap;

            }
            catch(Exception error)
            {
                Debug.WriteLine("Cache image error:" + error.Message);
                return null;
            }
            finally
            {
                asyncLock.Release();
            }
        }

記UWP開發——多線程操作/並發操作中的坑