c#列表并发性

本文关键字:并发 列表 | 更新日期: 2023-09-27 18:17:03

我正在制作一个简单的irc聊天机器人(专门用于twitch)。tv流),我使用List来保存频道中所有用户的列表。当有人离开或加入时,我将他们从列表中添加或删除。然后我有一个线程每分钟运行一次,检查流是否在线,如果是,它会向我的用户列表中的所有人分发"货币"。

我相信你已经知道我的问题在哪里了。如果在我的程序循环遍历列表中的用户时有人离开或加入,那么我会得到一个Collection Modified异常。目前,作为一种变通方法,我只是制作一个临时列表并将真实列表复制到其中,然后循环遍历临时列表,但我只是好奇是否有"更好"的方法来做到这一点?

快速psuedocode:

private List<string> users = new List<string>();
private void IrcInitialize(){
    //connect to irc stuff
    //blah
    //blah
    //blah
    Thread workThread = new Thread(new ThreadStart(doWork());
    workThread.Start();
}
private void ircListener(){
    parseIRCMessage(StreamReader.ReadLine());
}
private void parseIRCMessage(msg){
    if (msgType == "JOIN"){
        users.Add(user);
    }
    else if (msgType == "PART"){
        users.Remove(user);
    }
}
private void doWork(){
    while (true) {
        if (streamOnline() && handOutTime()){
            handOutCurrency();
        }
        Thread.Sleep(60000);
    }
}
private void handOutCurrency(){
    List<string> temp = users; //This is what I'm currently doing
    foreach (String user in temp) {
        database.AddCurrency(user, 1);
    }
}

还有其他建议吗?

c#列表并发性

我建议用ConcurrentBag<string>代替users

这允许多线程访问用户,即使它正在被枚举。

最大的好处是你不必担心locking

有两种方法可以解决这个问题:

  • 使用锁同步两个线程之间的访问,或者
  • 从一个线程执行所有访问。

第一种方法很简单:在读取或修改users列表的代码周围添加lock(users) {...}块。

第二种方法稍微复杂一些:在类中定义两个并发队列,toAddtoRemove。不要直接从users列表中添加或删除用户,而是将它们添加到toAddtoRemove队列中。当休眠线程醒来时,它应该首先清空两个队列,并根据需要执行修改。只有这样,它才应该发放货币。

ConcurrentQueue<string> toAdd = new ConcurrentQueue<string>();
ConcurrentQueue<string> toRemove = new ConcurrentQueue<string>();
private void parseIRCMessage(msg){
    if (msgType == "JOIN"){
        toAdd.Enqueue(user);
    }
    else if (msgType == "PART"){
        toRemove.Enqueue(user);
    }
}
private void doWork(){
    while (true) {
        string user;
        while (toAdd.TryDequeue(out user)) {
            users.Add(user);
        }
        while (toRemove.TryDequeue(out user)) {
            users.Remove(user);
        }
        if (streamOnline() && handOutTime()){
            handOutCurrency();
        }
        Thread.Sleep(60000);
    }
}

dasblinkenlight的回答建议很好。另一种选择是做与您建议的类似的事情:使用列表的不可变副本。除了正常的List,你需要确保它没有改变,而你复制它(你实际上需要复制列表,而不仅仅是引用它,像你的代码建议)。

这种方法的一个更好的版本是使用来自不可变集合库的ImmutableList。这样,每次修改都会创建一个新的集合(但与前一个版本共享大部分内容以提高效率)。通过这种方式,您可以让一个线程修改列表(实际上是基于旧列表创建新列表),同时还可以从另一个线程读取列表。这将有效,因为新的更改不会反映在列表的旧副本中。

这样,你的代码看起来就像这样:
private ImmutableList<string> users = ImmutableList<string>.Empty;
private void ParseIRCMessage(string msg)
{
    if (msgType == "JOIN")
    {
        users = users.Add(user);
    }
    else if (msgType == "PART")
    {
        users = users.Remove(user);
    }
}
private void HandOutCurrency()
{
    foreach (String user in users)
    {
        database.AddCurrency(user, 1);
    }
}

您需要在列表的所有读、写和迭代期间锁定列表。

private void parseIRCMessage(msg){
  lock(users)
  {
    if (msgType == "JOIN"){
        users.Add(user);
    }
    else if (msgType == "PART"){
        users.Remove(user);
    }
  }
}
private void doWork(){
    while (true) {
        if (streamOnline() && handOutTime()){
            handOutCurrency();
        }
    Thread.Sleep(60000);
    }
}
private void handOutCurrency(){
  lock(users)
  {
    foreach (String user in users) {
        database.AddCurrency(user, 1);
    }
  }
}

等等…