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);
}
}
还有其他建议吗?
我建议用ConcurrentBag<string>
代替users
。
这允许多线程访问用户,即使它正在被枚举。
最大的好处是你不必担心locking
有两种方法可以解决这个问题:
- 使用锁同步两个线程之间的访问,或者
- 从一个线程执行所有访问。
第一种方法很简单:在读取或修改users
列表的代码周围添加lock(users) {...}
块。
第二种方法稍微复杂一些:在类中定义两个并发队列,toAdd
和toRemove
。不要直接从users
列表中添加或删除用户,而是将它们添加到toAdd
和toRemove
队列中。当休眠线程醒来时,它应该首先清空两个队列,并根据需要执行修改。只有这样,它才应该发放货币。
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);
}
}
}
等等…