记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

时间: 2024-08-03 06:16:28

记UWP开发——多线程操作/并发操作中的坑的相关文章

更高效地提高redis client多线程操作的并发吞吐设计

Redis是一个非常高效的基于内存的NOSQL数据库,它提供非常高效的数据读写效能.在实际应用中往往是带宽和CLIENT库读写损耗过高导致无法更好地发挥出Redis更出色的能力.下面结合一些redis本身的特性和一些client操作上的改变来提高整个redis操作的交通. 上图是反映平常操作redis的情况,每个线程都独立的发起相应连接对redis的网络读写.虽然我们可以通过批操作的方式来把当前多个操作合并成一个,但这种方式只能针对当单线程,而多线程相互合并则设计上很少关注.从redis的协议来

iOS开发--多线程 并行开发

概览 大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行.改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作:对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程

Windows UWP 开发 - 异步编程

在 Windows UWP 开发中最基础也是最重要的就是异步编程,Windows Runtime 库,也就是 RT 库,其中的很多函数都是 async 结尾的,比如 PickSingleFolderAsync,凡是此类函数都是异步操作. MSDN 上的异步编程指南:https://msdn.microsoft.com/zh-cn/library/windows/apps/mt187340.aspx 在 Windows UWP C++/CX 中进行异步编程是非常容易的,有着基本固定的格式,如下.

UWP开发入门(二十一)——保持Ui线程处于响应状态

GUI的程序有时候会因为等待一个耗时操作完成,导致界面卡死.本篇我们就UWP开发中可能遇到的情况,来讨论如何优化处理. 假设当前存在点击按钮跳转页面的操作,通过按钮打开的新页面,在初始化过程中存在一些耗时的操作. public void OnNavigatedTo(object obj) { var watch = new Stopwatch(); Debug.WriteLine("---------------Start"); watch.Start(); //假设耗时1秒 DoBu

iOS开发多线程之自定义NSOperation

iOS开发多线程篇—自定义NSOperation 一.实现一个简单的tableView显示效果 实现效果展示: 代码示例(使用以前在主控制器中进行业务处理的方式) 1.新建一个项目,让控制器继承自UITableViewController. 1 // 2 // YYViewController.h 3 // 01-自定义Operation 4 // 5 // Created by apple on 14-6-26. 6 // Copyright (c) 2014年 itcase. All rig

iOS开发多线程篇—自定义NSOperation

iOS开发多线程篇—自定义NSOperation 一.实现一个简单的tableView显示效果 实现效果展示: 代码示例(使用以前在主控制器中进行业务处理的方式) 1.新建一个项目,让控制器继承自UITableViewController. 1 // 2 // YYViewController.h 3 // 01-自定义Operation 4 // 5 // Created by apple on 14-6-26. 6 // Copyright (c) 2014年 itcase. All rig

iOS开发多线程篇 09 —NSOperation简单介绍

iOS开发多线程篇—NSOperation简单介绍 一.NSOperation简介 1.简单说明 NSOperation的作?:配合使用NSOperation和NSOperationQueue也能实现多线程编程 NSOperation和NSOperationQueue实现多线程的具体步骤: (1)先将需要执行的操作封装到一个NSOperation对象中 (2)然后将NSOperation对象添加到NSOperationQueue中 (3)系统会?动将NSOperationQueue中的NSOpe

iOS开发多线程篇 05 —GCD介绍

iOS开发多线程篇—GCD介绍 一.简单介绍 1.什么是GCD? 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器” 纯C语言,提供了非常多强大的函数 2.GCD的优势 GCD是苹果公司为多核的并行运算提出的解决方案 GCD会自动利用更多的CPU内核(比如双核.四核) GCD会自动管理线程的生命周期(创建线程.调度任务.销毁线程) 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码 3.提示 (1)GCD存在于libdispatch.dylib这个库中

iOS开发多线程篇—多线程简单介绍

iOS开发多线程篇—多线程简单介绍 一.进程和线程 1.什么是进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内 比如同时打开QQ.Xcode,系统就会分别启动2个进程 通过“活动监视器”可以查看Mac系统中所开启的进程 2.什么是线程 1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程) 线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行 比如使用酷狗播放音乐.使用迅雷下载电影,都需要在线程中执行 3.线程