在这篇博客中,我们将实现一个简单的模拟程序,模拟10只猴子在抢夺100,000个香蕉的过程中进行并发操作。通过这个例子,我们将学习如何在C#中使用异步编程 (async/await
)、多线程 (Task.Run
) 以及线程安全机制(如 lock
)来处理共享资源的并发访问
项目需求
- 游戏背景:有100,000个香蕉,10只猴子参与抢夺。
- 每只猴子的行为:每只猴子在抢夺香蕉时,每次随机抓取1到100个香蕉。每次抓取后,剩余香蕉数会减少,直到没有香蕉可以抢夺为止。
- 线程安全:由于多个猴子会并发访问共享资源(剩余香蕉数量),因此需要确保线程安全,避免在多线程环境下发生数据竞态。
设计思路
- 共享资源:香蕉的数量是共享资源。我们需要使用
lock
来确保每次只有一个猴子可以修改剩余香蕉数量,从而避免竞争条件。 - 异步操作:每只猴子的抢夺行为是异步执行的,因此我们使用
Task.Run
来并行处理每个猴子的任务,并使用async/await
来等待所有任务完成。 - UI更新:在任务完成后,我们需要在UI线程中更新界面,显示每只猴子抢到的香蕉数量。
代码实现
以下是完整的代码实现,模拟了多个猴子并发抢香蕉的过程。
namespace Monkey
{
public partial class Form1 : Form
{
// 总香蕉数量
private const int TotalBananas = 100000;
// 剩余香蕉数量
private int bananasRemaining;
// 锁对象
private readonly object lockObject = new object();
// 猴子列表
private List<Monkey> monkeys = new List<Monkey>();
// 随机数生成器
private Random random = new Random();
public Form1()
{
InitializeComponent();
InitializeMonkeys(); // 初始化猴子
bananasRemaining = TotalBananas; // 设置剩余香蕉数量为总数量
}
/// <summary>
/// 初始化猴子的方法
/// </summary>
private void InitializeMonkeys()
{
monkeys.Clear(); // 清空之前的猴子数据
// 创建10只猴子并添加到列表中
for (int i = 0; i < 10; i++)
{
Monkey monkey = new Monkey($"猴子 {i + 1}");
monkeys.Add(monkey);
}
}
/// <summary>
/// 点击 开始 按钮的事件处理方法
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnStart_Click(object sender, EventArgs e)
{
// 为新一轮重置剩余香蕉数量
bananasRemaining = TotalBananas;
// 重置每只猴子抓到的香蕉数量
foreach (var monkey in monkeys)
{
monkey.Reset();
}
try
{
var tasks = new List<Task>();
// 为每只猴子创建抢夺香蕉的任务
foreach (var monkey in monkeys)
{
tasks.Add(Task.Run(() => monkey.GrabBananas(lockObject, ref bananasRemaining, random)));
}
// 等待所有猴子的任务完成
await Task.WhenAll(tasks);
// 显示抢夺结果
DisplayResults();
}
catch (Exception ex)
{
// 捕获异常并显示错误信息
MessageBox.Show($"发生错误: {ex.Message}");
}
}
/// <summary>
/// 显示抢夺结果的方法
/// </summary>
private void DisplayResults()
{
// 使用Invoke确保在UI线程中更新
this.Invoke((MethodInvoker)delegate
{
// 构建结果字符串
string result = $"剩余香蕉数量: {bananasRemaining}\n\n";
foreach (var monkey in monkeys)
{
result += $"{monkey.Name} 抢到了 {monkey.BananasGrabbed} 个香蕉。\n";
}
// 显示结果消息框
MessageBox.Show(result);
});
}
}
public class Monkey
{
public string Name { get; } // 猴子的名称
public int BananasGrabbed { get; private set; } // 抢到的香蕉数量
public Monkey(string name)
{
Name = name;
BananasGrabbed = 0; // 初始化抢到的数量
}
/// <summary>
/// 重置抓到的香蕉数量
/// </summary>
public void Reset()
{
BananasGrabbed = 0; // 为新一轮重置数量
}
/// <summary>
/// 抢夺香蕉的方法
/// </summary>
/// <param name="lockObject"></param>
/// <param name="bananasRemaining"></param>
/// <param name="random"></param>
public void GrabBananas(object lockObject, ref int bananasRemaining, Random random)
{
while (true)
{
lock (lockObject) // 线程安全锁
{
if (bananasRemaining <= 0) {
break; // 如果没有香蕉了,退出循环
}
// 随机抢夺1到100个香蕉
int grabAmount = random.Next(1, 101);
if (grabAmount > bananasRemaining)
{
grabAmount = bananasRemaining; // 如果抢的数量超过剩余数量,则只抢剩余数量
}
bananasRemaining -= grabAmount; // 更新剩余香蕉数量
BananasGrabbed += grabAmount; // 更新猴子抓到的数量
}
// 使用延迟,确保每只猴子都能抢到
Task.Delay(random.Next(0,2 )).Wait(); // 小延迟,模拟抢夺过程
}
}
}
}
代码分析
-
初始化和设置:
- 在
Form1
的构造函数中,我们初始化了10只猴子,并为它们分配了名称和初始的抓到香蕉数量。总香蕉数设置为100,000个。
- 在
-
抢香蕉逻辑:
- 每只猴子通过
GrabBananas
方法抢夺香蕉。方法通过lock
关键字保证了对剩余香蕉数量的线程安全访问。 - 每次抢夺时,猴子随机获取1到100个香蕉,如果请求的数量超过了剩余的香蕉,就只抢剩余的部分。
- 为了模拟猴子抢夺过程的延迟,使用了
Task.Delay
。
- 每只猴子通过
-
任务执行和UI更新:
- 当点击 "开始" 按钮时,程序启动所有猴子的抢香蕉任务,并使用
Task.WhenAll
等待所有任务完成。 - 当所有猴子完成任务后,我们通过
DisplayResults
方法显示每只猴子抓到的香蕉数量。
- 当点击 "开始" 按钮时,程序启动所有猴子的抢香蕉任务,并使用
-
线程安全:
- 由于多个猴子同时修改
bananasRemaining
变量,我们使用了lock
来保证对这个共享资源的访问是安全的。
- 由于多个猴子同时修改
designer代码如下:
namespace Monkey
{
partial class Form1
{
// 按钮组件
private System.Windows.Forms.Button btnStart;
/// <summary>
/// 初始化UI组件的方法
/// </summary>
private void InitializeComponent()
{
// 创建按钮实例
this.btnStart = new System.Windows.Forms.Button();
this.SuspendLayout(); // 开始布局
//
// btnStart
//
this.btnStart.Location = new System.Drawing.Point(100, 120); // 设置按钮位置,稍微向下移动
this.btnStart.Name = "btnStart"; // 设置按钮名称
this.btnStart.Size = new System.Drawing.Size(120, 60); // 设置按钮大小,稍微增大
this.btnStart.TabIndex = 0; // 设置按钮的Tab索引
this.btnStart.Text = "开始"; // 设置按钮显示的文本为中文
this.btnStart.UseVisualStyleBackColor = true; // 使用系统视觉样式
this.btnStart.Click += new System.EventHandler(this.btnStart_Click); // 绑定点击事件
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); // 设置自动缩放的尺寸
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; // 使用字体自动缩放模式
this.ClientSize = new System.Drawing.Size(320, 240); // 设置窗体大小
this.Controls.Add(this.btnStart); // 将按钮添加到窗体控件集合中
this.Name = "Form1"; // 设置窗体名称
this.Text = "猴子香蕉模拟器"; // 设置窗体标题为中文
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; // 窗体启动时居中显示
this.BackColor = System.Drawing.Color.LightYellow; // 设置窗体背景颜色为淡黄色
this.ResumeLayout(false); // 结束布局
}
}
}
运行截图:
总结
通过这个例子,我们可以学到如何在 C# 中实现并发编程,特别是在多线程环境下如何管理共享资源(如剩余香蕉数量)。lock
确保了线程安全,而 Task.Run
和 async/await
让我们的代码可以并行执行而不阻塞UI线程。通过合理的设计和线程管理,我们实现了一个模拟猴子抢香蕉的游戏。