并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题;Go语言作为一个出道以来就自带 『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是值得开发者去探究的,而Go语言中的并发(并行)编程是经由goroutine实现的,goroutine是golang最重要的特性之一,具有使用成本低、消耗资源低、能效高等特点,官方宣称原生goroutine并发成千上万不成问题,于是它也成为Gopher们经常使用的特性。

一、goroutine简介

Golang被极度赞扬的是它的异步机制,也就是goroutine。goroutine使用方式非常的简单,只需使用go关键字即可启动一个协程, 并且它是处于异步方式运行,你不需要等它运行完成以后再执行以后的代码。

  1. go func()//通过go关键字启动一个协程来运行函数

除去语法上的简洁,goroutine是一个协程,也就是比线程更节省资源,一个线程中可以有多个协程,而且goroutine被分配到多个CPU上运行,是真正意义上的并发。

  1. go func()//通过go关键字启动一个协程来运行函数

二、goroutine内部原理

在介绍goroutine原理之前,先对一些关键概念进行介绍:

关键概念

并发

一个cpu上能同时执行多项任务,在很短时间内,cpu来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。

并行

当系统有多个CPU时,每个CPU同一时刻都运行任务,互不抢占自己所在的CPU资源,同时进行,称为并行。

简单理解

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

在计算机中就是:

所以我认为它们最关键的点就是:是否是『同时』。

  

进程

cpu在切换程序的时候,如果不保存上一个程序的状态(也就是我们常说的context--上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。

因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。

线程

cpu切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。

线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

NOTE:线程包括三大类,而且goroutine也并非真正地协程。(请查看:《线程那些事儿》)

有时候为了方便理解可以简单把goroutine类比成协程,但心里一定要有个清晰的认知 — goroutine并不等同于协程。

协程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。

G-P-M调度模型简介

groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。

Go的调度器内部的三个重要的结构:M,P,G
M:M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
P:P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine

NOTE:G-P-M模型详解,请查看该篇博文。

 

调度实现

从上图中看,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。
P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),
Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个
goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。

当一个OS线程M0陷入阻塞时(如下图),P转而在运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。

当MO返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来,
如果没有拿到的话,它就把goroutine放在一个global runqueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查global runqueue并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行。
 
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:
参考地址:http://morsmachine.dk/go-scheduler
 

三、使用goroutine

基本使用

设置goroutine运行的CPU数量,最新版本的go已经默认已经设置了。

  1. num := runtime.NumCPU() //获取主机的逻辑CPU个数
  2. runtime.GOMAXPROCS(num) //设置可同时执行的最大CPU数

使用示例

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7.  
  8. func cal(a int , b int ) {
  9. c := a+b
  10. fmt.Printf("%d + %d = %d\n",a,b,c)
  11. }
  12.  
  13. func main() {
  14.   
  15. for i :=0 ; i<10 ;i++{
  16. go cal(i,i+1) //启动10个goroutine 来计算
  17. }
  18. time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成
  19. }
  20. //结果
  21. //8 + 9 = 17
  22. //9 + 10 = 19
  23. //4 + 5 = 9
  24. //5 + 6 = 11
  25. //0 + 1 = 1
  26. //1 + 2 = 3
  27. //2 + 3 = 5
  28. //3 + 4 = 7
  29. //7 + 8 = 15
  30. //6 + 7 = 13

goroutine异常捕捉

当启动多个goroutine时,如果其中一个goroutine异常了,并且我们并没有对进行异常处理,那么整个程序都会终止,所以我们在编写程序时候最好每个goroutine所运行的函数都做异常处理,异常处理采用recover

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7.  
  8. func addele(a []int ,i int) {
  9. defer func() { //匿名函数捕获错误
  10. err := recover()
  11. if err != nil {
  12. fmt.Println("add ele fail")
  13. }
  14. }()
  15. a[i]=i
  16. fmt.Println(a)
  17. }
  18.  
  19. func main() {
  20. Arry := make([]int,4)
  21. for i :=0 ; i<10 ;i++{
  22. go addele(Arry,i)
  23. }
  24. time.Sleep(time.Second * 2)
  25. }
  26. //结果
  27. add ele fail
  28. [0 0 0 0]
  29. [0 1 0 0]
  30. [0 1 2 0]
  31. [0 1 2 3]
  32. add ele fail
  33. add ele fail
  34. add ele fail
  35. add ele fail
  36. add ele fail

