多线程,lambdas和局部变量

时间:2011-12-05 19:50:43

标签: c# multithreading .net-4.0 lambda task-parallel-library

我的问题是,在下面的代码中,我可以确定实例方法将访问我认为它们将要的变量,还是可以在我还在工作时由另一个线程更改?封闭是否与此有关,即我是否正在处理IEnumerable<T>的本地副本,因此枚举是否安全?

为了解释我的问题,如果我从不写共享变量,我是否需要任何锁?

public class CustomerClass
{
    private Config cfg = (Config)ConfigurationManager.GetSection("Customer");

    public void Run()
    {
        var serviceGroups = this.cfg.ServiceDeskGroups.Select(n => n.Group).ToList();

        var groupedData = DataReader.GetSourceData().AsEnumerable().GroupBy(n => n.Field<int>("ID"));
        Parallel.ForEach<IGrouping<int, DataRow>, CustomerDataContext>(
            groupedData,
            () => new CustomerDataContext(),
            (g, _, ctx) =>
            {
                var inter = this.FindOrCreateInteraction(ctx, g.Key);

                inter.ID = g.Key;
                inter.Title = g.First().Field<string>("Title");

                this.CalculateSomeProperty(ref inter, serviceGroups);

                return ctx;
            },
            ctx => ctx.SubmitAllChanges());
    }

    private Interaction FindOrCreateInteraction(CustomerDataContext ctx, int ID)
    {
        var inter = ctx.Interactions.Where(n => n.Id = ID).SingleOrDefault();

        if (inter == null)
        {
            inter = new Interaction();
            ctx.InsertOnSubmit(inter);
        }

        return inter;
    }

    private void CalculateSomeProperty(ref Interaction inter, IEnumerable<string> serviceDeskGroups)
    {
        // Reads from the List<T> class instance variable. Changes the state of the ref'd object.
        if (serviceGroups.Contains(inter.Group))
        {
            inter.Ours = true;
        }
    }
}

2 个答案:

答案 0 :(得分:3)

我似乎找到了答案,并在此过程中,也是问题。

真正的问题是,对于并发访问,本地“变量”(实际上是对象)是否可以信任。答案是否定的,如果它们恰好具有未以线程安全方式处理的内部状态,则所有投注均已关闭。闭包没有帮助,它只是捕获对所述对象的引用。

在我的特定情况下 - 来自IEnumerable<T>的并发读取并且没有写入它, 实际上是线程安全的,因为每次调用foreachContains()Where()等获取一个全新的IEnumerator,它只能从请求它的线程中看到。但是,还必须逐个检查任何其他对象。

所以,万岁,我没有锁或同步收藏:)

感谢@ebb和@Dave,虽然你没有直接回答这个问题,但你指出了我正确的方向。


如果您对结果感兴趣,可以在家用PC(四核)上运行Thread.SpinWait来模拟一行的处理时间。真正的应用程序在本地网络上使用SQL Server的双核超线程机器上的改进几乎提高了2倍(01:03 vs 00:34)。

Singlethreaded 单线程,使用foreach。我不知道为什么,但是有相当多的跨核心上下文切换。

Multithreaded 使用Parallel.ForEach,在需要时使用线程本地锁定。

答案 1 :(得分:1)

现在,据我所知,你的实例方法没有使用任何成员变量。这使他们无国籍,因此线程安全。但是,在同样的情况下,为了代码清晰度和轻微的性能优势,最好将它们标记为“静态”。

如果这些实例方法使用的是成员变量,那么它们只是作为该变量的线程安全(例如,如果你使用了一个简单的列表,它就不会是线程安全的,你可能会看到奇怪的行为)。长话短说,成员变量是易线程安全的敌人。

这是我的重构(免责声明,未经测试)。如果您想提供传入的数据,如果您将它们作为参数传递并且不将它们保留为成员变量,您将保持稳定:

更新:您要求引用只读列表的方法,所以我添加了这个并删除了静态标记(以便可以共享实例变量)。

public class CustomerClass
{

private List<string> someReadOnlyList;

    public CustomerClass(){
      List<string> tempList  = new List<string>() { "string1", "string2" };
      someReadOnlyList = ArrayList.Synchronized(tempList);
    }

    public void Run()
    {
        var groupedData = DataReader.GetSourceData().AsEnumerable().GroupBy(n => n.Field<int>("ID"));

        Parallel.ForEach<IGrouping<int, DataRow>, CustomerDataContext>(
            groupedData,
            () => new CustomerDataContext(),
            (g, _, ctx) =>
            {
                var inter = FindOrCreateInteraction(ctx, g.Key);

                inter.ID = g.Key;
                inter.Title = g.First().Field<string>("Title");

                CalculateSomeProperty(ref inter);

                return ctx;
            },
            ctx => ctx.SubmitAllChanges());
    }

    private Interaction FindOrCreateInteraction(CustomerDataContext ctx, int ID)
    {
        var query = ctx.Interactions.Where(n => n.Id = ID);

        if (query.Any())
        {
            return query.Single();
        }
        else
        {
            var inter = new Interaction();
            ctx.InsertOnSubmit(inter);
            return inter;
        }
    }

    private void CalculateSomeProperty(ref Interaction inter)
    {
        Console.Writeline(someReadOnlyList[0]);
        //do some other stuff
    }
}