目录
1.实验目的
探索、理解并掌握操作系统同步机制的设计和实现机理,针对所谓的银行账户转账同步问题,构建基于 Peterson 算法的同步解决方案以及基于 Windows(或 Linux)操作系统同步机制(主要是互斥机制)的解决方案,并进行分析和比较
2.实验内容
针对银行账户转账同步问题,分析、设计和利用 C 语言编程实现基于 Peterson 算法的同步解决方案,以及基于Windows(或 Linux)操作系统同步机制的相应解决方案,并就自编同步机制与操作系统自身同步机制的效率进行比较和分析。
3.实验要求
同步机制及应用编程实现与比较实验功能设计要求:
(1)银行账户转账同步问题的抽象及未采取同步控制情况下的编程实现;
(2)基于 Peterson 算法的银行账户转账同步问题解决方案;
(3)基于 Windows(或 Linux)操作系统同步机制的银行账户转账同步问题解决方案;
(4)Peterson 算法同步机制和 Windows(或 Linux)操作系统同步机制的效率比较。
4.实验环境
开发环境 | Visual Studio |
运行环境 | windows |
测试环境 | 终端(笔记本) |
- 实验设计与实现
5.1实验总体设计
5.1.1Account类设计
受控的操作流程:通过定义的 deposit 和 withdraw 方法,类对存款和取款操作进行了控制,防止出现不合法的操作。例如,withdraw 方法确保余额不会因为取款操作而变成负数。这让账户操作变得安全、可预测。
安全性:由于所有的余额操作都被封装在方法内,可以轻松加入一些额外的规则或验证,比如防止负存款、记录日志、验证交易限额等。这能避免外部代码进行未经检查的非法操作。
// 自定义账户类
class Account {
private:
int balance; // 余额
public:
// 构造函数,初始化余额
Account(int initialBalance) : balance(initialBalance) {}
// 存款操作
void deposit(int amount) {
balance += amount;
}
// 取款操作,返回是否成功(防止余额为负)
bool withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
return true;
}
else {
return false; // 余额不足
}
}
// 获取当前余额
int getBalance() const {
return balance;
}
};
5.1.2主函数设计
创建两个线程 t1 和 t2,分别传入线程 ID(0 和 1),这两个线程会执行 transfer 函数。
使用 join() 等待两个线程完成转账操作。
打印账户 1 和账户 2 的最终余额。
1.int main() {
2. // 创建两个线程
3. std::thread t1(transfer, 0); // 线程 1 的 id 为 0
4. std::thread t2(transfer, 1); // 线程 2 的 id 为 1
5.
6. t1.join();
7. t2.join();
8.
9. std::cout << "Final Account Balances: " << std::endl;
10. std::cout << "Account 1: " << account1.get_balance() << std::endl;
11. std::cout << "Account 2: " << account2.get_balance() << std::endl;
12.
13. return 0;
14.}
5.1.3未采取同步控制
在已有账户类的基础上,我们创建两个账户
1.// 全局账户对象
2.BankAccount account1(0);
3.BankAccount account2(0);
设计转账函数:转账函数流程图如下:
转账函数具体实现:
1.// 未同步的转账函数
2.void transfer(int thread_id) {
3. int nLoop = 0;
4. int nTemp1, nTemp2, nRandom;
5.
6. do {
7. nRandom = rand() % 100; // 随机生成转账金额
8. nTemp1 = account1.get_balance();
9. nTemp2 = account2.get_balance();
10.
11. // 模拟转账操作
12. account1.deposit(nRandom);
13. account2.withdraw(nRandom);
14.
15. // 输出每次操作后的账户余额
16. std::cout << "Thread " << thread_id << " - Step " << nLoop + 1 << ": "
17. << "Account 1: " << account1.get_balance() << ", "
18. << "Account 2: " << account2.get_balance() << std::endl;
19.
20. nLoop++;
21. } while (account1.get_balance()+ account2.get_balance()==0);
22.}
主函数如下:我们创建了两个线程
1.int main() {
2. // 创建两个线程
3. std::thread t1(transfer, 1);
4. std::thread t2(transfer, 2);
5.
6. t1.join();
7. t2.join();
8.
9. std::cout << "Final Account Balances: " << std::endl;
10. std::cout << "Account 1: " << account1.get_balance() << std::endl;
11. std::cout << "Account 2: " << account2.get_balance() << std::endl;
12.
13. return 0;
14.}
5.1.4peterson解决方案
1.// Peterson 算法的全局变量
2.bool flag[2] = { false, false };
3.int turn;
flag
- 定义:flag 是一个布尔数组,通常有两个元素 flag[0] 和 flag[1],分别对应两个进程。
- 作用:用于表示每个进程是否希望进入临界区。具体来说:
- 如果 flag[i] 为 true,则进程 i 想进入临界区。
- 如果 flag[i] 为 false,则进程 i 不想进入临界区。
turn
- 定义:turn 是一个整型变量,通常用来指示哪个进程应该进入临界区。
- 作用:用于解决两个进程之间的竞争问题,确保在临界区的进程是合适的。具体来说:
- 当一个进程想进入临界区时,它会将 turn 设置为另一个进程的 ID,以表示它让出进入临界区的权利给另一个进程。
- 这样可以避免两个进程同时进入临界区的情况,确保了互斥性。
设计peterson算法的同步函数
在 Peterson 算法中,将 turn 设置为另一个线程的 ID 的主要目的是为了实现线程之间的公平性和避免饥饿。以下是这一设计决策的几个重要原因:
如果没有优先让另一个线程进入临界区的机制,某个线程可能会一直处于进入临界区的状态,而另一个线程则可能永远无法获得进入的机会。通过设置 turn,确保每个线程都有机会执行临界区代码。
1.// Peterson 算法的同步函数
2.void enter_critical_section(int thread_id) {
3.
4. //如果 thread_id 是 0,那么 other 就是 1,反之亦然
5. int other = 1 - thread_id;
6.
7. //表示当前线程希望进入临界区
8. flag[thread_id] = true;
9.
10. //设置 turn 为另一个线程的 ID
11. turn = other;
12.
13. //核心,只有当另一个线程不想进入临界区 (flag[other] == false) 或者 turn 变为当前线程时
14. //才能退出循环,进入临界区
15. while (flag[other] && turn == other); //flag[other]==0 或者 turn = thread_id时,该线程才能进入临界区
16.}
设计peterson算法,用于线程离开临界区时的操作
1.void leave_critical_section(int thread_id) {
2. flag[thread_id] = false;
}
转账操作的函数:通过使用临时变量 nTemp1 和 nTemp2 来暂存账户余额,可以保证在临界区内对账户余额的读取和修改是同步的,并且在修改账户余额前不会发生不一致的情况。
临时变量先获取当前的余额,之后在计算结束后再统一更新账户余额,从而减少意外并发问题。
1.// 使用 Peterson 算法进行同步的转账函数
2.void transfer(int thread_id) {
3. int nLoop = 0;
4. int nTemp1, nTemp2, nRandom;
5.
6. do {
7. enter_critical_section(thread_id); // 进入临界区
8.
9. nRandom = rand() % 100; // 随机生成转账金额
10.
11. //在多线程环境中,两个线程可能会同时读取和修改共享变量(例如 nAccount1 和 nAccount2),导致竞态条件。竞态条件可能导致读取和写入共享数据的顺序出现错误,从而导致错误的结果。
12.
13. //通过使用临时变量 nTemp1 和 nTemp2 来暂存账户余额,
14. //可以保证在临界区内对账户余额的读取和修改是同步的,
15. //并且在修改账户余额前不会发生不一致的情况。
16. //临时变量先获取当前的余额,之后在计算结束后再统一更新账户余额,
17. //从而减少意外并发问题。
18. nTemp1 = account1.get_balance();
19. nTemp2 = account2.get_balance();
20.
21. // 模拟转账操作
22. account1.deposit(nRandom);
23. account2.withdraw(nRandom);
24.
25. //加上条件
26. if (account1.get_balance() + account2.get_balance() != 0) {
27. break;
28. }
29. // 输出每次操作后的账户余额
30. std::cout << "Thread " << thread_id << " - Step " << nLoop + 1 << ": "
31. << "Account 1: " << nTemp1 << ", "
32. << "Account 2: " << nTemp2 << std::endl;
33.
34. leave_critical_section(thread_id); // 离开临界区
35.
36. nLoop++;
37. } while (nLoop < 1000000);
38.}
5.1.5windows同步机制
核心函数设计
HANDLE 是 Windows API 中用于表示对象的一个类型,广泛用于各种系统资源的操作,如线程、进程、互斥量、文件、事件等。它是一个指向内部数据结构的指针,提供了一种间接访问这些资源的方式。
1.加锁过程
当一个线程需要访问共享资源时,它会尝试获得互斥量的锁:
请求锁: 线程调用 WaitForSingleObject(hMutex, INFINITE)(在 Windows 中)或 pthread_mutex_lock(&mutex)(在 POSIX 中),请求获得互斥量的控制权。
锁定成功: 如果互斥量当前没有被其他线程锁定,线程将成功获得锁,继续执行临界区的代码。
锁定失败: 如果互斥量已被其他线程锁定,调用线程将被阻塞,直到锁被释放。
只有获得锁的线程可以进入临界区,执行对共享资源的操作(如修改账户余额)。
其他试图获取锁的线程会在此处等待,确保同一时间只有一个线程在修改共享数据,避免数据冲突和不一致。
2.解锁过程:当线程完成了对共享资源的操作后,需要释放互斥量,允许其他线程访问:
线程调用 ReleaseMutex(hMutex)(在 Windows 中)或 pthread_mutex_unlock(&mutex)(在 POSIX 中)释放锁。
1.DWORD WINAPI Transfer(LPVOID lpParameter) {
2. int nLoop = 0;
3. int nRandom;
4. int threadID = *(int*)lpParameter; // 获取线程 ID
5.
6. while (nLoop < 1000) { // 假设我们限制循环 1000 次
7. nRandom = rand() % 100; // 限制随机转账金额为 0 到 99 之间
8.
9. // 加锁保护临界区
10. WaitForSingleObject(hMutex, INFINITE); //INFINITE 是一个常量,表示一个无限的时间值,表示线程一直等待,直到某一个条件实现
11.
12. // 检查并进行转账操作,确保不会导致账户 2 余额不足
13. if (nAccount2.withdraw(nRandom)) {
14. nAccount1.deposit(nRandom);
15.
16. // 使用 cout 打印转账信息
17. cout << "线程 " << threadID << ": 转账 " << nRandom
18. << " 元,账户 1 余额: " << nAccount1.getBalance()
19. << " 元,账户 2 余额: " << nAccount2.getBalance() << " 元" << endl;
20. }
21.
22. // 解锁
23. ReleaseMutex(hMutex);
24.
25. nLoop++;
26. Sleep(10); // 模拟处理时间
27. }
28.
29. return 0;
30.}
6.实验结果
6.1未采取同步控制运行
可能会出现以下情况:
- 多线程输出混乱,输出语句不完整
- 单线程占用资源
没有对 account1 和 account2 的访问进行同步。在多线程环境中,多个线程同时访问和修改共享资源(如账户余额)可能导致数据不一致和竞争条件。此时一个线程可能会在另一个线程完成之前反复执行转账操作