一段时间以来,我一直感兴趣的是看一些任务在跨多个线程拆分时是否能很好地工作。对我来说是一项单独的任务,所以今天早上在修改一项任务时,我以为可以尝试一下。做这样的事情,所以我想问一下我的代码出了什么问题以及如何做得更好。它的作用不是很重要,我不关心是否应该分拆这种任务,但是我对编写的代码的优劣更感兴趣。

private static void ExportDocTypes(IEnumerable<string> docTypes)
{
    var queue = new Queue<string>(docTypes);
    var tasks = new List<Task>();
    for (byte i = 0; i < threadCount; i++)  //threadCount is a const set to 4.
    {
        Action a = () =>
        {
            while(queue.Count() > 0) {
                var type = queue.Dequeue();
                ExportDocuments(type, i);  //i is only sent for response.write
            }
        };
        var t = new Task(a);
        t.Start();
        tasks.Add(t);
        System.Threading.Thread.Sleep(10);
    }
    while (tasks.Count() > 0)
    {
        foreach (var t in tasks)
        {
            if (t.IsCompleted)
            {
                t.Dispose();
                tasks.Remove(t);
            }
        }
        System.Threading.Thread.Sleep(1000);
    }
}


评论

该代码是否有效?如果两个任务同时完成,我希望它会崩溃。

确实可以。极其不幸的是,两个会同时结束,但是我仍然不知道那会是一个问题。 Sleep(10)使得两个任务不会尝试同时获取相同的docType。

当从任务中删除t时,任务的大小会更改,这会使foreach循环无效。想象这是一个for循环,一旦列表缩小,您将在列表的边界之外进行迭代。您可以打破foreach循环,但是Task.WaitAll()甚至linq .All(t => t.IsCompleted)会更​​加优雅。

在进行多线程编程时,@ JonathanPeel总是在发生“非常不可能”的事情。您只是在问问间歇性,难以重现的错误。

以我的经验,“极不可能”表示“保证在最坏的时间发生”,“最坏的可能”表示无法复制它或在重要客户面前。

#1 楼

而不是查看您的特定代码-乍一看似乎非常非常有问题-让我直接回答您的问题:


C#中多线程的最佳实践是什么? />


不要。多线程是一个坏主意。程序很难通过单点控制流程来理解。为什么要在地球上增加第二,第三,第四...?您是否喜欢追踪难以置信的难以复制,无法理解的错误?您对弱记忆模型和强记忆模型之间的区别是否有百科全书知识?没有?然后不要编写多线程程序!流程是一个非常好的工作单元;如果有工作要做,请生成一个进程。
哦,所以无论如何都要进行多线程编程。在这种情况下,请使用可用的最高级别的抽象。直接针对线程编程意味着您可以编写有关作业的程序时编写有关工人的程序。记住,工人只是达到目的的手段;您真正想要完成的是工作。使用任务并行库:任务是工作,线程是工作程序。让TPL为您找出如何有效利用工人的方法。
等一下,让我们再回到第一点。问问自己,您是否真的需要并发?有了真正的并发性,我们实际上就可以同时在一个程序中拥有两个或多个控制点。通过模拟并发,控制的两个点可以使用处理器轮流使用,因此它们实际上不是并发的。在大多数情况下,我们最终得到的是模拟并发,因为线程多于处理器。这自然使我们得出这样的结论:也许实际的并发并不是我们所需要的。人们在很多时候真正想要的是异步。并发是实现异步的一种方法,但不是唯一的方法。使用C#5中新的异步/等待功能来表示异步工作流。
如果您对上一点感到困惑,并且想稍等片刻,那么在一个进程中没有多个控制线程的情况下就无法实现异步,请阅读Stephen Cleary的文章“没有线程”。在理解多线程程序之前,请不要尝试编写它。
不惜一切代价避免共享内存。大多数线程错误是由于无法理解现实世界的共享内存语义所致。如果必须创建线程,则将它们视为进程:将它们提供给它们完成工作所需的一切,并让它们工作而无需修改与任何其他线程关联的内存。就像一个进程无法修改任何其他进程的内存一样。
哦,所以您想要跨线程共享内存,尽管这样做很容易出错。再次,让我回到使用可用的最高级别的工具。这些工具是由专家编写的。你不是专家。您是否知道如何将延迟计算的值写入字段,以便绝对保证延迟计算不会重复执行一次,并且无论任何给定处理器中的状态如何,都永远不会观察到该字段处于不一致状态缓存?我不。你可能不会。 Joe Duffy这样做了,这就是为什么您应该使用他编写的Lazy<T>原语而不是尝试自己编写。为什么要编写自己的共享内存的代码?然后为天国着想。我看到如此多的错误代码使人们进入头脑,而锁却是昂贵的,而且他们编写了这种令人费解的,令人费解的错误代码,从而逃避了一把锁的花费,而这花了他们十二纳米的时间。避免使用锁来节省成本,就像在抱怨拨动开关时花费太长时间才能到达灯泡一样。锁是确保内存一致性的强大工具。如果您打算在线程级别对共享内存进行编程,则避免了锁定的危险。
如果在任何生产代码中将Thread.Sleep与非零或一的参数一起使用,则可能做错了什么。线程很昂贵;您不付钱给工人睡觉,所以也不要付钱去睡觉。如果您通过睡眠来避免时间问题来解决正确性问题(就像您在代码中那样),那么您肯定做错了很多事情。多线程代码需要正确无视计时的偶然性。


