【问题标题】:Synchronizing access to a collection同步访问集合
【发布时间】:2009-09-23 20:23:01
【问题描述】:

我有可能在两个不同线程上同时运行的这两段代码:

users = (from user in users
         orderby user.IsLoggedIn descending ,
                 user.Username
         select user).ToList();

和:

users=null;

第二段代码将在应用程序的主 UI 线程上运行。如何防止在 LINQ 操作完成之前将用户设置为 null?将用户集合封装在一个属性中,并在 getter 和 setter 上加锁,我认为这还不够……

编辑: 我构建了以下测试类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace MultiThreading_Spike
{
    class Program
    {
        private static List<User> users;
        private static Timer timer;
        static void Main(string[] args)
        {

            timer=new Timer(OrderUsers,null,5,5);
            for (int i = 0; i < 10000; i++)
            {
                ResetUsers();
                Thread.Sleep(5);
                users = new List<User>
                            {
                                new User {UserName = "John"},
                                new User {UserName = "Peter"},
                                new User {UserName = "Vince"},
                                new User {UserName = "Mike"}
                            };
                Thread.Sleep(5);
            }
            ResetUsers();
            Thread.Sleep(5)
            Debug.Assert(users==null);
        }

        private static void OrderUsers(object state)
        {
            if(users==null)return;
            Thread.Sleep(2);
            try
            {
                users = (from user in users
                         orderby user.IsLoggedIn descending ,
                             user.UserName
                         select user).ToList();
            }
            catch(Exception e)
            {
                Console.WriteLine("Error: {0}",e.Message);
            }
        }

        private static void ResetUsers()
        {
            users = null;
        }
    }


    public class User
    {
        bool isLoggedIn;
        public bool IsLoggedIn
        {
            get { return isLoggedIn; }
            set { isLoggedIn = value; }

        }

        private string userName;
        public string UserName
        {
            get { return userName; }
            set { userName = value; }
        }
    }
}

此代码失败,OrderUsers 方法中出现空引用异常。 然后我实施了建议的解决方案: 解决方案一:

//identical code omitted
        private static void OrderUsers(object state)
        {
         lock(syncRoot)
         {
            if(users==null)return;
            Thread.Sleep(2);
            try
            {
                users = (from user in users
                         orderby user.IsLoggedIn descending ,
                             user.UserName
                         select user).ToList();
            }
            catch(Exception e)
            {
                Console.WriteLine("Error: {0}",e.Message);
            }
         }
      }

        private static void ResetUsers()
        {
            lock(syncRoot)
            {
               users = null;
            }
        }
    }

没有例外!

解决方案 2:

   private static void OrderUsers(object state)
    {
        if(users==null)return;
        var tempUsers = users;
        Thread.Sleep(2);
        try
        {
            tempUsers = (from user in tempUsers
                     orderby user.IsLoggedIn descending ,
                         user.UserName
                     select user).ToList();
        }
        catch(Exception e)
        {
            Console.WriteLine("Error: {0}",e.Message);
        }
        users = tempUsers;
    }

没有空引用异常,但最终用户为空的Assert可能会失败。

解决方案 3:

private static void OrderUsers(object state)
        {
            if(users==null)return;
            try
            {
                users.Sort((a, b) => Math.Sign(-2 * a.IsLoggedIn.CompareTo(b.IsLoggedIn) + a.UserName.CompareTo(b.UserName)));
            }
            catch(Exception e)
            {
                Console.WriteLine("Error: {0}",e.Message);
            }
        }

没有例外。我一直有一种烦人的感觉,即排序可能“就位”,但不一定是原子的。

解决方案 4: 我无法编译它。 VolatileRead 方法有一个重载对象,但我无法让它接受一个列表

