122
社区成员




这个作业属于哪个课程 | https://bbs.csdn.net/forums/fzusdn-0831 |
---|---|
这个作业要求在哪里 | https://bbs.csdn.net/topics/617335682 |
个人学号 | 102101501 |
结对成员学号 | 102101409 |
GitHub 仓库地址 | https://github.com/MisMOONMORE/2023FZU_SE_sudoku |
如何实现异步:JavaScript的事件循环机制Event Loop
在JavaScript当中,任务被分成同步任务和异步任务两种,当整个Script进入运行之后,同步任务将进入主线程依次执行,而异步任务将被分为宏任务和微任务。
当主线程内的任务执行完毕后,检查微任务队列,如果有任务,进入主线程全部执行;如果没有,从宏任务队列读取下一个宏任务。
每执行完一个宏任务清空一次微任务队列。
在本次任务中,九个数独对象通过事件的方式绑定到同一个求解按钮上,每个对象又分别绑定了一个求解计算事件回调函数,主线程运行到该回调函数时,会将该回调函数push进事件循环队列,等待同步执行代码执行完后再执行,从执行结果上观察如同和同步代码是并发的,异步代码也不会阻碍同步代码的执行。
JavaScript的多线程实现:Web Workers(仅存在于构想)
具体步骤
主线程中新建worker对象(需要多个线程的情况下定义worker数组,一个worker对应一个线程)- >主线程中放置事件监听器 -> 因为worker无法直接操作DOM内容,通过postMessage方法传递必要的方法、数值等 -> 在worker.js中放置事件监听器 -> 在worker.js中写有复杂计算的函数,把结果同样通过postMessage传递给主线程。
在这里,主线程和worker.js就如同主函数和主函数中被调用的函数,可以把postMessage理解成把运算结果return。
注意:绝大部分浏览器出于安全考虑会禁用来自本地文件的Web Workers,利用GitHub Page传到服务器上才能运行。
代码示例:
//在主线程中
var workers = [];
var worker = new Worker("worker.js");
worker.addEventListener('message', function(event) {
//要收取的消息(在sudoku_test里本来是打算直接把Cells数组直接传过去操作的所以就没监听)
});
worker.postMessage({
//要传递的信息
});
workers.push(worker);
//在worker.js中
self.addEventListener('message', function(event) {
var data = event.data.data;//传过来的数值存入data变量
//此处完成复杂的计算
self.postMessage(result);//传回
});
求解算法:回溯算法
为了优化速度,我们首先去掉已经用过的数字,按照1-9的顺序往下填充。如果有一个格子没有数可填,则回溯到前一个格子,用不同的数填充。
this.ToggleShowAnswer = function () {
if (this.ShowAnswer) {
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
this.Answer[i][j].N = this.Cells[i][j].N;
this.Answer[i][j].Fixed = this.Cells[i][j].Fixed;
this.Answer[i][j].OptNumbes = this.Cells[i][j].OptNumbes;
this.Answer[i][j].InConflict = this.Cells[i][j].InConflict;
}
}
if (this.solveSudoku(this.Answer) != true) {
alert('No solution!');
}
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
this.beforeAnswer[i][j].N = this.Cells[i][j].N;
this.beforeAnswer[i][j].Fixed = this.Cells[i][j].Fixed;
this.beforeAnswer[i][j].OptNumbes = this.Cells[i][j].OptNumbes;
this.beforeAnswer[i][j].InConflict = this.Cells[i][j].InConflict;
}
}
this.Cells = this.Answer;
} else {
this.Cells = this.beforeAnswer;
}
this.ShowAnswer = !this.ShowAnswer;
this.EditPannel = null;
this.EditingCellIndex = null;
return this.ShowAnswer;
};
this.solveSudoku = function(matrix) {
function isSafe(matrix, row, col, num) {
return (
!usedInRow(matrix, row, num) &&
!usedInCol(matrix, col, num) &&
!usedInBox(matrix, row - (row % 3), col - (col % 3), num)
);
}
function usedInRow(matrix, row, num) {
for (let col = 0; col < matrix.length; col++) {
if (matrix[row][col].N === num) {
return true;
}
}
return false;
}
function usedInCol(matrix, col, num) {
for (let row = 0; row < matrix.length; row++) {
if (matrix[row][col].N === num) {
return true;
}
}
return false;
}
function usedInBox(matrix, boxStartRow, boxStartCol, num) {
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
if (matrix[row + boxStartRow][col + boxStartCol].N === num) {
return true;
}
}
}
return false;
}
let row = 0;
let col = 0;
let checkBlankSpaces = false;
// 验证数独是否已解决,如果尚未解决,则获取下一个空格的位置
for (row = 0; row < matrix.length; row++) {
for (col = 0; col < matrix[row].length; col++) {
if (matrix[row][col].N == null) {
checkBlankSpaces = true;
break;
}
}
if (checkBlankSpaces === true) {
break;
}
}
//当没有空格时则意味着已经解决
if (checkBlankSpaces === false) {
return true;
}
// 尝试用正确的数字填充空格
for (let num = 1; num <= 9; num++) {
// isSafe 用于检查在行、列或 3x3 的格子中是否已经存在了数字 num(代码实现在后面)
if (isSafe(matrix, row, col, num)) {
matrix[row][col].N = num;
if (this.solveSudoku(matrix)) {
return true;
}
// 如果 num 所在的位置不合适,需要再次标记为“空格”,然后用不同的 num 回溯
matrix[row][col].N = null;
}
}
return false;
}
究竟什么是并发?什么是多线程?什么是异步?
首先,并发 多线程 异步分别指:
并发:同时处理多件事情
多线程:并发的一种形式,它采用多个线程来执行程序。对多个线程的管理使用了线程池。
异步:并发的一种形式,
(1)它采用回调机制,避免产生不必要的线程。
(2)多线程也可以成为实现程序异步的一种方式,在这里,异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段(这违反了异步操作的本质)。
三者之间的结构关系为:
对于单线程语言JavaScript,它可以实现异步,但并不能实现多线程。
在JS中,异步也是单线程的,相当于另开一个任务队列(在事件循环机制那一项中已经细说),在操作上虽然可以讲是异步并发,但因为实际上对象在队列中还是一个个生成的(所以其实虽说这次的代码理论上实现了并发,但也不是有意义的9个数组同时生成/求解,而是在本就没什么同步操作的代码里画蛇添足一下点个题),故只是在错误选择JS进行开发后的无奈之举。
为什么最终再次放弃Web Workers?
因为在第一次作业中愚蠢地采用基于原型的对象生成模式又把一堆东西杂在原型方法里,一切代码跟着Canvas对象狠狠地和DOM绑定在一起难舍难分。试图分离的无效努力过程存在仓库2023_FZU_Sudoku_test ,以CanvasId无法传递告终。
如果想要使用Web Workers,一定要一开始就直接把计算代码写在worker.js里!而且worker.js的功能非常有限,只能完成数值计算,所有花里胡哨的功能想都不要想,像本人这种把Draw( )也绑在对象上的基本就是别想用这个方法了,所以一开始就要想好自己要做什么。
难搞的Draw( )
在两次作业编程中最经常遇到的问题就是“怎么又Draw不出来”,第一次是因为ctx对象没定义清楚,这一次为了把之前生成代码改成异步,btnInitSudo_Click()中调用Draw( )出了点问题,一直找不出原因,最后一气之下选择了最为粗暴的“点击生成重新加载页面”,但不得不说有的时候最简单的方法就是最好用的方法,从用户视角去测试和之前根本没区别,白花那么多时间在那里调整函数内容。总结经验是这种静态的简单的页面还是自己踏实写吧,虽然说被迫学了下Canvas是什么怎么用是件好事但是这也是后面限制不能实现并发的重大问题之一。
太快进入开发,选择错误的语言和编写方式,导致后续开发陷入绝境(第一次结对作业期间遗留问题)
在第一次作业规划的时候,因为缺乏对多线程、并发和JavaScript语言的基本概念,错误地选择了开发语言,导致后期耗费大量的时间在解决“JS是单线程语言”和并发性计算的冲突上,实际上直到这次任务结束,这个问题也没有得到真正的解决(异步处理本质上都是把其丢到一个队列里,并不能真正实现大量资源换运行时间的严格意义上的多线程并发,Web Workers并不能操作DOM,又无法分离计算部分传递过去),而真正面对多线程技术、并发技术需要利用到的线程锁、线程池等概念,因为在JS中无法实现而仅仅停留在了“了解到”这个阶段,并未能在作业中实现,这一点非常遗憾。在未来有时间希望可以使用Java重构这项作业。
意外收获:JS是一门什么样的语言?
在做作业的过程中逐渐明白因为一开始的选择这次的技能点绝对是点歪了(预期应该是想要我们学会多线程编程,大概是采用Java或者Python,学些类似线程池、线程安全保证之类的操作和简单的前端设计),这个收获实在是很意外。在试图探索JS是否有和多线程交叉的可能的时候学到了JS的事件循环机制以及Web Workers这个api,会了一些简单的CSS、JS、ES6、HTML编程,可以尝试自己写静态网页了,勉强算是掌握了一个技能。
JS为什么会是一门单线程语言,这个问题在此过程中也得到了答案,作为能够操作DOM的、同时有要应付难以预计的用户操作语言,多线程会带来严重的同步问题。这也决定了即使诞生了Web Workers,它的操作空间也是极其受限的。
紧抓不放原先的结果和重构怎么选?
其实在作业过程中两个人都有产生过用其他语言重构之前的代码的想法,但是最后并没有选择进行重构(那个时候看到别人使用异步能够实现并发又燃起了希望,毕竟都很珍惜自己的劳动成果,至于后来了解到异步和并发的区别又是后话了。)
事实上,站在只有几个小时就要交差的现在来看,应该在第二次任务的初期及时地进行重构,反而可能会节省花在无用探索上的时间, 也更有可能完成真实意义上的“多线程”与“并发生成/求解”。
PSP | 预估时间(小时) | 实际时间(小时) |
---|---|---|
计划 | 0.5 | 1 |
学习 | 8 | 20 |
编写代码 | 8 | 9.5 |
测试 | 0.5 | 1 |
修改代码 | 6 | 9 |
编写博客 | 1 | 1.5 |
合计 | 24 | 42 |
这两次作业警醒本人,至少要积累常见的几种开发语言有基本的概念和了解,哪些是用于前端开发、哪些是用于后端开发、有哪些常用架构。应该要以开发为导向进行一些成套的学习(比如适用于静态页面开发的JS+HTML+CSS,或者适用于跨平台软件开发的C++与Qt等)。遇到自己不熟悉的概念不应该指望一边开发一边了解,而是应该先建立基本的概念再进行具体架构的选择,理解清楚客户的需求究竟是什么(在这次任务中,我对并发和多线程的理解两次都出现了较大的偏差,对工具的选择也太为随意,错误地理解了“规划阶段不要太纠结于细节,比如开发语言”的意思),尤其对于对接成本较高的多人任务和有后继开发可能的任务,前期准备工作更应该慎之又慎,否则很可能面对不得不全部推翻重构的局面。不过确实需要重构的时候也应该当机立断。
在这里非常感谢我的搭档,因为国庆期间一直忙于搬家所以前中期开发工作主要都是由我的搭档在完成,在我陷入困境的时候也给我提供了很多可行的思路,让我切实体会到合作工作和自己独自开发确实是很不一样的。
另:上面也提到了,因为选择错误地关系逃课了多线程编程,接下来要规划对Java多线程编程的学习,比如创建Thread类、实现Runnable接口、线程控制(启动终止同步)。查询过程中可以看得出来本来这些和最近在学的操作系统联系紧密,本来是很好的规划(悲)。