Go语言并发之扇入和扇出

article/2025/8/28 23:22:08

1、Go语言并发之扇入和扇出

编程中经常遇到扇入和扇出两个概念,所谓的扇入是指将多路通道聚合到一条通道中处理,Go 语言最简单的扇入

就是使用 select 聚合多条通道服务;所谓的扇出是指将一条通道发散到多条通道中处理,在Go语言里面具体实

现就是使用go关键字启动多个 goroutine 并发处理。

中国有句经典的哲学名句叫分久必合,合久必分,软件的设计和开发也遵循同样的哲学思想,扇入就是合,扇出

就是分。当生产者的速度很慢时,需要使用扇入技术聚合多个生产者满足消费者,比如很耗时的加密/解密服务;

当消费者的速度很慢时,需要使用扇出技术,比如Web服务器并发请求处理。扇入和扇出是Go并发编程中常用的

技术。

在这里插入图片描述

1.1 单链工作模式

如果我们没有使用扇入扇出的流程,就是传统的单链工作模式。

package mainimport "fmt"func A(n int) <-chan string {out := make(chan string)go func() {defer close(out)for i := 1; i <= n; i++ {out <- fmt.Sprint("节点A-", i)}}()return out
}func B(in <-chan string) <-chan string {out := make(chan string)go func() {defer close(out)for c := range in {out <- "节点B" + c}}()return out
}func C(in <-chan string) <-chan string {out := make(chan string)go func() {defer close(out)for c := range in {out <- "节点C" + c}}()return out
}func main() {componentA := A(9)componentB := B(componentA)componentC := C(componentB)for goods := range componentC {fmt.Println(goods)}
}
# 程序输出
节点C节点B节点A-1
节点C节点B节点A-2
节点C节点B节点A-3
节点C节点B节点A-4
节点C节点B节点A-5
节点C节点B节点A-6
节点C节点B节点A-7
节点C节点B节点A-8
节点C节点B节点A-9

1.2 扇入扇出模式

如果使用扇入扇出模式的话,我们在增加一个汇聚的功能函数就可以了。

package mainimport ("fmt""sync"
)func A(n int) <-chan string {out := make(chan string)go func() {defer close(out)for i := 1; i <= n; i++ {out <- fmt.Sprint("节点A-", i)}}()return out
}func B(in <-chan string) <-chan string {out := make(chan string)go func() {defer close(out)for c := range in {out <- "节点B" + c}}()return out
}func C(in <-chan string) <-chan string {out := make(chan string)go func() {defer close(out)for c := range in {out <- "节点C" + c}}()return out
}// 扇入的主要操作
func merge(ins ...<-chan string) <-chan string {var wg sync.WaitGroupout := make(chan string)// 将一个channel中的数据发送到out当中dispose := func(in <-chan string) {defer wg.Done()for c := range in {out <- c}}// 添加等待组的数量wg.Add(len(ins))// 扇入阶段,启动多个goroutine处理在channel当中的数据for _, cs := range ins {go dispose(cs)}// 等待所有的输入的数据ins处理完,再关闭输出的outgo func() {wg.Wait()close(out)}()return out
}func main() {componentA := A(9)componentB1 := B(componentA)componentB2 := B(componentA)componentB3 := B(componentA)// 汇聚三个进行扇入操作mergeComponent := merge(componentB1, componentB2, componentB3)// 扇出操作componentC := C(mergeComponent)for goods := range componentC {fmt.Println(goods)}
}
# 程序输出
节点C节点B节点A-2
节点C节点B节点A-3
节点C节点B节点A-1
节点C节点B节点A-5
节点C节点B节点A-7
节点C节点B节点A-4
节点C节点B节点A-6
节点C节点B节点A-8
节点C节点B节点A-9

1.3 扇入扇出法寻找素数

扇入扇出法,体现并发性能的,相信每个学golang的都应该听过扇入扇出法寻找素数,我们先来看看不使用扇入

扇出法的。