【问题讨论】:

    标签: c# .net multithreading synchronization


    【解决方案1】:

    您需要在执行 linq 查询之前和设置为 null 之前锁定:

    lock(syncRoot) 
    {
        users = null;
    }
    

    和

    lock(syncRoot) 
    {
        users = 
            (from user in users
             orderby user.IsLoggedIn descending,
                     user.Username
             select user).ToList(); 
    }
    

    在哪里同步根:

    private static syncRoot = new object();
    

    【讨论】:

    • 嘘!呸! syncRoot 不应该是静态的,除非用户是静态的。
    • 如果“用户”是方法的本地,那么就不需要锁定它。
    • @darin:重点是users 也被另一个线程分配了。阅读问题。
    【解决方案2】:

    将用户保存到本地变量和操作符上,这样您就不必同步任何内容。

    var tempUsers = users;
    if (tempUsers != null)
    {
        tempUsers = (from user in tempUsers
                 orderby user.IsLoggedIn descending, user.Username
                 select user).ToList()
    }
    

    【讨论】:

    • 我必须通过将 tempUsers 分配给用户来完成,对吧?如果是这样,我必须先检查 null,因为一旦用户集合设置为 null,我就不想填充它;用户不能在其他线程检查后立即设置为 null 吗?
    【解决方案3】:

    最好的方法是调用List&lt;T&gt;.Sort,它对列表进行就地排序并且不需要分配。

    例如:

    users.Sort((a, b) => Math.Sign(
        -2 * a.IsLoggedIn.CompareTo(b.IsLoggedIn) + a.UserName.CompareTo(b.UserName)
    ));
    

    (此示例假设没有一个用户为空)

    如果在执行此操作时,另一个线程执行users = null,则旧的List&lt;T&gt; 将被排序,但users 变量不会受到影响。

    【讨论】:

      【解决方案4】:

      无论users 变量是否重置为null,LINQ 查询都会完成,因为ToList() 强制进行急切评估。但是,我不确定这是否会解决您的问题:如果我理解正确,您希望确保不会在 users 中留下陈旧的集合,因为设置为 null 的时间太早了。

      在这种情况下,您可能只需声明一个用户锁定对象并在 setter 和 query-and-set 语句周围对其进行同步即可。

      【讨论】:

        【解决方案5】:

        你锁定什么并不重要,只要两个线程同意锁定同一个东西。

        就个人而言,我会使用局部变量和无锁操作:

        var localUsers = Thread.VolatileRead(ref users);
        do
        {
          if (null == localUsers)
          {
            break;
          }
        
          var newLocalUsers = (from user in localUsers 
                orderby user.IsLoggedIn descending ,                 
                user.Username         
                select user).ToList();
        
          var currentLocalUsers = Interlocked.CompareExchange(ref users, newLocalUsers, localUsers);
          if (currentLocalUsers == localUsers)
          {
            break;
          }
          // bummer, try again
          localUsers = currentLocalUsers;
        } while(true);
        

        还有另一个线程:

        Interlocked.Exchange(ref users, null);
        

        我从未尝试过将 Interlocked 与 var 类型一起使用,不确定编译器如何接受它还是必须将其向下转换为 object。

        【讨论】:

        • "var" 不是类型。 “var” 简单的意思是“编译器,这个变量的类型就是它的初始化器的类型”。
        • @Eric:我更想知道Interlocked.CompareExchange&lt;T&gt;() 泛型是否能够将linq 结果中List&lt;T&gt; 的类型与users 的任何类型相匹配和 i> 和 VolatileRead 返回类型。后者是object,我认为需要一些演员表。
        • 啊,我明白了。在这种情况下,联锁方法是您最不用担心的。如果 users 和 localUsers 都被键入为对象,编译器应该如何弄清楚查询理解的含义?在开始担心查询结果的类型之前,您必须能够弄清楚查询的含义!
        猜你喜欢
        • 2017-10-08
        • 2020-07-22
        • 2010-11-18
        • 1970-01-01
        • 2013-03-04
        • 1970-01-01
        • 1970-01-01
        • 2019-10-13
        • 2017-12-07
        相关资源
        最近更新 更多