冯诺依曼式计算机CPU模拟器(双核版)
一、课程设计要求简介
在先前设计的 单核版 基础上,增加一个核心,即实现双线程,进行指定的抢票操作。抢票功能已由给定的文件中的指令实现,只需扩展CPU核心并实现多线程支持。
对于多个核心,寄存器是各自独立的,而32KB内存则应当是共享的。
*指令输入从文件 “dict1.dic” "dict2.dic"中获取,非手动输入,只有遇到指令集中的输入操作时才从键盘读入。
*输入输出样例见 冯诺依曼结构作业_提取码BUPT,也可移步 我的Github
二、指令集
指令 | 说明 | |
---|---|---|
停机 指令 | 00000000 00000000 0000000000000000 | 停止程序执行。 |
数据传送 指令 | 00000001 00010000 0000000000000000 | 将一个立即数传送至寄存器1。 |
00000001 00010101 0000000000000000 | 将寄存器5中地址所指向的内存单元(2个字节)的内容传送至寄存器1。 | |
00000001 01010001 0000000000000000 | 将寄存器1的内容传送至寄存器5中地址所指向的内存单元(2个字节)。 | |
算术运算 指令 | 00000010 00010000 0000000000000000 | 将寄存器1内的数与一个立即数相加,结果保存至寄存器1。 |
00000010 00010101 0000000000000000 | 将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数相加,结果保存至寄存器1。 | |
00000011 00010000 0000000000000000 | 将寄存器1内的数减去一个立即数,结果保存至寄存器1。 | |
00000011 00010101 0000000000000000 | 将寄存器1内的数减去寄存器5中地址所指向的内存单元(2个字节)里存的数,结果保存至寄存器1。 | |
00000100 00010000 0000000000000000 | 将寄存器1内的数与一个立即数相乘,结果保存至寄存器1。 | |
00000100 00010101 0000000000000000 | 将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数相乘,结果保存至寄存器1。 | |
00000101 00010000 0000000000000000 | 将寄存器1内的数除以(C语言的整数除法)一个立即数,结果保存至寄存器1。 | |
00000101 00010101 0000000000000000 | 将寄存器1内的数除以(C语言的整数除法)寄存器5中地址所指向的内存单元(2个字节)里存的数,结果保存至寄存器1。 | |
逻辑运算 指令 | 00000110 00010000 0000000000000000 | 将寄存器1内的数与一个立即数做逻辑与,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) |
00000110 00010101 0000000000000000 | 将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑与,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00000111 00010000 0000000000000000 | 将寄存器1内的数与一个立即数做逻辑或,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00000111 00010101 0000000000000000 | 将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑或,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00001000 00010000 0000000000000000 | 将寄存器1内的数做逻辑非,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00001000 00000101 0000000000000000 | 将寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑非,结果仍保存至寄存器5中地址所指向的内存单元。(如果结果为真则保存1,否则保存0) | |
比较 指令 | 00001001 00010000 0000000000000000 | 将寄存器1内的数与一个立即数比较,如两数相等,则标志寄存器被修置为0,如寄存器1大,则标志寄存器被置为1,如寄存器1小,则标志寄存器被置为-1。 |
00001001 00010101 0000000000000000 | 将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数比较,如两数相等,则标志寄存器被置为0,如寄存器1大,则标志寄存器被置为1,如寄存器1小,则标志寄存器被置为-1。 | |
跳转 指令 | 00001010 00000000 0000000000000000 | 无条件跳转指令,转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 |
00001010 00000001 0000000000000000 | 如果标志寄存器内的值为0,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 | |
00001010 00000010 0000000000000000 | 如果标志寄存器内的值为1,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 | |
00001010 00000011 0000000000000000 | 如果标志寄存器内的值为-1,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 | |
输入输出 指令 | 00001011 00010000 0000000000000000 | 从输入端口读入一个整数并保存在寄存器1中。也就是从键盘读一个整数到寄存器1中。 |
00001100 00010000 0000000000000000 | 将寄存器1中的数输出到输出端口。也就是将寄存器1中的数以整数的形式输出到显示器上,同时输出一个换行符。 | |
多核版指令 | 00001101 00000000 0000000000000000 | 立即数为内存地址,请求互斥对象,用于锁住立即数所指定的内存。 |
00001110 00000000 0000000000000000 | 立即数为内存地址,释放互斥对象,释放掉锁住立即数所指定的内存的互斥对象。与上一条指令对应。 | |
00001111 00000000 0000000000000000 | 休眠立即数毫秒。 |
三、思路与分析
主要问题
- 怎样修改先前设计的数据结构来适应多线程
- 如何实现多线程
修改数据结构
在保证多核心间互不干扰的前提下,显然寄存器应当是核心私有的,而内存应当是全局共享的。
容易想到的思路就是,将原先设置的寄存器增加一倍,即单变量变为一维数组,一维数组变为二维数组,如 ax[9] 变为 ax[2][9],由CPU id来区分使用哪一组寄存器。
当然,其他思路也可行,例如,若是在线程函数中创建各寄存器变量,就省去了启动线程时传入过多参数和扩增数组的麻烦,这正是面向对象编程思想的体现。
对于内存而言,是不需要做修改的,课程已经规定了两个核心的代码段所处位置,二者不会重叠,否则将产生段错误。
多线程实现
对初学者而言,多线程是一个比较复杂的概念,事实也如此,对于大型项目,想要实现线程同步等,需要耗费较大的精力。
这里假设你阅读过附件中的PPT,或是了解过相关概念,有一定的经验,所以只着重说明一些重要内容。
运行多线程
请先在代码中包含头文件<process.h>和<windows.h>
相较C++提供的多线程,C下的更为繁琐,但是无论如何都需要提供一个包装好的函数,其返回值和参数都有特殊要求,可自行了解_beginthreadex()。多线程的实质就是同时运行这个包装好的函数。对线程的控制一般通过句柄进行,句柄就相当于变量名。
同时,由于传参个数限制为1个void*,如果要传多个参数,可以使用结构体。
根据最新课程要求,除了全局句柄外,依旧不允许使用全局变量 (但是,我就是要用)。
unsigned __stdcall func(void *p); //将被新线程调用的函数定义
HANDLE hThread1, hThread2; //声明两个句柄
...
int main() {
int id1 = 0, id2 = 1; //核心1和核心2的参数分别为0, 1
void* p1 = &id1, * p2 = &id2; //生成void*指针,用于传参
hThread1 = (HANDLE)_beginthreadex(NULL, 0, func, p1, 0, NULL);
//创建线程1,调用的函数为func,参数为p1
hThread2 = (HANDLE)_beginthreadex(NULL, 0, func, p2, 0, NULL);
//创建线程2,调用的函数为func,参数为p2
...
}
多线程的运行情况通常如下,
主线程先行启动->创建多线程->多线程并行运行->其中一个线程先结束->另一个线程结束->主线程结束
可以知道,如果不等待多线程全部结束,就继续主线程,也就是main函数中的语句,可能会导致另一个线程还未结束,程序就终止了,这样的结果不是我们所期待的,所以必须等待两个线程都结束,再返回主线程。
WaitForSingleObject(hThread1, INFINITE); //等待直到线程1结束
CloseHandle(hThread1); //关闭线程1句柄
WaitForSingleObject(hThread2, INFINITE); //等待直到线程2结束
CloseHandle(hThread2); //关闭线程2句柄
mainFuction(); //继续主函数的其他功能
线程同步(防止线程冲突)
试想,在全局共享的内存中存储有一个数据,多个线程可以同时对其进行读写,如果操作都是原子操作,即调用的函数在汇编意义上不可细分,一步完成,线程的操作先后次序是明确的,当然不会产生冲突。
但是,如果该操作不是原子操作,设想以下情景:
- 线程1读取了内存数据(100)到寄存器,并使其自减1,此时该更新后的数(99)即将从寄存器写回内存原位置
- 线程2同时也读取了这个数据(100),这个值在内存中尚未变更
- 线程1将寄存器內的值(99)写回内存
- 线程2将寄存器内的值自减1,并将更新后的数(99)写回内存
显然,最后产生的结果是,该数被两个线程修改了两次,然而值只减少了1。假设这个数据代表剩余车票张数,那么两个人将会成功抢到同一张票,这是不可接受的。
为此,我们必须保证线程同步,即避免线程冲突发生。
由上面的分析容易推断出,线程冲突的原因就在于多个线程将要同时读写同一块全局内存,并且它们进行的操作不是原子操作,在汇编的过程中有多条指令产生,线程执行的速度不一。
可以使用线程锁来解决这一矛盾。锁的具体实现,既可以直接调用库函数,也可以自己模拟,下边将探讨这一内容。
锁的操作
方式一:调库
<windows.h>为我们提供了互斥量,这是一种锁,其创建和使用途径为:
HANDLE hMutex; //事先在全局区声明句柄
...
int main() {
hMutex = CreateMutex(NULL, false, NULL); //创建互斥量
...
}
void func() {
WaitForSingleObject(hMutex, INFINITE); //等待互斥量可被申请并占用它
dosomething(); //具体的操作
ReleaseMutex(hMutex); //释放互斥量
}
若将锁设置在一个函数内部,则当线程1先行调用这一函数并进行操作时,锁的所有权就归线程1,而线程2随后进入时,将一直等待直到锁可用。
方式二:模拟
若对锁的占用时间较长,所有权可能横跨多个函数,又或是单纯想持续锁住内存的某一部分,可采用模拟的方法,即建立一个数组类型的锁结构,当申请所有权时,将其他核心的读写权限设置为0,即不可读写,而释放所有权时,再重置各核心权限。
对于想要读写一块内存的核心,必须先检查自己是否有读写权限,若无,则持续等待,若有,则正常访问。
bool memLock[2][32 * 1024]; //全局变量,模拟的内存锁
...
void lock(int id, int pos) { //加锁
while(memLock[id][pos] == 0) Sleep(10); //无读写权限,持续等待,否则内存被死锁
id == 0 ? memLock[1][pos] = 0 : memLock[0][pos] = 0; //将另一核心权限置为不可读写
}
void unlock(int id, int pos) { //释放锁
id == 0 ? memLock[1][pos] = 1 : memLock[0][pos] = 1; //无需等待,直接归还权限
}
void func(int id) { //普通读写操作
while(memLock[id][pos] == 0) Sleep(10); //无读写权限,持续等待
dosomething(); //获得权限,进行对应操作,涉及权限归还时应调用unlock()
}
原子操作:输出
保证了线程访问全局内存时不会冲突,并不代表锁就没有其他用处了。
类似printf这样的函数,在C语言中只是一条语句,但在汇编中将被翻译为多条语句,对应多种参数,这说明它不是原子操作。
不是原子操作,在程序输出时就会有显著的问题,多个线程的输出内容时常混杂在一起,甚至有时连一个完整的单词都没有。
为此,有必要将非原子操作利用锁转换为原子操作。
一个线程在输出前,必须先获取IO锁的权限,然后进行它的输出工作,输出完毕后,交还权限。这样就实现了原子操作。
void output() {
WaitForSingleObject(IOMutex, INFINITE);
printf("something");
ReleaseMutex(IOMutex);
}
四、参考代码
鼓励独立思考,独立解题,请勿直接复制,否则查重无法通过
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <process.h>
#include <windows.h>
typedef struct Byte { //stored by bit
unsigned char bit0 : 1, bit1 : 1, bit2 : 1, bit3 : 1,
bit4 : 1, bit5 : 1, bit6 : 1, bit7 : 1;
}Byte;
typedef union Memory {
unsigned char data;
Byte b;
}Memory;
typedef union Register {
short data;
struct {
Byte b0;
Byte b1;
}b;
}Register;
const int START = 16384; //dataSegment starts from 16384
const int CODESTART[2] = { 0,256 }; //beginning of codeSegment of core 1 and core 2
Memory memory[32 * 1024]; //32Kb Memory
bool memLock[2][32 * 1024]; //Memory lock
bool threadLock[2]; //Thread lock. Threads take turns to get tickets
unsigned int PC[2]; //ProcessCounter
Register IR[2]; //InstructionRegister, stores only 2 bytes
Register ax[2][9]; //GeneralRegisters, store only 2 bytes (don't use ax[][0])
int FR[2]; //FlagsRegister
HANDLE hThread1, hThread2, IOMutex;
void ReadToMemory(FILE* fp, int begin);
unsigned __stdcall process(void* p);
void move1(int dest, int src, int id);
void move2(int dest, int src, int id);
void cal(int dest, int src, int mode, int id);
void AND(int dest, int src, int id);
void OR(int dest, int src, int id);
void NOT(int dest, int mode, int id);
void cmp(int dest, int src, int id);
void show(int id);
void checkLock(int pos, int id);
int main() {
FILE* fp1 = fopen("dict1.dic", "r"), * fp2 = fopen("dict2.dic", "r");
ReadToMemory(fp1, CODESTART[0]);
ReadToMemory(fp2, CODESTART[1]);
fclose(fp1);
fclose(fp2);
memory[16385].data = 100;
int id1 = 0, id2 = 1;
void* p1 = &id1, * p2 = &id2;
IOMutex = CreateMutex(NULL, FALSE, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, process, p1, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, process, p2, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
CloseHandle(hThread1);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread2);
printf("\ncodeSegment :\n");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 8; j++) {
if (j) putchar(' ');
printf("%d", (memory[i * 32 + j * 4].data << 24) | (memory[i * 32 + j * 4 + 1].data << 16) |
(memory[i * 32 + j * 4 + 2].data << 8) | memory[i * 32 + j * 4 + 3].data);
}
putchar('\n');
}
printf("\ndataSegment :\n");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
if (j) putchar(' ');
printf("%hd", (memory[START + i * 32 + j * 2].data << 8) | memory[START + i * 32 + j * 2 + 1].data);
}
putchar('\n');
}
return 0;
}
void move1(int dest, int src, int id) { //immed/memory->ax
checkLock(src, id);
short data = (memory[src].data << 8) | memory[src + 1].data;
ax[id][dest].data = data;
}
void move2(int dest, int src, int id) { //ax->memory
checkLock(dest, id);
memory[dest].data = ax[id][src].data >> 8;
memory[dest + 1].data = ax[id][src].data & 255;
}
void cal(int dest, int src, int mode, int id) { //calculate, mode: 0->add, 1->sub, 2->mul, 3->div
checkLock(src, id);
short data = (memory[src].data << 8) | memory[src + 1].data;
if (mode == 0) ax[id][dest].data += data;
else if (mode == 1) ax[id][dest].data -= data;
else if (mode == 2) ax[id][dest].data *= data;
else if (mode == 3) ax[id][dest].data /= data;
}
void AND(int dest, int src, int id) { //logic and
checkLock(src, id);
short data = (memory[src].data << 8) | memory[src + 1].data;
(ax[id][dest].data & data) ? (ax[id][dest].data = 1) : (ax[id][dest].data = 0);
}
void OR(int dest, int src, int id) { //logic or
checkLock(src, id);
short data = (memory[src].data << 8) | memory[src + 1].data;
(ax[id][dest].data | data) ? (ax[id][dest].data = 1) : (ax[id][dest].data = 0);
}
void NOT(int dest, int mode, int id) { //logic not
checkLock(dest, id);
if (mode == 0) ax[id][dest].data = !ax[id][dest].data;
else {
if (memory[dest].data || memory[dest + 1].data) memory[dest].data = memory[dest + 1].data = 0;
else memory[dest + 1].data = 1;
}
}
void cmp(int dest, int src, int id) { //compare
checkLock(src, id);
short data = (memory[src].data << 8) | memory[src + 1].data;
if (ax[id][dest].data == data) FR[id] = 0;
else if (ax[id][dest].data > data) FR[id] = 1;
else FR[id] = -1;
}
void show(int id) { //show you all the information
WaitForSingleObject(IOMutex, INFINITE);
printf("id = %d\nip = %hd\nflag = %d\nir = %hd\n", id + 1, PC[id] + CODESTART[id], FR[id], IR[id].data);
printf("ax1 = %hd ax2 = %hd ax3 = %hd ax4 = %hd\n", ax[id][1].data, ax[id][2].data, ax[id][3].data, ax[id][4].data);
printf("ax5 = %hd ax6 = %hd ax7 = %hd ax8 = %hd\n", ax[id][5].data, ax[id][6].data, ax[id][7].data, ax[id][8].data);
ReleaseMutex(IOMutex);
}
void ReadToMemory(FILE* fp, int begin) { //read data and store in memory
unsigned int cnt = begin;
while (1) {
char line[33] = { 0 };
if (fscanf(fp, "%s", line) != EOF) {
if (line[0] < '0' || line[0] > '1') break; //unavailable data
for (int i = 0; i < 4; i++) {
unsigned char _data = 0;
for (int j = 0; j < 8; j++) _data = _data * 2 + line[i * 8 + j] - '0';
memory[cnt++].data = _data;
}
}
else break;
}
}
unsigned __stdcall process(void* p) { //main function, to judge and operate
int id = *((int*)p);
while (1) {
IR[id].data = (memory[PC[id]].data << 8) | memory[PC[id] + 1].data;
int cmd = IR[id].data >> 8;
int from = IR[id].data & 15, to = (IR[id].data & 240) >> 4;
bool noJump = 1;
if (cmd == 0) { //shut down
PC[id] += 4;
show(id);
break;
}
if (cmd == 1) { //move
if (from == 0) move1(to, PC[id] + 2 + CODESTART[id], id); //immed->ax[id]
else if (from >= 5) move1(to, ax[id][from].data, id); //memory->ax[id]
else move2(ax[id][to].data, from, id); //ax[id]->memory
}
else if (cmd == 2) { //add
from == 0 ? cal(to, PC[id] + 2 + CODESTART[id], 0, id) : cal(to, ax[id][from].data, 0, id);
}
else if (cmd == 3) { //subtract
from == 0 ? cal(to, PC[id] + 2 + CODESTART[id], 1, id) : cal(to, ax[id][from].data, 1, id);
}
else if (cmd == 4) { //multiply
from == 0 ? cal(to, PC[id] + 2 + CODESTART[id], 2, id) : cal(to, ax[id][from].data, 2, id);
}
else if (cmd == 5) { //divide
from == 0 ? cal(to, PC[id] + 2 + CODESTART[id], 3, id) : cal(to, ax[id][from].data, 3, id);
}
else if (cmd == 6) { //logic and
from == 0 ? AND(to, PC[id] + 2 + CODESTART[id], id) : AND(to, ax[id][from].data, id);
}
else if (cmd == 7) { //logic or
from == 0 ? OR(to, PC[id] + 2 + CODESTART[id], id) : OR(to, ax[id][from].data, id);
}
else if (cmd == 8) { //logic not
from == 0 ? NOT(to, 0, id) : NOT(ax[id][from].data, 1, id);
}
else if (cmd == 9) { //compare
from == 0 ? cmp(to, PC[id] + 2 + CODESTART[id], id) : cmp(to, ax[id][from].data, id);
}
else if (cmd == 10) { //jump
short data = (memory[PC[id] + 2 + CODESTART[id]].data << 8) | memory[PC[id] + 3 + CODESTART[id]].data;
if (from == 0 || from == 1 && FR[id] == 0 || from == 2 && FR[id] == 1 || from == 3 && FR[id] == -1) {
PC[id] += data;
noJump = 0;
show(id);
}
}
else if (cmd == 11) { //input
WaitForSingleObject(IOMutex, INFINITE);
printf("in:\n");
ReleaseMutex(IOMutex);
scanf("%hd", &ax[id][to].data);
}
else if (cmd == 12) { //output
WaitForSingleObject(IOMutex, INFINITE);
printf("id = %d out: %hd\n", id + 1, ax[id][to].data);
ReleaseMutex(IOMutex);
}
else if (cmd == 13) { //lock
short pos = (memory[PC[id] + 2 + CODESTART[id]].data << 8) + memory[PC[id] + 3 + CODESTART[id]].data;
while (threadLock[id]) Sleep(10);
checkLock(pos, id);
id == 0 ? (memLock[1][pos] = 1) : (memLock[0][pos] = 1);
}
else if (cmd == 14) { //release
short pos = (memory[PC[id] + 2 + CODESTART[id]].data << 8) + memory[PC[id] + 3 + CODESTART[id]].data;
if (id == 0) {
memLock[1][pos] = 0;
threadLock[0] = 1;
threadLock[1] = 0;
}
else {
memLock[0][pos] = 0;
threadLock[0] = 0;
threadLock[1] = 1;
}
}
else if (cmd == 15) { //sleep
short sleepTime = (memory[PC[id] + 2 + CODESTART[id]].data << 8) + memory[PC[id] + 3 + CODESTART[id]].data;
Sleep(sleepTime);
}
if (noJump) {
PC[id] += 4;
show(id);
}
}
_endthreadex(0);
return 0;
}
void checkLock(int pos, int id) {
while (memLock[id][pos]) Sleep(10);
}
五、实用工具
备注1:感谢19级学长制作的工具,为debug提供了很大帮助。生成代码时,请注意按照指令集的规定进行生成,例如操作对象处于高位还是低位,通常都会有影响。同时,其中所给的测试样例具有一些指令集未说明的情况,我写的代码只遵循指令集的要求,虽然OJ上可以通过,但不适应这些情况。
备注2:工具中的执行操作是无效的,因为链接似乎挂掉了。
六、扩展思考
关于多线程中的休眠
线程休眠对于多线程编程有重要意义,由于其涉及到操作系统原理,在此仅简单谈几点。
指令集中规定的指令为休眠立即数毫秒,具体体现在样例中为2ms。实现非常简单,调用Sleep()函数即可,其单位为ms。
但是,为何要进行休眠操作?可以测试,如果将样例中的休眠指令移除,将有一个线程在一段时间內抢到大量的票,这是因为指令速度太快了。但是,让线程休眠2ms,该线程真的就精确地休眠了2ms吗?其背后的控制原理如何呢?
进一步测试可以发现,当线程1卖出50张票时,其休眠总时长理论上应为100ms,而实际上却超过100ms数倍之多。将休眠时间更改为2~20內的常数,可以发现休眠用时相差无几,这应当和CPU的调度有关,因为操作系统可能不接受太短时间的休眠,换句话说,即使真的只休眠了2ms,该线程被唤醒后,仍要与电脑上的其他线程竞争CPU,未必就会立即得到CPU资源分配。
因此,这个休眠2ms很大程度上不是精确地休眠2ms,而更多地是主动让出CPU资源给其他线程,使本程序中其他线程也能抢到票。
再进一步思考,在抢票的过程中,线程1和线程2先后启动,其执行的指令是相同的,理论上来说应该轮流获得内存锁,抢票,释放内存锁,休眠,那么抢票结果也应当是交替的,即线程1抢得一张票后,下一张会由线程2抢得。但事实真的是这样吗?可以发现,有时会出现同一线程连续抢到几张票的情况。
这不仅与抢票期间的操作并不是真正意义上的原子操作有关,也与休眠有关。在休眠期间,其他线程可能完成多次抢票操作。为什么?因为休眠时间不是精确的2ms!假设线程1抢到票后,从休眠到继续运行花了18ms,而线程2休眠2次,总共用时却只有16ms,是否就能抢到2张票呢?
如何解决这个问题?下面来讨论一种可行的方法。(当然也可以不解决,并没有硬性要求)
实现轮流抢票
如果你仔细观察参考代码输出,就会发现并没有出现一个线程连续抢到多张票的情况
得益于使用全局变量,我们能够模拟各种锁,并且使其在整个程序中都生效。
要想使得线程交替抢票,自然也可以考虑新增一种锁,这个锁的职责就是当某一线程抢到票后,阻止其再次抢票,直到另一线程抢到票为止。
来尝试构思这个过程:
- 初始时,线程1和线程2都未被线程锁锁住
- 线程1先行开始抢票,锁住内存,反馈结果,解锁内存,解锁线程2(尽管它并没有被锁),锁住自己,休眠
- 线程2在内存解锁后开始抢票,锁住内存,反馈结果,解锁内存,解锁线程1,锁住自己,休眠
- 线程1休眠结束后,再等待自己解锁和内存解锁,两个锁都解除后,重复上述步骤
此处,所谓的上锁和解锁都只是变量逻辑值的变化,即由0变1或由1变0。
具体的实现十分简单,这里不再说明,可参考上方代码的lock()和release()。
我就是要用全局变量
正所谓上有政策,下有对策,没有什么能够阻止一个头铁的人。
之前已经提到过,开启新线程时,只能传入一个void*指针,但该指针的类型不定,可以指向结构体变量,因而可以将多种所需变量放入结构体中,再由指针传入,供线程使用。
由此启发我们,如果不能够使用表面上的全局变量,何不将main里的局部变量变为全局变量!
将所有希望作为全局变量的变量在主函数中定义后,把它们的指针封装到结构体內,CPU各核心私有的数据也可封装进结构体中,从而在具体的各个线程里,实际上得以共同访问“全局变量”。
注意,传入的是void*指针,必须先在线程函数开始时将其转换为具体的结构体指针,再把其中含有的变量解包出来,这样,先前设计的函数就无需过多地更改参数列表。
参考下列代码:
typedef struct Byte {
unsigned char bit0 : 1, bit1 : 1, bit2 : 1, bit3 : 1,
bit4 : 1, bit5 : 1, bit6 : 1, bit7 : 1;
}Byte;
typedef union Memory {
unsigned char data;
Byte b;
}Memory;
typedef union Register {
short data;
struct {
Byte b0;
Byte b1;
}b;
}Register;
typedef struct DataShell {
int id; //CPU id
Memory* memory; //全局内存
bool* memLock, * threadLock; //全局内存锁,线程锁
unsigned int* PC; //后续均为私有寄存器
Register* IR, * ax;
int* FR;
}DataShell;
unsigned __stdcall func(void* p) { //线程函数定义及实现
DataShell* ptr = (DataShell*)p; //转换void*指针为具体指针,便于下面进行解包
Memory* memory = ptr->memory;
bool* memLock = ptr->memLock, * threadLock = ptr->threadLock;
unsigned int* PC = ptr->PC;
Register* IR = ptr->IR, * ax = ptr->ax;
int* FR = ptr->FR;
dosomething();
...
}
HANDLE hThread1, hThread2;
int main() {
Memory memory[32 * 1024] = { 0 };
bool memLock[2][32 * 1024] = { 0 };
bool threadLock[2] = { 0 };
unsigned int PC[2] = { 0 };
Register IR[2] = { 0 };
Register ax[2][9] = { 0 };
int FR[2] = { 0 };
DataShell s1 = { 0, memory, memLock[0], threadLock, &PC[0], &IR[0], ax[0], &FR[0] }, //将变量封装进结构体
s2 = { 1, memory, memLock[0], threadLock, &PC[1], &IR[1], ax[1], &FR[1] };
void* p1 = &s1, * p2 = &s2; //生成void*指针,用于传参
hThread1 = (HANDLE)_beginthreadex(NULL, 0, func, p1, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, func, p2, 0, NULL);
...
}
在C++下的构建
在单核版中提到过,C++提供的面向对象编程使得我们可以构建两个CPU对象,它们拥有自己的数据成员,不会互相干扰,又可共享使用全局内存。同时,C++11的标准库提供的<thread>能够方便地实现多线程,而<mutex>又为构建互斥量创造了便捷途径。这些都是极佳的做法,课程限制无法使用是一大遗憾。