同步的goroutine

由于goroutine是异步执行的,那很有可能出现主程序退出时还有goroutine没有执行完,此时goroutine也会跟着退出。此时如果想等到所有goroutine任务执行完毕才退出,go提供了sync包和channel来解决同步问题,当然如果你能预测每个goroutine执行的时间,你还可以通过time.Sleep方式等待所有的groutine执行完成以后在退出程序(如上面的列子)。

示例一:使用sync包同步goroutine
sync大致实现方式
WaitGroup 等待一组goroutinue执行完毕. 主程序调用 Add 添加等待的goroutinue数量. 每个goroutinue在执行结束时调用 Done ,此时等待队列数量减1.,主程序通过Wait阻塞,直到等待队列为0.

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. func cal(a int , b int ,n *sync.WaitGroup) {
  9. c := a+b
  10. fmt.Printf("%d + %d = %d\n",a,b,c)
  11. defer n.Done() //goroutinue完成后, WaitGroup的计数-1
  12.  
  13. }
  14.  
  15. func main() {
  16. var go_sync sync.WaitGroup //声明一个WaitGroup变量
  17. for i :=0 ; i<10 ;i++{
  18. go_sync.Add(1) // WaitGroup的计数加1
  19. go cal(i,i+1,&go_sync)
  20. }
  21. go_sync.Wait() //等待所有goroutine执行完毕
  22. }
  23. //结果
  24. 9 + 10 = 19
  25. 2 + 3 = 5
  26. 3 + 4 = 7
  27. 4 + 5 = 9
  28. 5 + 6 = 11
  29. 1 + 2 = 3
  30. 6 + 7 = 13
  31. 7 + 8 = 15
  32. 0 + 1 = 1
  33. 8 + 9 = 17

示例二:通过channel实现goroutine之间的同步。

实现方式:通过channel能在多个groutine之间通讯,当一个goroutine完成时候向channel发送退出信号,等所有goroutine退出时候,利用for循环channe去channel中的信号,若取不到数据会阻塞原理,等待所有goroutine执行完毕,使用该方法有个前提是你已经知道了你启动了多少个goroutine。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7.  
  8. func cal(a int , b int ,Exitchan chan bool) {
  9. c := a+b
  10. fmt.Printf("%d + %d = %d\n",a,b,c)
  11. time.Sleep(time.Second*2)
  12. Exitchan <- true
  13. }
  14.  
  15. func main() {
  16.  
  17. Exitchan := make(chan bool,10) //声明并分配管道内存
  18. for i :=0 ; i<10 ;i++{
  19. go cal(i,i+1,Exitchan)
  20. }
  21. for j :=0; j<10; j++{
  22. <- Exitchan //取信号数据,如果取不到则会阻塞
  23. }
  24. close(Exitchan) // 关闭管道
  25. }

goroutine之间的通讯

goroutine本质上是协程,可以理解为不受内核调度,而受go调度器管理的线程。goroutine之间可以通过channel进行通信或者说是数据共享,当然你也可以使用全局变量来进行数据共享。

示例:使用channel模拟消费者和生产者模式

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. func Productor(mychan chan int,data int,wait *sync.WaitGroup) {
  9. mychan <- data
  10. fmt.Println("product data:",data)
  11. wait.Done()
  12. }
  13. func Consumer(mychan chan int,wait *sync.WaitGroup) {
  14. a := <- mychan
  15. fmt.Println("consumer data:",a)
  16. wait.Done()
  17. }
  18. func main() {
  19.  
  20. datachan := make(chan int, 100) //通讯数据管道
  21. var wg sync.WaitGroup
  22.  
  23. for i := 0; i < 10; i++ {
  24. go Productor(datachan, i,&wg) //生产数据
  25. wg.Add(1)
  26. }
  27. for j := 0; j < 10; j++ {
  28. go Consumer(datachan,&wg) //消费数据
  29. wg.Add(1)
  30. }
  31. wg.Wait()
  32. }
  33. //结果
  34. consumer data 4
  35. product data 5
  36. product data 6
  37. product data 7
  38. product data 8
  39. product data 9
  40. consumer data 1
  41. consumer data 5
  42. consumer data 6
  43. consumer data 7
  44. consumer data 8
  45. consumer data 9
  46. product data 2
  47. consumer data 2
  48. product data 3
  49. consumer data 3
  50. product data 4
  51. consumer data 0
  52. product data 0
  53. product data 1