评论


\ $ \ begingroup \ $
@SimonAndréForsberg:Sleep(0)具有非常具体的含义;它的意思是“将控制权交给准备运行的任何优先级相同的线程;如果不存在这样的线程,则不要放弃控制权”。 Sleep(1)表示“放弃控制”。这种微妙的差异有时很重要。
\ $ \ endgroup \ $
–埃里克·利珀特
2014-6-26 at 16:34



\ $ \ begingroup \ $
@Blindy:假设您没有产生专用线程,而是使用了Task.Delay(400)。现在,当前线程可以在时间流逝的情况下继续工作,而不必坐在那里睡觉。
\ $ \ endgroup \ $
–埃里克·利珀特
14年6月26日在16:38

\ $ \ begingroup \ $
可以,但是线程不必执行其他任何操作。那是个主意,线程在大多数时间都处于睡眠状态,无论是硬睡眠还是等待套接字I / O,而程序的其余部分都在执行该操作。它是Producer-Consumer-UI trifecta的生产者部分。我并不是说使用Sleep通常是个好主意,特别是不能作为同步,但是有使用案例IMO
\ $ \ endgroup \ $
–盲目的
14年6月26日在16:43

\ $ \ begingroup \ $
@Blindy:很好。通过专用于执行一个逻辑工作的线程,通过明确定义的机制指定它与其他线程进行通信以及将线程接触共享内存的程度限制为特定的线程安全对象,您实际上将线程视为轻量级的过程,这是一个好习惯。在没有异步/等待的世界中,这也是我可能也会这样做的方式。我会稍微减轻我的建议。
\ $ \ endgroup \ $
–埃里克·利珀特
14年6月26日在16:52

\ $ \ begingroup \ $
@EricLippert Windows XP之后,Windows API的“睡眠”功能更改为“零值将导致该线程将其时间片的剩余部分放弃给其他随时可以运行的线程”,无论其他线程如何线程的优先级。
\ $ \ endgroup \ $
–dcastro
2014年6月27日10:44



#2 楼

您可以使用Parallel类:

private static void ExportDocTypes(IEnumerable<string> docTypes)
{
    int i = -1;
    Parallel.ForEach(docTypes, docType => ExportDocuments(docType, Interlocked.Increment(ref i)));
}


http://msdn.microsoft.com/zh-cn/library/system.threading.tasks.parallel_methods%28v=vs .110%29.aspx

评论


