{"id":21396969,"url":"https://github.com/tcl606/bankservices","last_synced_at":"2025-03-16T14:43:23.404Z","repository":{"id":165104652,"uuid":"637062863","full_name":"TCL606/BankServices","owner":"TCL606","description":"银行柜员服务问题","archived":false,"fork":false,"pushed_at":"2023-05-06T11:49:23.000Z","size":110,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-23T02:12:31.106Z","etag":null,"topics":["csharp","multi-threading","operating-system","os","tsinghua-university"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TCL606.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-06T11:47:40.000Z","updated_at":"2024-12-06T13:27:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"4d6faab9-e6ad-4eea-825b-3a7aa09f4f1b","html_url":"https://github.com/TCL606/BankServices","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TCL606%2FBankServices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TCL606%2FBankServices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TCL606%2FBankServices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TCL606%2FBankServices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TCL606","download_url":"https://codeload.github.com/TCL606/BankServices/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243885888,"owners_count":20363644,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","multi-threading","operating-system","os","tsinghua-university"],"created_at":"2024-11-22T14:31:04.970Z","updated_at":"2025-03-16T14:43:23.367Z","avatar_url":"https://github.com/TCL606.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 进程间同步互斥问题：银行柜员服务问题\n\n## 问题描述\n\n银行有 n 个柜员负责为顾客服务，顾客进入银行先取一个号码，然后等着叫号。当某个柜员空闲下来，就叫下一个号。\n\n编程实现该问题，用 P、V 操作实现柜员和顾客的同步。\n\n## 实现要求\n\n1. 某个号码只能由一名顾客取得；\n2. 不能有多于一个柜员叫同一个号；\n3. 有顾客的时候，柜员才叫号；\n4. 无柜员空闲的时候，顾客需要等待；\n5. 无顾客的时候，柜员需要等待。\n\n## 实验平台\n\nWindows OS；编程语言 C# 10（.NET 6）\n\n## 实验原理\n\n使用信号量的 P、V 操作，控制线程间的同步与互斥关系。对于共享资源的访问与修改，可以使用锁进行控制。\n\nC# 中提供了 `System.Threading` 命名空间，进行线程相关的操作。本次实验中主要用到其中的两种类：\n\n1. `System.Threading.Thread` 类，为 C# 中的线程类。\n2. `System.Threading.Semaphore` 类，为 C# 中的信号量类。\n\n本次实验中还将用到 C# 中锁的语法：`lock (Object obj)`，用于上锁与解锁，这里的 `obj` 对象可以认为是锁。锁理论上可以用 P、V操作实现，是一种方便的接口。锁实现的伪代码如下：\n\n~~~\nsema = new SemaPhore(obj); // 创建 obj 对象对应的信号量\nP(sema)\n// 执行操作\nV(sema)\n~~~\n\n本实验中为了方便，直接使用 C# 的 lock 语法实现锁。\n\n## 实现思路\n\n本任务中主要涉及两种对象：顾客与银行。\n\n顾客类，需包含顾客的 ID、到达时间、服务时间等属性，是要被服务的对象。主要的协调算法在银行类中实现。\n\n### 1.顾客加入时\n\n根据问题描述，顾客依次进入银行，等待被服务，因此需要在银行中维护一个队列 `customersQueue`，每当有顾客加入，就往这个队列里添加一名顾客。注意，由于可能有顾客同时加入，因此对 `customersQueue` 的操作需要控制互斥，因此需要上锁。\n\n当有顾客加入时，根据问题需求，此时银行需要进行协调，这里我在顾客加入后，尝试进行叫号的操作，具体分析见第 2 点。\n\n顾客加入时的代码如下\n\n~~~C#\npublic class Bank\n{ \n    \n    // ......\n    \n    public void JoinCustomer(Customer customer)\n    {\n        lock (this.customersQueueLock)\n        {\n            customersQueue.Enqueue(customer);\n        }\n        Console.WriteLine($\"{customer} arrives at time {(Environment.TickCount - this.initTime) / this.timeInterval}\");\n        new Thread(() =\u003e TryServeCustomer()).Start();\n    }\n}\n~~~\n\n### 2.尝试叫号\n\n顾客加入时，尝试叫号，执行 `TryServeCustomer` 的操作。这里需要用信号量控制线程间的同步关系。\n\n设置一个信号量 `attendantSema` ，用于表示当前柜员的个数，初始化为最大柜员数量；同时维护一张柜员表，记录每个柜员是否正在工作。\n\n每当执行 `TryServeCustomer` 时，需要尝试将柜员个数减 1，这对应着信号量 `attendantSema` 的 P 操作。如果有空闲柜员，则说明原始信号量 `attendantSema` 值大于 0，进行 P 操作后不会阻塞，可以分配一个柜员给对应的顾客。此时将顾客从顾客队列 `customerQueue` 中取出，分配给一个空闲的柜员，更新记录柜员表；若当前已经没有空闲柜员了，则信号量执行 P 操作后会阻塞，直到有柜员服务完毕再度变为空闲时，才会从阻塞中恢复。由于柜员表的读写可能同时进行，因此需要锁变量控制柜员表的访问。\n\n成功分配柜员给顾客后，则将进行模拟服务操作，见第 3 点。\n\n尝试叫号的代码如下\n\n~~~C#\npublic class Bank\n{ \n    \n    // ......\n    \n    public void TryServeCustomer()\n    {\n        this.attendantSema.WaitOne();\n        Customer customer;\n        lock (this.customersQueueLock)\n        {\n            customer = this.customersQueue.Dequeue();\n        }\n        lock (this.serversLock) // 分配空闲柜员\n        {\n            var server = this.availServers.First(kvp =\u003e !kvp.Value);\n            this.availServers[server.Key] = true;\n            customer.ServerID = server.Key;\n        }\n        customer.StartTime = (Environment.TickCount - this.initTime) / this.timeInterval;\n        new Thread(() =\u003e\n        {\n            // 模拟服务操作\n        }).Start();\n    }\n}\n~~~\n\n### 3.模拟服务\n\n当顾客成功匹配空闲的柜员时，则开始模拟服务操作。这里直接通过线程睡眠的方式模拟服务。\n\n在服务结束后，要将接待顾客的柜员重新置为空闲，即修改柜员记录表；然后要释放信号量 `attendantSema`，代表此服务已结束，有柜员重归空闲。\n\n具体代码如下\n\n~~~C# \n// 模拟服务操作\nConsole.WriteLine($\"{customer} starts at time {customer.StartTime}. Being served at ServerID {customer.ServerID} for {customer.ServiceTime} time\");\nThread.Sleep(customer.ServiceTime * this.timeInterval);\ncustomer.LeaveTime = customer.StartTime + customer.ServiceTime;\nConsole.WriteLine($\"End serving {customer} at time {customer.LeaveTime}\");\nlock (this.serversLock)\n{\n    this.availServers[customer.ServerID] = false;\n}\nthis.attendantSema.Release();\n~~~\n\n## 具体细节\n\n### 1.模拟顾客到来\n\n模拟顾客依次到来，只需保证顾客按照给定的时间加入银行队列即可。进入银行可通过银行类的 `JoinCustomer` 函数实现。\n\n具体模拟方式如下\n\n~~~C# \nvar sleepTime = customerList[0].ArriveTime;\nThread.Sleep(sleepTime * timeInterval);\nbank.JoinCustomer(customerList[0]);\nfor (int i = 1; i \u003c customerList.Count; i++)\n{\n    sleepTime = customerList[i].ArriveTime - customerList[i - 1].ArriveTime;\n    Thread.Sleep(sleepTime * timeInterval);\n    bank.JoinCustomer(customerList[i]);\n}\n~~~\n\n这里通过睡眠，控制每个顾客到来的时间。\n\n### 2.判断所有顾客都已完成服务\n\n直接判断所有顾客是否都完成服务比较麻烦，这里直接估计出所有顾客完成服务的时间上限，然后阻塞主线程，确保最终所有顾客都已完成服务后再显示最终结果。\n\n所有顾客完成服务的时间上限为：在所有顾客都加入银行后，至多还需要等待 \"所有顾客的服务时长 + 第一个顾客到达的时间 - 最后一个顾客到达的时间\"。这个上限可以通过假设银行只有一个柜员来得到。\n\n具体代码如下：\n\n~~~C#\nvar maxPossibleSleepTime = customerList.Select(x =\u003e x.ServiceTime).Sum() + customerList[0].ArriveTime - customerList[customerList.Count - 1].ArriveTime;\nThread.Sleep(maxPossibleSleepTime * timeInterval + timeInterval);\n\nConsole.WriteLine();\nConsole.WriteLine(\"================================================================================\");\nforeach (var customer in customerList)\n{\n    Console.WriteLine($\"{customer} , Arrive Time: {customer.ArriveTime}, Start Time: {customer.StartTime}, Leave Time: {customer.LeaveTime}, Server ID: {customer.ServerID}\");\n}\n~~~\n\n### 3.时间控制\n\n在模拟银行柜员服务问题中，需要记录各顾客的到达、开始服务、结束服务的时间。这里通过线程睡眠阻塞的方式模拟顾客服务，并通过使用 C# 中的 `System.Environmnet.TickCount` 获得当前时间，并与初始时记录的时间作差，得到目前程序执行的时间。\n\n在程序中，我设置单位时间长度 `timeInterval=200ms` 进行模拟。单位时间长度不宜太小，否则模拟将不精确。\n\n## 实验模拟\n\n构造测例如下\n\n### 测例1\n\n![test1exp](img/test1exp.png)\n\n指定柜员数目为 2，运行结果如下：\n\n![test1res](img/test1res.png)\n\n理论分析知：\n\n1. 顾客 1 在 3 时刻到达，服务 10，应当在 13 结束，分配柜员 0；\n2. 顾客 2 在 5 时刻到达，此时仍有空闲柜员，分配柜员 1 后继续执行；\n3. 顾客 3 在 6 时刻到达，此时没有空闲柜员，需等到时刻 7 顾客 2 执行完毕后，到顾客 3 执行，柜员号与顾客 2 相同，为 1。\n\n程序执行结果与理论分析一致，结果正确。\n\n### 测例2\n\n![test2exp](img/test2exp.png)\n\n指定柜员数目为 2，运行结果如下：\n\n![test2res](img/test2res.png)\n\n理论分析知：\n\n1. 顾客 0 于 1 时刻到达，执行 7 时间，分配柜员 0 。\n2. 顾客 1 于 2 时刻到达，执行 1 时间，分配柜员 1 。\n3. 顾客 2 于 5 时刻到达，此时柜员 0 未结束，柜员 1 已结束，因此会给顾客 2 分配柜员 1，继续执行。\n4. 顾客 3 于 7 时刻到达，此时柜员 0,1 都不空闲，需等到 8 时刻，柜员 0 服务完顾客 0 后，才能开始被服务。\n5. 柜员 4 于 9 时刻到达，此时柜员 0,1 都不空闲，需等到 10 时刻，柜员 1 服务完顾客 2 后，才能开始被服务。\n\n程序最终结果与理论分析一致，结果正确。\n\n## 思考题\n\n### 1.柜员人数和顾客人数对结果分别有什么影响？\n\n理论上说，固定顾客人数，随着柜员人数增加时，服务完所有顾客的总时间会先迅速下降，再平缓下降。当增加到柜员人数大于顾客人数时，总时间不会再变化。\n\n原因是，当柜员人数较小时，顾客人数相对较多，柜员非常忙碌，大部分时间里都处在工作状态，每增加一个柜员，就能迅速缓解顾客的需求。当柜员人数逐渐增加后，部分柜员便有部分时间会处于空闲状态，而不是总处于工作状态。此时再增加柜员人数，对总时间的影响会逐渐变小。当柜员人数增加到大于顾客人数时，此时柜员总是充足的，总时间由顾客人数决定。\n\n固定柜员人数，随着顾客人数的增加，顾客较少时，总时间增加的比较缓慢，随着顾客逐渐增加（到远大于柜员人数），此时总时间与顾客人数的关系基本呈线性。\n\n原因是，当顾客较少时，柜员是相对充足的，顾客人数对总时间影响较小；而当顾客逐渐增多，远大于柜员人数时，柜员总是处于忙碌状态，而后到的顾客长时间处于等待被服务中，顾客人数越多等待的人就越多，总时间会线性增长。\n\n### 2.实现互斥的方法有哪些？各自有什么特点？效率如何？\n\n1. 忙等待方法。通过循环不断判断某些变量的值，直到该变量的值发生改变，才跳出循环执行之后的语句。这种算法实现简单，但会占用 CPU 资源。且如果不能设计好，还可能会引起多进程（线程）同时访问临界区的错误。使用效率上，由于该方法会持续占用 CPU 资源，所以效率较低，不宜在等待时间较长的场景下使用。\n2. 中断禁用法。进程（线程）的打断要通过中断。在进入临界区前禁用中断即可实现互斥。这种方法在平常用户编程中不常用，因为需要操作系统支持，将控制中断的接口暴露。而且这种方法有很大的风险，例如在临界区中的操作倘若发生异常，由于中断被关闭，程序很有可能崩溃或直接卡死，造成严重的后果。\n3. 信号量方法。信号量方法通过原子操作实现互斥。该方法会阻塞进程（线程），不会占用 CPU 资源，效率高，是一种常用的控制同步或互斥的方法。但是大量使用信号量（或通过其实现的锁）也可能会带来不小的开销。\n\n## 实验体会\n\n本次实验中通过模拟的方法解决银行柜员服务问题。其中，对于单位时间长度的我有一点体会。由于程序在执行中还是会消耗一定时间的，所以单位时间长度不宜太小，否则容易出错。在我的反复尝试下，我认为单位时间长度在 `200ms` 及以上是比较合适的。本次实验中设置为 `200ms`。\n\n## 文件说明\n\n`BankServices/`：源代码与项目文件\n\n`publish/`：可执行文件与依赖项\n\n`TestFiles/`：测试文件\n\n`run.cmd`：运行 `publish/BankServices.exe` 程序\n\n- `publish/BankServices.exe` 参数指定：\n  - `--fileName`：测试文件路径\n  - `--serverNum`：柜员个数\n  - `--timeInterval`：单位时间长度（ms）","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftcl606%2Fbankservices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftcl606%2Fbankservices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftcl606%2Fbankservices/lists"}