3

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

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

快速伪代码:

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);
    }
}

还有其他建议吗?

4

4 回答 4

7

我建议使用ConcurrentBag<string>.users

即使在枚举时,这也允许对用户进行多线程访问。

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

于 2013-06-06T01:11:17.853 回答
4

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

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

第一种方法很简单:在读取或修改列表lock(users) {...}的代码周围添加块。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);
    }
}
于 2013-06-06T01:12:41.767 回答
3

dasblinkenlight 的回答的建议很好。另一种选择是执行类似于您建议的操作:使用列表的不可变副本。除了 normal 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);
    }
}
于 2013-06-06T01:43:49.407 回答
2

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

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);
    }
  }
}

ETC...

于 2013-06-06T01:07:50.960 回答