\ $ \ begingroup \ $
我以前没有看过Parallel类,但这确实有用。 doxType的列表可以包含100或1000个项目。这些都将一起运行,还是自动处理线程限制?
\ $ \ endgroup \ $
–乔纳森·皮尔(JonathanPeel)
2014年6月26日9:55



\ $ \ begingroup \ $
默认情况下,它尝试计算最佳线程数(通常是CPU拥有的内核数)。但是,您可以手动设置此数字。检查将ParallelOptions类作为参数的重载方法。
\ $ \ endgroup \ $
– Nikita B
14-6-26在10:05



\ $ \ begingroup \ $
太酷了,我喜欢它能够确定最佳状态的想法(我很确定它会比我做得更好)。我会试一试。您认为这比其他方法更好吗?它是更少的代码,可能是更整洁的代码,除了它还有其他优点吗?
\ $ \ endgroup \ $
–乔纳森·皮尔(JonathanPeel)
14年6月26日在11:18

\ $ \ begingroup \ $
您可以使用Reflector或其他实用程序来检查实际的实现,但是我敢肯定,它将与您尝试做的非常相似:包含任务,线程池和其他东西。而且没有“魔术”。现在,如果您能提出比微软更好的实施方案,那我就没有判断力了。 :)您当然可以尝试。
\ $ \ endgroup \ $
– Nikita B
2014年6月26日11:40



\ $ \ begingroup \ $
@JonathanPeel,默认情况下,按值传递值类型,是的。但是,您有不同的情况。您不会在任何地方“通过”我。您可以在匿名委托的内部作用域中使用外部作用域中的变量,然后通过引用对其进行访问。这就是所谓的“关闭”,一开始可能会令人困惑/您可以在此处阅读更多信息:diditwith.net/…
\ $ \ endgroup \ $
– Nikita B
14-6-27在11:16



#3 楼

我会这样做:

private static void ExportDocTypes(IEnumerable<string> docTypes)
{
    var queue = new ConcurrentQueue<string>(docTypes);
    var tasks = new List<Task>();

    for (byte i = 0; i < threadCount; i++)  //threadCount is a const set to 4.
    {
        byte iteration = i;
        Task t = Task.Factory.StartNew(() =>
        {
            string type;
            bool queueNotEmpty = queue.TryDequeue(out type);
            while(queueNotEmpty) 
            {
                ExportDocuments(type, iteration);  //i is only sent for response.write
                bool queueEmpty = queue.TryDequeue(out type);
            }
        });
        tasks.Add(t);
    }

    Task.WaitAll(tasks.ToArray());
    tasks.ForEach(task => task.Dispose());
}


使用ConcurrentQueue之类的东西,因为许多线程将同时访问它。另外,在Task上更多地使用Linq和静态方法来检查是否完成,而不是使用Thread.Sleep。 for循环,否则i将指向当前值,而不是该迭代时捕获的值。循环生成Task时需要注意的事项。

评论


\ $ \ begingroup \ $
谢谢您在while(queue.TryDequeue(out type))工作的同时,有理由不这样做吗?
\ $ \ endgroup \ $
–乔纳森·皮尔(JonathanPeel)
14年6月26日在7:07

\ $ \ begingroup \ $
确实可以改善。我只是为了清楚起见才这样写。
\ $ \ endgroup \ $
– patchandthat
14年6月26日在7:21

\ $ \ begingroup \ $
我也注意到了迭代问题。谢谢。
\ $ \ endgroup \ $
–乔纳森·皮尔(JonathanPeel)
14年6月26日在8:01

\ $ \ begingroup \ $
根本不需要该变量,我是在响应。仅为我自己的利益编写它。
\ $ \ endgroup \ $
–乔纳森·皮尔(JonathanPeel)
14年6月26日在8:03

\ $ \ begingroup \ $
我认为没有理由在这里使用队列。您可以简单地在foreach循环中遍历docType。
\ $ \ endgroup \ $
– Nikita B
2014年6月26日上午11:50