记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();
            }
        }
原文地址:https://www.cnblogs.com/loyieking/p/9506036.html