天天看点

冯诺依曼式计算机CPU模拟器(双核版)——北邮19/20/21计导大作业冯诺依曼式计算机CPU模拟器(双核版)

冯诺依曼式计算机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

休眠立即数毫秒。

三、思路与分析

主要问题

  1. 怎样修改先前设计的数据结构来适应多线程
  2. 如何实现多线程

修改数据结构

在保证多核心间互不干扰的前提下,显然寄存器应当是核心私有的,而内存应当是全局共享的。

容易想到的思路就是,将原先设置的寄存器增加一倍,即单变量变为一维数组,一维数组变为二维数组,如 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. 线程1读取了内存数据(100)到寄存器,并使其自减1,此时该更新后的数(99)即将从寄存器写回内存原位置
  2. 线程2同时也读取了这个数据(100),这个值在内存中尚未变更
  3. 线程1将寄存器內的值(99)写回内存
  4. 线程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);
}
           

五、实用工具

北邮19级大作业CPU模拟器指令生成及执行工具

备注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. 初始时,线程1和线程2都未被线程锁锁住
  2. 线程1先行开始抢票,锁住内存,反馈结果,解锁内存,解锁线程2(尽管它并没有被锁),锁住自己,休眠
  3. 线程2在内存解锁后开始抢票,锁住内存,反馈结果,解锁内存,解锁线程1,锁住自己,休眠
  4. 线程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>又为构建互斥量创造了便捷途径。这些都是极佳的做法,课程限制无法使用是一大遗憾。