四、channel

不同goroutine之间是如何进行通讯的呢?

  • 方法一:全局变量和锁同步
  • 方法二:Channel

这里我们主要注重讲解下go中特有的channel,其类似于UNIX中的管道(piple)。

channel概念

channel俗称管道,用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全多个goroutine可同时修改一个channel,不需要加锁

channel操作

定义和声明:

  1. var 变量名 chan 类型 //channel是有类型的,一个整数的channel只能存放整数
  2.  
  3. var test chan int
  4.  
  5. var test chan map[string]string
  6.  
  7. var test chan *stu

channel可分为三种:

只读channel:只能读channel里面数据,不可写入

只写channel:只能写数据,不可读

一般channel:可读可写

  1. var readOnlyChan <-chan int // 只读chan
  2. var writeOnlyChan chan<- int // 只写chan
  3. var mychan chan int //读写channel
  1. mychannel = make(chan int,10)
  2.  
  3. //或者
  4. read_only := make (<-chan int,10)//定义只读的channel
  5. write_only := make (chan<- int,10)//定义只写的channel
  6. read_write := make (chan int,10)//可同时读写
  1. 定义完成以后需要make来分配内存空间,不然会deadlock!
  1. //定义一个结构体类型的channel
  2.  
  3. package main
  4.  
  5. type student struct{
  6. name string
  7. }
  8.  
  9. func main() {
  10. var stuChan chan student
  11. stuChan = make(chan student, 10)
  12.  
  13. stu := student{name:"syu01"}
  14.  
  15. stuChan <- stu
  16. }

struct类型channel

读写数据

  1. ch <- "wd" //写数据
  2. a := <- ch //读取数据
  3. a, ok := <-ch //推荐的读取数据方法

注意:

  • 管道如果未关闭,在读取超时会则会引发deadlock异常
  • 管道如果关闭进行写入数据会pannic
  • 当管道中没有数据时候再行读取或读取到默认值,如int类型默认值是0

遍历管道

  • 使用for range遍历管道,如果管道未关闭会引发deadlock错误。
  • 如果采用for死循环已经关闭的管道,当管道没有数据时候,读取的数据会是管道的默认值,并且循环不会退出。
  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7.  
  8. func main() {
  9. mychannel := make(chan int,10)
  10. for i := 0;i < 10;i++{
  11. mychannel <- i
  12. }
  13. close(mychannel) //关闭管道
  14. fmt.Println("data lenght: ",len(mychannel))
  15. for v := range mychannel { //遍历管道
  16. fmt.Println(v)
  17. }
  18. fmt.Printf("data lenght: %d",len(mychannel))
  19. }

带缓冲区channe和不带缓冲区channel

带缓冲区channel:定义声明时候制定了缓冲区大小(长度),可以保存多个数据。

不带缓冲区channel:只能存一个数据,并且只有当该数据被取出时候才能存下一个数据。

  1. ch := make(chan int) //不带缓冲区
  2. ch := make(chan int ,10) //带缓冲区

不带缓冲区示例:

  1. package main
  2.  
  3. import "fmt"
  4.  
  5. func test(c chan int) {
  6. for i := 0; i < 10; i++ {
  7. fmt.Println("send ", i)
  8. c <- i
  9. }
  10. }
  11. func main() {
  12. ch := make(chan int)
  13. go test(ch)
  14. for j := 0; j < 10; j++ {
  15. fmt.Println("get ", <-ch)
  16. }
  17. }
  18.  
  19. //结果:
  20. send 0
  21. send 1
  22. get 0
  23. get 1
  24. send 2
  25. send 3
  26. get 2
  27. get 3
  28. send 4
  29. send 5
  30. get 4
  31. get 5
  32. send 6
  33. send 7
  34. get 6
  35. get 7
  36. send 8
  37. send 9
  38. get 8
  39. get 9

channel实现作业池