package mainimport ("fmt""math/rand""time"
)// 重复调用函数的生成器
var repeatFn = func(done <-chan interface{}, fn func() interface{}) <-chan interface{} {valueStream := make(chan interface{})go func() {defer close(valueStream)for {select {case <-done:returncase valueStream <- fn():}}}()return valueStream
}// 转为int的channel
var toInt = func(done <-chan interface{}, valueStream <-chan interface{}) <-chan int {intStream := make(chan int)go func() {defer close(intStream)for v := range valueStream {select {case <-done:returncase intStream <- v.(int):}}}()return intStream
}// 返回素数
var primeChanArr = func(done <-chan interface{}, intStream <-chan int) <-chan interface{} {primeStream := make(chan interface{})go func() {defer close(primeStream)// intStream一直会循环生成for integer := range intStream {integer -= 1prime := truefor divisor := integer - 1; divisor > 1; divisor-- {if integer%divisor == 0 {prime = falsebreak}}if prime {select {case <-done:returncase primeStream <- integer:}}}}()return primeStream
}// 将另外一个channel的数据从全部倒腾出来
var take = func(done <-chan interface{}, valueStream <-chan interface{}, num int) <-chan interface{} {takeStream := make(chan interface{})go func() {defer close(takeStream)for i := 0; i < num; i++ {select {case <-done:returncase takeStream <- <-valueStream:}}}()return takeStream
}func main() {// 返回一个随机数randNum := func() interface{} {return rand.Intn(50000000)}// 完成chandone := make(chan interface{})defer close(done)start := time.Now()// 生成整形数字randIntStream := toInt(done, repeatFn(done, randNum))// channel数组fmt.Printf("Primes:\n")// 取出10个素数for prime := range take(done, primeChanArr(done, randIntStream), 10) {fmt.Printf("%d\n", prime)}fmt.Printf("消耗时间:%v", time.Since(start))
}
# 程序输出
Primes:
24941317
36122539
6410693
10128161
25511527
2107939
14004383
7190363
45931967
2393161
消耗时间:22.3454984s

然后我们来使用扇入删除法。

package mainimport ("fmt""math/rand""runtime""sync""time"
)// 重复调用函数的生成器
var repeatFn = func(done <-chan interface{}, fn func() interface{}) <-chan interface{} {valueStream := make(chan interface{})go func() {defer close(valueStream)for {select {case <-done:returncase valueStream <- fn():}}}()return valueStream
}// 转为int的channel
var toInt = func(done <-chan interface{}, valueStream <-chan interface{}) <-chan int {intStream := make(chan int)go func() {defer close(intStream)for v := range valueStream {select {case <-done:returncase intStream <- v.(int):}}}()return intStream
}var primeChanArr = func(done <-chan interface{}, intStream <-chan int) <-chan interface{} {primeStream := make(chan interface{})go func() {defer close(primeStream)for integer := range intStream {integer -= 1prime := truefor divisor := integer - 1; divisor > 1; divisor-- {if integer%divisor == 0 {prime = falsebreak}}if prime {select {case <-done:returncase primeStream <- integer:}}}}()return primeStream
}// 将另外一个channel的数据从全部倒腾出来
var take = func(done <-chan interface{}, valueStream <-chan interface{}, num int) <-chan interface{} {takeStream := make(chan interface{})go func() {defer close(takeStream)for i := 0; i < num; i++ {select {case <-done:returncase takeStream <- <-valueStream:}}}()return takeStream
}var fanIn = func(done <-chan interface{}, channels ...<-chan interface{}) <-chan interface{} {var wg sync.WaitGroupmultiplexedStream := make(chan interface{})multiplex := func(c <-chan interface{}) {defer wg.Done()for i := range c {select {case <-done:returncase multiplexedStream <- i:}}}wg.Add(len(channels))for _, c := range channels {go multiplex(c)}// 等待所有的读取的完成go func() {wg.Wait()close(multiplexedStream)}()return multiplexedStream
}func main() {// 生成随机数randNum := func() interface{} {return rand.Intn(50000000)}// 完成chandone := make(chan interface{})defer close(done)start := time.Now()randIntStream := toInt(done, repeatFn(done, randNum))cpuNumber := runtime.NumCPU()fmt.Printf("find cpu is : %d\n", cpuNumber)// channel数组chanArray := make([]<-chan interface{}, cpuNumber)fmt.Printf("Primes:\n")for i := 0; i < cpuNumber; i++ {chanArray[i] = primeChanArr(done, randIntStream)}for prime := range take(done, fanIn(done, chanArray...), 10) {fmt.Println(prime)}fmt.Printf("消耗时间:%v", time.Since(start))
}
# 程序输出
find cpu is : 8
Primes:
6410693
24941317
10128161
36122539
25511527
2107939
14004383
7190363
2393161
45931967
消耗时间:4.3099368s

1.4 扇入扇出法模拟任务处理

package mainimport ("context""log""sync""time"
)// Task包含任务编号及任务所需时长
type Task struct {Number intCost   time.Duration
}// task channel生成器
func taskChannelGerenator(ctx context.Context, taskList []Task) <-chan Task {taskCh := make(chan Task)go func() {defer close(taskCh)for _, task := range taskList {select {case <-ctx.Done():returncase taskCh <- task:}}}()return taskCh
}// doTask处理并返回已处理的任务编号作为通道的函数
func doTask(ctx context.Context, taskCh <-chan Task) <-chan int {doneTaskCh := make(chan int)go func() {defer close(doneTaskCh)for task := range taskCh {select {case <-ctx.Done():returndefault:log.Printf("do task number: %d\n", task.Number)// task 任务处理// 根据任务耗时休眠time.Sleep(task.Cost)// 已处理任务的编号放入通道doneTaskCh <- task.Number}}}()return doneTaskCh
}// fan-in意味着将多个数据流复用或合并成一个流
// merge函数接收参数传递的多个通道taskChs,并返回单个通道<-chan int
func merge(ctx context.Context, taskChs []<-chan int) <-chan int {var wg sync.WaitGroupmergedTaskCh := make(chan int)mergeTask := func(taskCh <-chan int) {defer wg.Done()for t := range taskCh {select {case <-ctx.Done():returncase mergedTaskCh <- t:}}}wg.Add(len(taskChs))for _, taskCh := range taskChs {go mergeTask(taskCh)}// 等待所有任务处理完毕go func() {wg.Wait()close(mergedTaskCh)}()return mergedTaskCh
}func main() {start := time.Now()// 使用context来防止goroutine泄漏,即使在处理过程中被中断ctx, cancel := context.WithCancel(context.Background())defer cancel()// taskList定义每个任务及其成本taskList := []Task{Task{1, 1 * time.Second},Task{2, 7 * time.Second},Task{3, 2 * time.Second},Task{4, 3 * time.Second},Task{5, 5 * time.Second},Task{6, 3 * time.Second},}// taskChannelGerenator是一个函数,它接收一个taskList并将其转换为Task类型的通道// 执行结果(int slice channel)存储在worker中// 由于doTask的结果是一个通道,被分给了多个worker,这就对应了fan-out处理taskCh := taskChannelGerenator(ctx, taskList)numWorkers := 4workers := make([]<-chan int, numWorkers)for i := 0; i < numWorkers; i++ {// doTask处理并返回已处理的任务编号作为通道的函数workers[i] = doTask(ctx, taskCh)}count := 0// merge从中读取已处理的任务编号for d := range merge(ctx, workers) {count++log.Printf("done task number: %d\n", d)}log.Printf("Finished. Done %d tasks. Total time: %fs", count, time.Since(start).Seconds())
}
# 程序输出
2023/06/10 17:51:04 do task number: 1
2023/06/10 17:51:04 do task number: 4
2023/06/10 17:51:04 do task number: 3
2023/06/10 17:51:04 do task number: 2
2023/06/10 17:51:05 do task number: 5
2023/06/10 17:51:05 done task number: 1
2023/06/10 17:51:06 do task number: 6
2023/06/10 17:51:06 done task number: 3
2023/06/10 17:51:07 done task number: 4
2023/06/10 17:51:09 done task number: 6
2023/06/10 17:51:10 done task number: 5
2023/06/10 17:51:11 done task number: 2
2023/06/10 17:51:11 Finished. Done 6 tasks. Total time: 7.009781s

1.5 扇入扇出法模拟流水线工作

假如我们有个流水线共分三个步骤,分别是 job1job2job3

package mainimport ("fmt""time"
)func job1(count int) <-chan int {outCh := make(chan int, 2)go func() {defer close(outCh)for i := 0; i < count; i++ {time.Sleep(time.Second)fmt.Println("job1 finish:", 1)outCh <- 1}}()return outCh
}func job2(inCh <-chan int) <-chan int {outCh := make(chan int, 2)go func() {defer close(outCh)for val := range inCh {// 耗时2秒time.Sleep(time.Second * 2)val++fmt.Println("job2 finish:", val)outCh <- val}}()return outCh
}func job3(inCh <-chan int) <-chan int {outCh := make(chan int, 2)go func() {defer close(outCh)for val := range inCh {val++fmt.Println("job3 finish:", val)outCh <- val}}()return outCh
}func main() {t := time.Now()firstResult := job1(10)secondResult := job2(firstResult)thirdResult := job3(secondResult)for v := range thirdResult {fmt.Println(v)}fmt.Println("all finish")fmt.Println("duration:", time.Since(t).String())
}
# 程序输出
job1 finish: 1
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job2 finish: 2
job3 finish: 3
3
job2 finish: 2
job3 finish: 3
3
job2 finish: 2
job3 finish: 3
3
all finish
duration: 21.0077945s

共计计算21秒,主要是因为 job2 中的耗时太久导致,现在我们的主要任务就是解决掉这个问题了。

这里只用了一个 job2 来处理 job1 的结果,如果我们能多开启几个 goroutine job2 并行处理会不会提升性能呢?

现在我们改进下代码,解决 job2 耗时的问题,需要注意一下,这里对ch的关闭也要作一下调整,由于启用了多个

job2 的 goroutine,所以在 job2 内部进行关闭了。

package mainimport ("fmt""sync""time"
)func job1(count int) <-chan int {outCh := make(chan int, 2)go func() {defer close(outCh)for i := 0; i < count; i++ {time.Sleep(time.Second)fmt.Println("job1 finish:", 1)outCh <- 1}}()return outCh
}func job2(inCh <-chan int) <-chan int {outCh := make(chan int, 2)go func() {defer close(outCh)for val := range inCh {// 耗时2秒time.Sleep(time.Second * 2)val++fmt.Println("job2 finish:", val)outCh <- val}}()return outCh
}func job3(inCh <-chan int) <-chan int {outCh := make(chan int, 2)go func() {defer close(outCh)for val := range inCh {val++fmt.Println("job3 finish:", val)outCh <- val}}()return outCh
}func merge(inCh ...<-chan int) <-chan int {outCh := make(chan int, 2)var wg sync.WaitGroupfor _, ch := range inCh {wg.Add(1)go func(wg *sync.WaitGroup, in <-chan int) {defer wg.Done()for val := range in {outCh <- val}}(&wg, ch)}// 重要注意,wg.Wait() 一定要在goroutine里运行,否则会引起deadlockgo func() {wg.Wait()close(outCh)}()return outCh
}func main() {t := time.Now()firstResult := job1(10)// 拆分成三个job2,即3个goroutine (扇出)secondResult1 := job2(firstResult)secondResult2 := job2(firstResult)secondResult3 := job2(firstResult)// 合并结果(扇入)secondResult := merge(secondResult1, secondResult2, secondResult3)thirdResult := job3(secondResult)for v := range thirdResult {fmt.Println(v)}fmt.Println("all finish")fmt.Println("duration:", time.Since(t).String())
}
# 程序输出
job1 finish: 1
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job2 finish: 2
job3 finish: 3
job1 finish: 1
3
job2 finish: 2
job1 finish: 1
job3 finish: 3
3
job2 finish: 2
job3 finish: 3
3
job1 finish: 1
job2 finish: 2
job3 finish: 3
3
job2 finish: 2
job3 finish: 3
3
all finish
duration: 12.0213193s

可以看到,性能提升了90%,由原来的22s减少到12s。上面代码中为了演示效果,使用的缓冲 chan 很小,如果调

大的话,性能更明显。

FAN-OUT模式:多个 goroutine 从同一个通道读取数据,直到该通道关闭。OUT 是一种张开的模式,所以又被称

为扇出,可以用来分发任务。

FAN-IN模式:1个 goroutine 从多个通道读取数据,直到这些通道关闭。IN 是一种收敛的模式,所以又被称为扇

入,用来收集处理的结果。

是不是很像扇子的状态,先展开(扇出)再全并(扇入)。

总结:在类似流水线这类的逻辑中,我们可以使用 FAN-IN 和 FAN-OUT 模式来提升程序性能。


http://chatgpt.dhexx.cn/article/uMJ7COjs.shtml

相关文章

DWcc2018免费下载及详细安装教程

DWcc2018下载及安装教程&#xff08;内附安装包下载链接&#xff09; 安装包下载&#xff1a; 百度网盘下载链接链接&#xff1a;https://pan.baidu.com/s/1tCkVVobfiUWSIrZuc7OOIg 提取码&#xff1a;0bb5 也可保存下方图片&#xff0c;微信扫码即可下载 安装步骤&#xff1a…

dreamweaver (dw)cc 2017

dreamweaver cc 2017是一款在目前工作中最优越的网页设计软件&#xff0c;被简称称为dw cc 2017。新版本比以往任何时候都变得更加专注、高效和快速&#xff0c;拥有和全新代码编辑器和更直观的用户界面和多种增强功能。比如对css预处理器等新工作流程的支持&#xff0c;可以提…

DW小知识

1.学习网页开发&#xff0c;首先你得知道怎么编写和构造HTML标记&#xff0c;用HTML标记传达你想要给用户展示的内容&#xff0c;比如文字、图片、音频和视频等。用HTML标记内容的目的是为了赋予网页语义&#xff0c;换句话讲就是要给你的网页赋予某些用户代理能够理解的含义。…

dwcc怎么设置html默认,Dreamweaver CC 2019如何设置界面首选项?

设置界面首选项 Dreamweaver为用户提供了对基本程序界面的广泛控制。您可以按照自己的喜好设置、安排和 定制各种面板。开始本书课程之前&#xff0c;您应该首先访问的位置之一是Dreamweaver Preferences (首选项)对话框。 利其他Adobe应用一样&#xff0c;首选项对话框提供描述…

html锚点链接dw怎么做,最新的DW中的锚点在哪

CSS布局HTML小编今天和大家分享dreamweaver cc2018的锚点跟跳转菜单在哪 DW中怎样让页面按钮链接到另一个页面的锚点 Dreamweaver里怎么制作锚点链接&#xff0c;跳到当前网页顶端&#xff1f;Dreamweaver里怎么制作锚点链接&#xff0c;效果&#xff1a;跳到当前网页的顶端&am…

创建 DW 项目

开发工具与关键技术&#xff1a; DW 作者&#xff1a;陈海涛 撰写时间&#xff1a;2021/4/27 1.首先创建一个文件夹&#xff0c;再在里面创建三个文件夹&#xff08;注意&#xff1a;不能使用 中文来命名&#xff09; 2.打开 DW&#xff0c;然后点击 CTALN 或者点击左上…

网页设计软件dw cc 2019 mac激活方法

Dreamweaver CC 2019 for mac是知名的网站和网页设计软件,简称dw,是设计师和程序员必备的网页代码编辑器,新版本的dw cc 2019 mac破解版提供了JavaScript重构功能、全新的EcmaScript 6支持,而且dw cc2019破解版与Chromium嵌入式框架的最新版本进行集成,这样用户可以轻松构…

DW CC2019软件安装破解教程(附安装包下载)

DW CC2019 64bit下载地址&#xff1a; 链接&#xff1a; https://pan.baidu.com/s/15dYmXLPvqDt2p-IepYrUdQ 密码&#xff1a;5rwp 安装中有任何问题添加QQ群&#xff1a;606940296&#xff08;备注软件出现问题&#xff09; 软件介绍 Adobe Dreamweaver CC 2019是Adobe公司…

网页设计软件dw cc2019直装版

Dreamweaver CC 2019 for mac是Web设计人员和开发人员设计必备的软件,dw 2019 mac破解版支持HTML、CSS、JavaScript等,功能十分强大,可以轻松帮助用户设计精美的网站网页,这次Dreamweaver cc 2019 mac破解版主要针对安全性增强功能、JavaScript重构、ECMAScript 6支持、Git…

DWCC2018HTML网页字体添加、更改

一般情况下&#xff0c;DWCC2018里是没有像宋体、楷体、微软雅黑之类的字体&#xff0c;我们可以将系统自带的字体添加到DWCC中 1、添加软件内没有的字体 **①打开DWCC2018→工具** ![在这里插入图片描述](https://img-blog.csdn.net/20181007163628785?watermark/2/text/aHR…

DWCC2018HTML基本网页设计技巧方法详解

目录&#xff1a; 一、文本格式化标记 ----------------------1、各类标签及描述 二、HTNL链接 ----------------------1、HTML链接语法 ----------------------2、在当前页面跳到指定位置 ----------------------3、图片链接 三、插入视频、图片、列表项、邮件链接等 --------…

dwcc2019写php,mac网页设计软件:DreamweaverCC2019(dwcc2019直装版)

dw cc 2019 Mac新增功能 dw cc 2019 Mac推出了一些令 Web 设计人员和开发人员激动无比的新增功能。 1、JavaScript 重构 作为 Web 开发人员&#xff0c;您现在可以使用 JavaScript 重构&#xff0c;利用范围感知功能智能地重命名函数和变量。只需一次单击&#xff0c;您就可以将…

DWCC2018基本网页设计注意要点、使用技巧

目录&#xff1a; 一、注意要点 -------------1、“实时视图”“设计”的选择 -------------2、调出属性框 -------------3、在浏览器中实时浏览自己的网页 二、部分使用技巧 -------------1、文本格式化标记 -------------2、网页整体属性编辑 -------------3、插入视频、列表…

解决Windows10/11系统DWcc2021安装失败打不开问题 Adobe Dreamweaver CC2021详细安装教程

最初为美国MACROMEDIA公司开发 &#xff0c;2005年被Adobe公司收购。dw是集网页制作和管理网站于一身的所见即所得网页代码编辑器。利用对 HTML、CSS、JavaScript等内容的支持&#xff0c;设计师和程序员可以在几乎任何地方快速制作和进行网站建设快速&#xff0c;灵活的编码。…

DreamWeaver CC网页设计与制作

目录 第一章 初识DreamWeaver CC 1.1 DreamWeaver CC的工作界面 1.1.1 不同风格的界面 1.1.2 伸缩自如的功能面板 1.1.3 多文档的编辑界面 1.1.4 新颖的“插入”面板 1.1.5 更完整的CSS功能 1.2 创建网站框架 1.2.1 站点管理器 1.2.2创建文件夹 1.2.3 定义新站点 …

js删除数组中指定对象

js删除数组中指定对象 需求说明从数组中移除指定对象函数封装 removeArray从数组中获取指定对象索引函数封装 getArrayIndex 在Vue中调用函数使用 需求说明 点击删除按钮删除指定行数据&#xff0c;即删除数组中指定对象。 _arr表示一个Array数组&#xff0c;里面包括了很多的对…

div中移除某个元素 js_js移除某个元素

js 如何删除某个div下的div元素 HTML利用JS移除指定的标签 在JS数组中如何删除某个元素 Array.prototype.remove = function (dx) {if (isNaN(dx) || dx > this.length) { return false; }for (var i = 0, n = 0; i < this.length; i++) {if (this[i] 。= this[dx]) {thi…

vue js删除数组中指定索引的元素

在前端开发中&#xff0c;我们经常需要对数组进行操作&#xff0c;增删改是经常的事情&#xff0c;那我们js中该如何删除指定的下标元素呢&#xff1f;&#xff1f;&#xff1f;&#xff1f; 我们用splice来操作 1.定义和用法 splice() 方法用于添加或删除数组中的元素。 注…

springCloud 接口包错 Hystrix circuit short-circuited and is OPEN

接口报错如下图所示,发完版之后出现这种问题, Hystrix circuit short-circuited and is OPEN 就字面意思就理解是,熔断接口短路 原因&#xff1a;出现timeout的问题&#xff0c;基于我的理论&#xff0c;当然只是基于我的空想。 因为我请求的是一百个线程去访问&#xff0c…

基于SSM企业留言系统

开发工具(eclipse/idea)&#xff1a; eclipse4.5/4.8或者idea2018,jdk1.8 数据库&#xff1a;mysql 功能模块&#xff1a; 前台展示&#xff1a; 1.企业新闻资讯 2.企业介绍 3.企业产品展示 4.留言中心 5.用户注册 一、管理员&#xff1a; 1.新闻管理 2.企业介绍管理 3.企…