希望你能帮我 :)
背景:
我正在用 C# 创建一个具有一些特定功能的相对基本的图像查看器应用程序。
一个目标是让它加载相对较大的图像(~8mb,实际上只有.jpg)足够快,以提供无延迟的浏览体验(用左右箭头键翻阅图像)。
我的解决方案:
为了实现这一点,我创建了一个带有循环缓冲区的 BufferManager 类。有一个方法 MaintainBuffer 的类,它是一个无限循环,保持缓冲区充满。它只是检查当前查看图像的左侧和右侧的缓冲区中是否至少有 K 个图像,如果没有,它会将图像加载到缓冲区中,直到存在为止。现在我很自然地在一个新线程上调用了这个 MaintainBuffer 方法,这样用户就可以翻阅图像而不会注意到 BufferManager 在后台工作,因此应用程序可以简单地向用户显示缓冲区中的图像。
我的解决方案的问题:
我的问题是 Thread2 将图像加载到缓冲区中会减慢我的 Thread1(主线程)的速度,因此每次用户翻转到新图像时,在将新图像加载到缓冲区时都会出现一个小的延迟。
我的调试尝试:
我检查了我的任务管理器,以确保线程没有在同一个核心或类似的东西上执行,但我可以看到两个不同核心上的使用率出现两个明显的峰值。这两个线程共享一些内存,其中包括缓冲区,所以我认为这可能是问题所在,并尝试使用临时变量并且只将引用移动到缓冲区中,这没有帮助(可能是优化?)。
现在我读到一些模糊地暗示 IO 操作会减慢我所有线程的东西,但这对我来说没有太大意义,因为 CPU 不应该真正参与将文件移动到主内存中,只有 Thread2 应该等待 IO 操作完成。
我的问题:
- 假设只有 Thread2 的 IO 操作应该减慢速度,我错了吗?
- 问题可能是我在线程之间共享数据吗?如果是这样,我该如何解决?
- 还有什么可能导致我的麻烦?在我对多线程的理解中,我是否遗漏了一些基本的东西?
- 甚至有比我试图做的更好的解决方案吗?
编码:
在我的 Form1 类中调用线程:
private void browser_Load(object sender, EventArgs e)
{
//This is just getting the image and other file filenames (others needed for a feature)
string[] files = Directory.GetFiles(path);
imageSets = new Dictionary<string, List<string>>();
List<string> associatedFiles;
foreach (string f in files)
{
string filename = System.IO.Path.GetFileNameWithoutExtension(f);
string filenameExt = System.IO.Path.GetFileName(f);
if (imageSets.ContainsKey(filename))
{
imageSets.TryGetValue(filename, out associatedFiles);
associatedFiles.Add(filenameExt);
}
else
{
imageSets.Add(filename, new List<string>(){filenameExt});
}
}
//Getting only the names of the images
images = imageSets.Keys.ToList<string>();
//Creating a new bufferManager, note that the 'images' list is used by the object for getting the names of the images to load
bufferManager = new BufferManager(ref images, path);
//Have the bufferManager load the first image and put it in the buffer
currentImage = bufferManager.Init();
//Create a thread for maintaining the buffer
bufferThread = new Thread(bufferManager.MaintainBuffer);
bufferThread.Start();
//Display the intial image to the user
img_preview.BackgroundImage = currentImage;
}
BufferManager的初始化方法:
public Image Init()
{
currentIndex = lastBufferIndex = firstBufferIndex = 0;
Image fullSize = ImageTool.FromFile(path + "\\" + images[0] + ".jpg");
//Resize the image in the buffer to save memory and display faster
imageBuffer[0] = ImageTool.Resize(fullSize, 460); //The buffer is a simple array of fixed size
fullSize.Dispose();
return imageBuffer[0];
}
这里有趣的方法:在 Thread2 上调用的 MaintainBuffer
public void MaintainBuffer()
{
if (images.Count <= totalBufferSize)
{
//If the buffer can hold all the images without swapping images ind out, just load them all and dont worry about maintaining the buffer
totalBufferSize = images.Count;
for (int i = 0; i < totalBufferSize; ++i)
{
imageBuffer[i] = ImageTool.Resize(ImageTool.FromFile(path + "\\" + images[i] + ".jpg"), 460);
}
return;
}
while (!killBuffer)
{
//Ensure that enough images are buffered to the left of the current image. This looks a bit advanced due to being a circulair buffer, but the idea is simple
while (currentBufferIndex >= firstBufferIndex ? (currentBufferIndex - firstBufferIndex) < bufferWidth : (currentBufferIndex + totalBufferSize - firstBufferIndex) < bufferWidth)
{
--firstBufferIndex;
if (firstBufferIndex < 0)
firstBufferIndex = totalBufferSize - 1;
if (imageBuffer[firstBufferIndex] != null)
imageBuffer[firstBufferIndex].Dispose(); //Release the memory used by the image that is now "overwritten"
//Getting the image index of the image to load next
int imageNameIndex = currentIndex - (currentBufferIndex >= firstBufferIndex ? currentBufferIndex - firstBufferIndex : currentBufferIndex + totalBufferSize - firstBufferIndex);
if (imageNameIndex < 0)
imageNameIndex = images.Count + imageNameIndex;
Image fullSize = ImageTool.FromFile(path + "\\" + images[imageNameIndex] + ".jpg");
imageBuffer[firstBufferIndex] = ImageTool.Resize(fullSize, 460);
fullSize.Dispose();
}
//Ensure that enough images are buffered to the right of the current image
while (currentBufferIndex <= lastBufferIndex ? (lastBufferIndex - currentBufferIndex) < bufferWidth : (lastBufferIndex + totalBufferSize - currentBufferIndex) < bufferWidth)
{
++lastBufferIndex;
if (lastBufferIndex > totalBufferSize - 1)
lastBufferIndex = 0;
if (imageBuffer[lastBufferIndex] != null)
imageBuffer[lastBufferIndex].Dispose();
int imageNameIndex = (currentIndex + (currentBufferIndex <= lastBufferIndex ? lastBufferIndex - currentBufferIndex : lastBufferIndex + totalBufferSize - currentBufferIndex)) % (images.Count);
Image fullSize = ImageTool.FromFile(path + "\\" + images[imageNameIndex] + ".jpg");
imageBuffer[lastBufferIndex] = ImageTool.Resize(fullSize, 460);
fullSize.Dispose();
}
Thread.Sleep(100);
}
}
我省略了左右翻转图像的代码,因为它非常简单。但是,它确实会更新 Thread1 上 BufferManager 对象的 currentIndex(正在查看的图像的索引)和 currentBufferIndex(循环缓冲区中的当前图像位置),这是可能导致问题的数据共享示例?
正如您可能注意到的那样,我对多线程的经验并不多,所以我希望您能给我一些指示!谢谢