我们创建三个channel,一个channel用于接受任务,一个channel用于保持结果,还有个channel用于决定程序退出的时候。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. )
  6.  
  7. func Task(taskch, resch chan int, exitch chan bool) {
  8. defer func() { //异常处理
  9. err := recover()
  10. if err != nil {
  11. fmt.Println("do task error:", err)
  12. return
  13. }
  14. }()
  15.  
  16. for t := range taskch { // 处理任务
  17. fmt.Println("do task :", t)
  18. resch <- t //
  19. }
  20. exitch <- true //处理完发送退出信号
  21. }
  22.  
  23. func main() {
  24. taskch := make(chan int, 20) //任务管道
  25. resch := make(chan int, 20) //结果管道
  26. exitch := make(chan bool, 5) //退出管道
  27. go func() {
  28. for i := 0; i < 10; i++ {
  29. taskch <- i
  30. }
  31. close(taskch)
  32. }()
  33.  
  34. for i := 0; i < 5; i++ { //启动5个goroutine做任务
  35. go Task(taskch, resch, exitch)
  36. }
  37.  
  38. go func() { //等5个goroutine结束
  39. for i := 0; i < 5; i++ {
  40. <-exitch
  41. }
  42. close(resch) //任务处理完成关闭结果管道,不然range报错
  43. close(exitch) //关闭退出管道
  44. }()
  45.  
  46. for res := range resch{ //打印结果
  47. fmt.Println("task res:",res)
  48. }
  49. }

只读channel和只写channel

一般定义只读和只写的管道意义不大,更多时候我们可以在参数传递时候指明管道可读还是可写,即使当前管道是可读写的。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7.  
  8. //只能向chan里写数据
  9. func send(c chan<- int) {
  10. for i := 0; i < 10; i++ {
  11. c <- i
  12. }
  13. }
  14. //只能取channel中的数据
  15. func get(c <-chan int) {
  16. for i := range c {
  17. fmt.Println(i)
  18. }
  19. }
  20. func main() {
  21. c := make(chan int)
  22. go send(c)
  23. go get(c)
  24. time.Sleep(time.Second*1)
  25. }
  26. //结果
  27. 0
  28. 1
  29. 2
  30. 3
  31. 4
  32. 5
  33. 6
  34. 7
  35. 8
  36. 9

select-case实现非阻塞channel

原理通过select+case加入一组管道,当满足(这里说的满足意思是有数据可读或者可写)select中的某个case时候,那么该case返回,若都不满足case,则走default分支。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. )
  6.  
  7. func send(c chan int) {
  8. for i :=1 ; i<10 ;i++ {
  9. c <-i
  10. fmt.Println("send data : ",i)
  11. }
  12. }
  13.  
  14. func main() {
  15. resch := make(chan int,20)
  16. strch := make(chan string,10)
  17. go send(resch)
  18. strch <- "wd"
  19. select {
  20. case a := <-resch:
  21. fmt.Println("get data : ", a)
  22. case b := <-strch:
  23. fmt.Println("get data : ", b)
  24. default:
  25. fmt.Println("no channel actvie")
  26.  
  27. }
  28.  
  29. }
  30.  
  31. //结果:get data : wd

channel中定时器的使用

在对channel进行读写的时,可以对读写进行频率控制,通过time.Ticke实现

示例:

  1. package main
  2.  
  3. import (
  4. "time"
  5. "fmt"
  6. )
  7.  
  8. func main(){
  9. requests:= make(chan int ,5)
  10. for i:=1;i<5;i++{
  11. requests<-i
  12. }
  13. close(requests)
  14. limiter := time.Tick(time.Second*1)
  15. for req:=range requests{
  16. <-limiter
  17. fmt.Println("requets",req,time.Now()) //执行到这里,需要隔1秒才继续往下执行,time.Tick(timer)上面已定义
  18. }
  19. }
  20. //结果:
  21. requets 1 2018-07-06 10:17:35.98056403 +0800 CST m=+1.004248763
  22. requets 2 2018-07-06 10:17:36.978123472 +0800 CST m=+2.001798205
  23. requets 3 2018-07-06 10:17:37.980869517 +0800 CST m=+3.004544250
  24. requets 4 2018-07-06 10:17:38.976868836 +0800 CST m=+4.000533569

