0

我有一个非常简单的程序来计算字符串中的字符。一个整数threadnum设置线程的数量,并将数据threadnum相应地划分为块以供每个线程处理。

每个线程递增共享字典中包含的值,构建字符直方图。

private Dictionary<UInt32, int> dict = new Dictionary<UInt32, int>();
  • 为了等待所有线程完成并继续主进程,我调用Thread.Join
  • 最初,我为每个线程都有一个本地字典,然后将其合并,但是共享字典工作正常,没有锁定。
  • BuildDictionary方法中没有锁定任何引用,尽管锁定字典不会显着影响线程执行时间。
  • 每个线程都是定时的,并比较结果字典。
  • 无论是单线程还是多线程,字典内容都是相同的——应该如此
  • 每个线程都需要一个由 threadnum 确定的分数来完成 -应该是这样

问题

总时间大致是 的倍数threadnum,也就是说执行时间增加了?

(不幸的是,我目前无法运行 C# Profiler。此外,我更喜欢 C# 3 代码兼容性。)

其他人可能也在苦苦挣扎。可能是VS 2010 express edition vshost进程堆栈和调度线程顺序运行?

另一个 MT 性能问题最近在此处发布为“Visual Studio C# 2010 Express Debug running Faster than Release”

代码

public int threadnum = 8;
Thread[] threads = new Thread[threadnum];
Stopwatch stpwtch = new Stopwatch();
stpwtch.Start();
for (var threadidx = 0; threadidx < threadnum; threadidx++)
{
    threads[threadidx] = new Thread(BuildDictionary);
    threads[threadidx].Start(threadidx);
    threads[threadidx].Join(); //Blocks the calling thread, till thread completion
}
WriteLine("Total - time: {0} msec", stpwtch.ElapsedMilliseconds);

你能帮忙吗?

更新

由于 IDE 调试器的众多钩子,随着线程数的增加,几乎线性减速的奇怪行为似乎是一种伪影。

在开发人员环境之外运行该过程,我实际上在 2 个逻辑/物理核心机器上获得了 30% 的速度提升。在调试期间,我已经处于 CPU 利用率的高端,因此我怀疑在开发过程中通过额外的空闲内核留有一些余地是明智的。

与最初一样,我让每个线程在其自己的本地数据块上进行计算,该数据块被锁定并写回共享列表并在所有线程完成后聚合。

结论

注意进程运行的环境。

4

6 回答 6

3

我们可以暂时将 Tony the Lion 在他的回答中提到的字典同步问题放在一边,因为在您当前的实现中,您实际上并没有并行运行任何东西!

让我们看一下您当前在循环中所做的事情:

  • 开始一个线程。
  • 等待线程完成。
  • 开始下一个线程。

换句话说,您不应该Join在循环内调用。

相反,您应该按照您的方式启动所有线程,但使用诸如 an 之类的单一构造AutoResetEvent来确定所有线程何时完成。

请参阅示例程序:

class Program
{
    static EventWaitHandle _waitHandle = new AutoResetEvent(false);

    static void Main(string[] args)
    {
        int numThreads = 5;
        for (int i = 0; i < numThreads; i++)
        {
            new Thread(DoWork).Start(i);
        }
        for (int i = 0; i < numThreads; i++)
        {
            _waitHandle.WaitOne();
        }
        Console.WriteLine("All threads finished");
    }

    static void DoWork(object id)
    {
        Thread.Sleep(1000);
        Console.WriteLine(String.Format("Thread {0} completed", (int)id));
        _waitHandle.Set();
    }
}

Join或者,如果您有对可用线程的引用,您也可以在第二个循环中调用。

完成此操作后,您可以并且应该担心字典同步问题。

于 2012-09-06T08:14:09.393 回答
2

只要集合没有被修改,一个 Dictionary 可以同时支持多个阅读器。来自MSDN

你说:

但是共享字典工作正常,没有锁定。

每个线程递增共享字典中包含的值

根据定义,您的程序已损坏,如果您在没有适当锁定的情况下更改字典中的数据,最终会出现错误。无需多言。

于 2012-09-06T08:09:34.447 回答
1

我不会使用一些 shared static Dictionary,如果每个线程都在本地副本上工作,那么一旦所有线程都发出完成信号,你就可以合并你的结果。

WaitHandle.WaitAll避免在AutoResetEvent.

class Program
{
    static void Main()
    {
        char[] text = "Some String".ToCharArray();
        int numThreads = 5;

        // I leave the implementation of the next line to the OP.
        Partition[] partitions = PartitionWork(text, numThreads);

        completions = new WaitHandle[numThreads];
        results = IDictionary<char, int>[numThreads];

        for (int i = 0; i < numThreads; i++)
        {
            results[i] = new IDictionary<char, int>();
            completions[i] = new ManualResetEvent(false);
            new Thread(DoWork).Start(
                text,
                partitions[i].Start,
                partitions[i].End,
                results[i],
                completions[i]);
        }

        if (WaitHandle.WaitAll(completions, new TimeSpan(366, 0, 0, 0))
        {
            Console.WriteLine("All threads finished");
        }
        else
        {
            Console.WriteLine("Timed out after a year and a day");
        }

        // Merge the results
        IDictionary<char, int> result = results[0];
        for (int i = 1; i < numThreads - 1; i ++)
        {
            foreach(KeyValuePair<char, int> item in results[i])
            {
                if (result.ContainsKey(item.Key)
                {
                    result[item.Key] += item.Value;
                }
                else
                {
                   result.Add(item.Key, item.Value);
                }
            }
        }
    }

    static void BuildDictionary(
        char[] text, 
        int start, 
        int finish,
        IDictionary<char, int> result,
        WaitHandle completed)
    {
        for (int i = start; i <= finish; i++)
        {
            if (result.ContainsKey(text[i])
            {
                result[text[i]]++;
            }
            else
            {
               result.Add(text[i], 1);
            }
        }
        completed.Set();
    }
}

有了这个实现,唯一共享的变量是char[]of thetext并且始终是只读的。

您确实有最后合并字典的负担,但是对于避免任何并发问题,这是一个很小的代价。在更高版本的框架中,我会使用 TPL 并且ConcurrentDictionary可能会使用Partitioner<TSource>.

于 2012-09-06T09:29:14.153 回答
0

罗姆看到了。

您的主线程应该在启动所有其他线程后加入 X 个其他线程。

否则它等待第一个线程完成,启动并等待第二个线程。

for (var threadidx = 0; threadidx < threadnum; threadidx++)
{
    threads[threadidx] = new Thread(BuildDictionary);
    threads[threadidx].Start(threadidx);
}

for (var threadidx = 0; threadidx < threadnum; threadidx++)
{
    threads[threadidx].Join(); //Blocks the calling thread, till thread completion
}
于 2012-09-06T08:12:21.050 回答
0

我完全同意 TonyTheLion 和其他人的观点,当您解决在错误位置加入的实际问题时,(无)锁和更新共享字典仍然会有问题。我想给你一个快速的解决方法:只需将你的整数值包装到某个对象中:

代替:

Dictionary<uint, int> dict = new Dictionary<uint, int>();

利用:

class Entry { public int value; }
Dictionary<uint, Entry> dict = new Dictionary<uint, Entry>();

现在增加 Entry::value 。这样, Dictionary 将不会注意到任何更改,并且在不锁定 dictionary 的情况下是安全的。

注意:这只有在保证一个线程只使用它自己的一个条目时才有效。正如您所说的“字符直方图”,我刚刚注意到这不是真的。您必须在增量期间锁定每个条目,否则某些增量可能会丢失。尽管如此,与锁定整个字典相比,在入口层锁定将显着加快

于 2012-09-06T08:17:07.190 回答
0

正如 Rotem 指出的那样,通过加入循环,您正在等待每个线程完成,然后再继续。

可以在 MSDN 上的 Thread.Join 文档中找到为什么会这样的提示

阻塞调用线程直到线程终止

因此,在一个线程完成工作之前,您的循环不会继续。要启动所有线程然后等待它们完成,请将它们加入循环之外:

public int threadnum = 8;
Thread[] threads = new Thread[threadnum];
Stopwatch stpwtch = new Stopwatch();
stpwtch.Start();

// Start all the threads doing their work
for (var threadidx = 0; threadidx < threadnum; threadidx++) 
{
     threads[threadidx] = new Thread(BuildDictionary);
     threads[threadidx].Start(threadidx);
}
// Join to all the threads to wait for them to complete
for (var threadidx = 0; threadidx < threadnum; threadidx++) 
{
    threads[threadidx].Join();
}

System.Diagnostics.Debug.WriteLine("Total - time: {0} msec", stpwtch.ElapsedMilliseconds);

你真的需要发布你的 BuildDictionary 函数。多线程的操作很可能不会更快,并且线程开销实际上会增加执行时间。

于 2012-09-06T08:30:23.397 回答