对列表 C# 的多线程访问
本文关键字:多线程 访问 列表 | 更新日期: 2023-09-27 18:36:54
我有一个任务来显示同步和非同步多线程之间的差异。因此,我编写了一个模拟从客户银行账户中提取资金的应用程序。一些线程中的每一个都会选择一个随机用户并从帐户中提取资金。每个线程应该撤回每个帐户一次。线程第一次同步,但第二次不同步。因此,由同步和非同步线程撤回的帐户之间必须存在差异。对于不同数量的用户和线程,差异必须不同。但是在我的应用程序中,我只有 1000 个线程的差异。所以我需要非同步线程的结果与同步线程的结果有很大不同。类用户:
public class User : IComparable
{
public string Name { get; set; }
public int Start { get; set; }
public int FinishSync { get; set; }
public int FinishUnsync { get; set; }
public int Hypothetic { get; set; }
public int Differrence { get; set; }
...
}
取款方法:
public void Withdraw(ref List<User> users, int sum, bool isSync)
{
int ind = 0;
Thread.Sleep(_due);
var rnd = new Random(DateTime.Now.Millisecond);
//used is list of users, withrawed by the thread
while (_used.Count < users.Count)
{
while (_used.Contains(ind = rnd.Next(0, users.Count))) ; //choosing a random user
if (isSync) //isSync = if threads syncroized
{
if (Monitor.TryEnter(users[ind]))
{
try
{
users[ind].FinishSync = users[ind].FinishSync - sum;
}
finally
{
Monitor.Exit(users[ind]);
}
}
}
else
{
lock (users[ind])
{
users[ind].FinishUnsync = users[ind].FinishUnsync - sum;
}
}
_used.Add(ind);
}
done = true;
}
线程是这样创建的:
private void Withdrawing(bool IsSync)
{
if (IsSync)
{
for (int i = 0; i < _num; i++)
{
_withdrawers.Add(new Withdrawer(Users.Count, _due, _pause));
_threads.Add(new Thread(delegate()
{ _withdrawers[i].Withdraw(ref Users, _sum, true); }));
_threads[i].Name = i.ToString();
_threads[i].Start();
_threads[i].Join();
}
}
else
{
for (int i = 0; i < _num; ++i)
{
_withdrawers.Add(new Withdrawer(Users.Count, _due, _pause));
_threads.Add(new Thread(delegate()
{ _withdrawers[i].Withdraw(ref Users, _sum, false); }));
_threads[i].Name = i.ToString();
_threads[i].Start();
}
}
}
我以这种方式更改了 Withdraw 类,bc 问题可能在于独立于委托创建线程:
class Withdrawer
{
private List<int>[] _used;
private int _due;
private int _pause;
public int done;
private List<Thread> _threads;
public Withdrawer(List<User> users, int n, int due, int pause, int sum)
{
_due = due;
_pause = pause;
done = 0;
_threads = new List<Thread>(users.Count);
InitializeUsed(users, n);
CreateThreads(users, n, sum, false);
_threads.Clear();
while (done < n) ;
Array.Clear(_used,0,n-1);
InitializeUsed(users, n);
CreateThreads(users, n, sum, true);
}
private void InitializeUsed(List<User> users, int n)
{
_used = new List<int>[n];
for (int i = 0; i < n; i++)
{
_used[i] = new List<int>(users.Count);
for (int j = 0; j < users.Count; j++)
{
_used[i].Add(j);
}
}
}
private void CreateThreads(List<User> users, int n, int sum, bool isSync)
{
for (int i = 0; i < n; i++)
{
_threads.Add(new Thread(delegate() { Withdraw(users, sum, isSync); }));
_threads[i].Name = i.ToString();
_threads[i].Start();
}
}
public void Withdraw(List<User> users, int sum, bool isSync)
{
int ind = 0;
var rnd = new Random();
while (_used[int.Parse(Thread.CurrentThread.Name)].Count > 0)
{
int x = rnd.Next(_used[int.Parse(Thread.CurrentThread.Name)].Count);
ind = _used[int.Parse(Thread.CurrentThread.Name)][x];
if (isSync)
{
lock (users[ind])
{
Thread.Sleep(_due);
users[ind].FinishSync -= sum;
}
}
else
{
Thread.Sleep(_due);
users[ind].FinishUnsync -= sum;
}
_used[int.Parse(Thread.CurrentThread.Name)][x] = _used[int.Parse(Thread.CurrentThread.Name)][_used[int.Parse(Thread.CurrentThread.Name)].Count - 1];
_used[int.Parse(Thread.CurrentThread.Name)].RemoveAt(_used[int.Parse(Thread.CurrentThread.Name)].Count - 1);
Thread.Sleep(_pause);
}
done++;
}
}
现在的问题是 FinishUnSync 值是正确的,而 FinishSync 值绝对不是。 Thread.Sleep(_due);和 Thread.Sleep(_pause);
用于"持有"资源,BC 我的任务是线程应该获取资源,保持 _due 毫秒,处理后等待 _pause 毫秒再完成。
您的代码没有执行任何有用的操作,并且没有显示同步和非同步访问之间的区别。您需要解决很多问题。
代码中的注释指出_used
是线程已访问的用户列表。您显然是在按线程的基础上创建它。如果这是真的,我不知道如何。从外观上看,我会说所有线程都可以访问_used
。我在任何地方都没有看到您正在创建该列表的每线程版本。命名约定表明它在类范围内。
如果该列表不是按线程列出的,那么这将大大有助于解释为什么您的数据始终相同。您在这里还有一个真正的争用条件,因为您正在从多个线程更新列表。
断言_used
确实是每线程的数据结构......
你有以下代码:
if (isSync) //isSync = if threads syncroized
{
if (Monitor.TryEnter(users[ind]))
{
try
{
users[ind].FinishSync = users[ind].FinishSync - sum;
}
finally
{
Monitor.Exit(users[ind]);
}
}
}
else
{
lock (users[ind])
{
users[ind].FinishUnsync = users[ind].FinishUnsync - sum;
}
}
这两者都提供同步。在isSync
情况下,如果线程已锁定用户,则第二个线程将无法执行更新。在第二种情况下,第二个线程将等待第一个线程完成,然后进行更新。无论哪种情况,使用 Monitor
或 lock
都会阻止并发更新。
不过,如果多个线程可以同时执行isSync
代码,您可能会看到差异。但是您不会看到区别,因为在同步的情况下,您永远不会让多个线程执行。也就是说,您有:
if (IsSync)
{
for (int i = 0; i < _num; i++)
{
_withdrawers.Add(new Withdrawer(Users.Count, _due, _pause));
_threads.Add(new Thread(delegate()
{ _withdrawers[i].Withdraw(ref Users, _sum, true); }));
_threads[i].Name = i.ToString();
_threads[i].Start();
_threads[i].Join();
}
}
else
{
for (int i = 0; i < _num; ++i)
{
_withdrawers.Add(new Withdrawer(Users.Count, _due, _pause));
_threads.Add(new Thread(delegate()
{ _withdrawers[i].Withdraw(ref Users, _sum, false); }));
_threads[i].Name = i.ToString();
_threads[i].Start();
}
}
因此,在IsSync
情况下,您启动一个线程,然后等待它完成,然后再启动另一个线程。您的代码不是多线程的。在"不同步"的情况下,您使用lock
来阻止并发更新。因此,在一种情况下,一次只运行一个线程来阻止并发更新,而在另一种情况下,通过使用 lock
来阻止并发更新。不会有区别。
其他值得注意的是,您随机选择用户的方法效率非常低,并且可能是您看到的问题的一部分。基本上,您正在做的是选择一个随机数并检查它是否在列表中。如果是,请重试,依此类推。而且这个名单还在不断增长。快速实验表明,在获得所有随机数之前,我必须生成 7,000 个介于 0 和 1,000 之间的随机数。因此,您的线程花费大量时间尝试查找下一个未使用的帐户,这意味着它们同时处理同一用户帐户的可能性较小。
你需要做三件事。首先,更改Withdrawl
方法,使其执行以下操作:
if (isSync) //isSync = if threads syncroized
{
// synchronized. prevent concurrent updates.
lock (users[ind])
{
users[ind].FinishSync = users[ind].FinishSync - sum;
}
}
else
{
// unsynchronized. It's a free-for-all.
users[ind].FinishUnsync = users[ind].FinishUnsync - sum;
}
无论IsSync
是否为真,您的Withdrawing
方法都应该相同。也就是说,它应该是:
for (int i = 0; i < _num; ++i)
{
_withdrawers.Add(new Withdrawer(Users.Count, _due, _pause));
_threads.Add(new Thread(delegate()
{ _withdrawers[i].Withdraw(ref Users, _sum, false); }));
_threads[i].Name = i.ToString();
_threads[i].Start();
}
现在,您始终有多个线程在运行。唯一的区别是是否同步对用户帐户的访问。
最后,将_used
列表作为索引列表放入users
列表中。像这样:
_used = new List<int>(users.Count);
for (int i = 0; i < _used.Count; ++i)
{
_used[i] = i;
}
现在,当您选择用户时,您将执行以下操作:
var x = rnd.Next(_used.Count);
ind = _used[x];
// now remove the item from _used
_used[x] = _used[_used.Count-1];
_used.RemoveAt(_used.Count-1);
这样,您可以更有效地生成所有用户。生成 n 个用户需要 n 个随机数。
几个吹毛求疵:
我不知道为什么您在Withdraw
方法中使用Thread.Sleep
调用。您认为它提供了什么好处?
没有真正的理由将DateTime.Now.Millisecond
传递给Random
构造函数。只需调用new Random()
将使用Environment.TickCount
作为种子。除非您真的想将种子限制为 0 到 1,000 之间的数字。