[GO语言的并发之道] Goroutine调度原理&Channel详解的更多相关文章

  1. go语言之行--golang核武器goroutine调度原理、channel详解

    一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...

  2. 弄懂goroutine调度原理

    goroutine简介 golang语言作者Rob Pike说,"Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法.一个运行的程序由一个或更多个go ...

  3. C语言操作WINDOWS系统存储区数字证书相关函数详解及实例

     C语言操作WINDOWS系统存储区数字证书相关函数详解及实例 以下代码使用C++实现遍历存储区证书及使用UI选择一个证书 --使用CertOpenSystemStore打开证书存储区. --在循环中 ...

  4. Linux- Linux自带定时调度Crontab使用详解

    Linux自带定时调度Crontab使用详解 在Linux当中,有一个自带的任务调度功能crontab,它是针对每个用户,每个用户都可以调度自己的任务. 示例:每分钟执行一次,将时间写入到指定文件当中 ...

  5. Kubernetes K8S之调度器kube-scheduler详解

    Kubernetes K8S之调度器kube-scheduler概述与详解 kube-scheduler调度概述 在 Kubernetes 中,调度是指将 Pod 放置到合适的 Node 节点上,然后 ...

  6. c语言:自增自减运算符的操作详解

    博主在回忆c语言的基本知识时,突然发现自增自减运算符(--.++)这个知识点有些模糊不清,故博主为了给同为小白的同学们提供一些经验,特写下这篇文章. 首先,自增自减运算符共有两种操作方式. 比如,我先 ...

  7. hadoop之 Yarn 调度器Scheduler详解

    概述 集群资源是非常有限的,在多用户.多任务环境下,需要有一个协调者,来保证在有限资源或业务约束下有序调度任务,YARN资源调度器就是这个协调者. YARN调度器有多种实现,自带的调度器为Capaci ...

  8. Yarn 调度器Scheduler详解

    理想情况下,我们应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源.在Yarn中,负责给应用分配资 ...

  9. YARN调度器(Scheduler)详解

    理想情况下,我们应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源.在Yarn中,负责给应用分配资 ...

随机推荐

  1. Java NIO学习系列四:NIO和IO对比

    前面的一些文章中我总结了一些Java IO和NIO相关的主要知识点,也是管中窥豹,IO类库已经功能很强大了,但是Java 为什么又要引入NIO,这是我一直不是很清楚的?前面也只是简单提及了一下:因为性 ...

  2. python接口自动化(二十八)--html测试 报告——下(详解)

    简介 五一小长假已经结束了,想必大家都吃饱喝足玩好了,那就继续学习吧.一天不学习,自己知道:两天不学习,对手知道:三天不学习,大家知道:一周不学习,智商输给猪.好了开个玩笑都逗大家一乐,但是想想还是有 ...

  3. Skyline WEB端开发5——添加标签后移动

    针对于标签或者模型,在skyline上可以进行移动.可以让一个模型可以像无人机似的飞行,或者描述从一个点到另一个点的飞行轨迹. 话不多说,直接上干货. 第一步 添加标签 参考网址:https://ww ...

  4. [ERROR]:INST-07008: Oracle 主目录(O) 位置的验证失败。用户没有创建主目录/实例位置的权限

    安装weblogic12.1.3.0时,输入的安装命令是: 老是报这个错误. 百度半天好像没人报过这错……看来只有我这么粗心了…… 后来发现wls.rsp里面的Oracle_HOME指向目录错误,修改 ...

  5. Windows10 OpenSSH 快捷设置 避免 Bad owener or permission on

    配置ssh 有两个地方 ~/.ssh/config 这个亲测失败,怎么搞都报错 bad owner .... c:/programdata/ssh/ssh_config 亲测有效 (显示隐藏文件才看的 ...

  6. openstack-neutron基本的网络类型以及分析

    [概述] Neutron是OpenStack中负责提供网络服务的组件,基于软件定义网络的思想,实现了网络虚拟化下的资源管理,即:网络即服务. [功能] ·二层交换 Neutron支持多种虚拟交换机,一 ...

  7. 用canvas绘制时钟

    用canvas做时钟其实很简单,下面是我做出的效果: 是不是还挺漂亮的? 下面上代码: html <div class="whole"> <canvas id=& ...

  8. python面向过程编程 - ATM

    前面程序整合加自定义日志 1.文件摆放 ├── xxxx │ ├── src.py │ └── fil_mode.py │ └── data_time.py │ └── loading.py │ └─ ...

  9. sql LocalDB 的安装环境和使用方法

    LocalDB LocalDB专门为开发商.它是非常容易安装,无需管理,但它提供了相同的T-SQL语言,编程表面和客户端供应商定期的SQL Server Express.实际上,目标SQL Serve ...

  10. webpack基础知识

    一.基础 1 安装 npm i -g webpack webpack-cli // 推荐安装至本地 npm i -D webpack webpack-cli 2 webpck基础使用 2.1 webp ...