【发布时间】:2009-12-16 15:00:52
【问题描述】:
我正在编写一个简单的应用程序(对于我的妻子来说同样如此 :-P ),它对可能大量的图像进行一些图像处理(调整大小、时间戳等)。所以我正在编写一个可以同步和异步执行此操作的库。我决定使用Event-based Asynchronous Pattern。使用此模式时,您需要在工作完成后引发事件。这是我在知道何时完成时遇到问题的地方。所以基本上,在我的 DownsizeAsync 方法(缩小图像的异步方法)中,我正在做这样的事情:
public void DownsizeAsync(string[] files, string destination)
{
foreach (var name in files)
{
string temp = name; //countering the closure issue
ThreadPool.QueueUserWorkItem(f =>
{
string newFileName = this.DownsizeImage(temp, destination);
this.OnImageResized(newFileName);
});
}
}
现在最棘手的部分是知道它们何时全部完成。
这是我考虑过的:使用 ManualResetEvents 像这里:http://msdn.microsoft.com/en-us/library/3dasc8as%28VS.80%29.aspx 但我遇到的问题是您只能等待 64 个或更少的事件。我可能还有更多图片。
第二种选择:有一个计数器来计算已经完成的图像,并在计数达到总数时引发事件:
public void DownsizeAsync(string[] files, string destination)
{
foreach (var name in files)
{
string temp = name; //countering the closure issue
ThreadPool.QueueUserWorkItem(f =>
{
string newFileName = this.DownsizeImage(temp, destination);
this.OnImageResized(newFileName);
total++;
if (total == files.Length)
{
this.OnDownsizeCompleted(new AsyncCompletedEventArgs(null, false, null));
}
});
}
}
private volatile int total = 0;
现在感觉“hacky”,我不完全确定这是否是线程安全的。
那么,我的问题是,最好的方法是什么?有没有另一种方法来同步所有线程?我不应该使用线程池吗?谢谢!!
更新根据 cmets 的反馈和一些答案,我决定采用这种方法:
首先,我创建了一个扩展方法,将一个可枚举的对象批量化为“批次”:
public static IEnumerable<IEnumerable<T>> GetBatches<T>(this IEnumerable<T> source, int batchCount)
{
for (IEnumerable<T> s = source; s.Any(); s = s.Skip(batchCount))
{
yield return s.Take(batchCount);
}
}
基本上,如果你这样做:
foreach (IEnumerable<int> batch in Enumerable.Range(1, 95).GetBatches(10))
{
foreach (int i in batch)
{
Console.Write("{0} ", i);
}
Console.WriteLine();
}
你得到这个输出:
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95
这个想法是(正如 cmets 中的某个人指出的那样)没有必要为每个图像创建单独的线程。因此,我将图像分批成 [machine.cores * 2] 个批次。然后,我将使用第二种方法,即简单地保持计数器运行,当计数器达到我期望的总数时,我就知道我已经完成了。
我现在确信它实际上是线程安全的原因是因为我已根据MSDN 将总变量标记为 volatile:
通常使用 volatile 修饰符 对于被访问的字段 多个线程不使用 lock 语句来序列化访问。 使用 volatile 修饰符可确保 一个线程检索最多 另一个人写入的最新值 线程
表示我应该清楚(如果没有,请告诉我!!)
这是我要使用的代码:
public void DownsizeAsync(string[] files, string destination)
{
int cores = Environment.ProcessorCount * 2;
int batchAmount = files.Length / cores;
foreach (var batch in files.GetBatches(batchAmount))
{
var temp = batch.ToList(); //counter closure issue
ThreadPool.QueueUserWorkItem(b =>
{
foreach (var item in temp)
{
string newFileName = this.DownsizeImage(item, destination);
this.OnImageResized(newFileName);
total++;
if (total == files.Length)
{
this.OnDownsizeCompleted(new AsyncCompletedEventArgs(null, false, null));
}
}
});
}
}
我愿意接受反馈,因为我绝不是多线程方面的专家,所以如果有人对此有任何问题,或者有更好的想法,请告诉我。 (是的,这只是一个自制的应用程序,但我对如何利用我在这里获得的知识来改进我们在工作中使用的搜索/索引服务有一些想法。)现在我会一直保持这个问题,直到我感觉我正在使用正确的方法。感谢大家的帮助。
【问题讨论】:
-
total++ 在我看来不是线程安全的!
-
计数器方法具有很高的可扩展性。只需使用 Interlocked.Increment 和 .Decrement.. 使其线程安全。
-
在实践中,64 是这个限制吗?即使使用 64 个物理内核,您也会遇到内存和磁盘瓶颈,这些瓶颈在并行访问下会降级得更快。但是,这些可能会在一两年内消失。
-
我只是对为什么线程不能重新分配更多任务感到困惑;你真的需要为每个文件创建一个线程吗?这似乎效率低下。
-
@Dean J:很好。请参阅我的编辑。我想我将采用“批量”方法,每个线程将负责更多的图像。
标签: c# multithreading threadpool