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


一切都要從新版風車動漫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();
            }
        }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM