Go 并发操作
goroutine
在其他的编程语言中,线程调度是交由os
来进行处理的。
但是在Go
语言中,会对此做一层封装,Go
语言中的并发由goroutine
来实现,它类似于用户态的线程,更类似于其他语言中的协程。它是交由Go
语言中的runtime
运行时来进行调度处理,这使得Go
语言中的并发性能非常之高。
一个Go
进程,可以启动多个goroutine
。
一个普通的机器运行几十个线程负载已经很高了,然而Go
可以轻松创建百万goroutine
。
Go
标准库的net
包,写出的go web server性能直接媲美Nginx
。
比如在java/c++
里,开发者通常要去自己维护一个线程池,并且需要包装多个线程任务,同时还要由开发者手动调度线程执行任务并且维护上下文切换,这非常的耗费心智,故在Go
语言中出现了goroutine
,它的概念类似于线程与协程,Go
语言内置的就有调度与上下文切换机制,所以不用开发人员再去注意这些,并且goroutine
的使用也非常的简单,它相较于其他语言的多并发编程更加轻松。
goroutine与线程
动态栈
操作系统中的线程都有固定的栈内存(一般为2MB),这使得开启大量的线程会面临性能下降的问题。
但是goroutine
在生命周期之处的栈内存一般只有2KB,并且它会按需进行增大和缩小。最大的栈限制可达到1GB,所以在Go
语言中一次创建上万级别的goroutine
是没有任何问题的。
goroutine调度
GPM
是Go
语言运行时runtime
层面的实现,这是Go
语言自己实现的一套调度系统,区别于操作系统来调度os
线程。
- G很好理解,就是单个goroutine的信息,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
- P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
- M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime
会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS
设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine
则是由Go运行时(runtime
)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
上面这么多专业术语看起来比较头痛,这边用一幅图来明确的进行表示。
goroutine使用
在调用函数前加上go
关键字,就可以为函数创建一个goroutine
。
一个goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
每个Go
语言都有一个goroutine
,类似于主线程的概念。
goroutine
的启动是随机进行调度的,这个无法手动控制。
基本使用
下面是创建单个goroutine
与主goroutine
进行并发执行任务。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println(i)
}
fmt.Println("子goroutine执行完毕")
}() // 立即执行函数,一个goroutine任务
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
下面是创建多个goroutine
与主goroutine
进行并发执行任务。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine1执行完毕")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
}
func main() {
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
sync.WaitGroup
该属性类似于一把全局锁,只有当子goroutine
任务结束后,主goroutine
任务才能结束。
类似于守护线程。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 当前有任务 0 个
func f1(){
defer wg.Done() // 执行完成后,任务减 1
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine执行完毕")
}
func main() {
wg.Add(1) // 任务加 1 注意,一定要放外面,不能放函数中
go f1()
wg.Wait() // 任务必须为0时才继续向下执行
fmt.Println("主goroutine执行完毕")
}
GOMAXPROCS
该函数可设定开启多少os
线程来运行子goroutine
任务。
默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
如下示例,两个子goroutine
任务在一个线程上运行,会通过时间片轮询等策略来抢占执行权。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine1执行完毕")
}
func f2(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
}
func main() {
runtime.GOMAXPROCS(1) // 设置最多开启1个子线程
f1()
f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
时间轮询
由于底层的os
线程切换机制是依照时间轮询进行切换,所以goroutine
的切换时机也是由时间片轮询来决定的。
使用runtime.Gosched()
可让当前任务让出线程占用,交由其他任务进行执行。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
if i == 300 {
runtime.Gosched() // 让出线程占用
}
}
fmt.Println("子goroutine1执行完毕")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
}
func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
终止任务
runtime.Goexit()
终止当前任务。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
if i == 300 {
runtime.Goexit() // 终止任务
fmt.Println("子goroutine任务被终止")
}
}
fmt.Println("子goroutine执行完毕")
}
func main() {
go f1()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
通道使用
多个goroutine
中必须要有某种安全的机制来进行数据共享,这就出现了channel
通道。
它类似于管道或者队列,作用在于保证多goroutine
访问同一资源时达到数据安全的目的。
类型声明
channel
是引用类型,这就代表必须要使用make()
进行内存分配。
初始值为nil
。
下面是进行声明的示例:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
channel使用
使用前要进行内存分配,并且它还可选缓冲区。
代表该通道最多可容纳多少数据。当然,缓冲区大小是可选的,它具有动态扩容的特性。
make(chan 元素类型, [缓冲大小])
示例如下:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel操作
以下是channel
的操作:
方法 | 说明 |
---|---|
ch <- 数据 | 将数据放入通道中 |
数据 <- ch | 将数据从通道取出 |
close() | 关闭通道 |
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
我们通过调用内置的close()
函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
阻塞通道
当一个通道无缓冲区时,将被称为阻塞通道。
通道中存放一个值,但该值并没有被取出时将会引发异常。
必须先收,后发。因为发送后会产生阻塞,如果没有接收者则会导致死锁异常
必须将通道中的值取尽,否则会发生死锁异常,也就是说放了几次就要取几次
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
lily := <- ch // 等待取百合花
fmt.Println(rose)
fmt.Println(lily)
}
func main(){
wg.Add(1)
ch := make(chan string)
go f1(ch) // 必须先有接收者
ch <- "玫瑰花" // 开始放入玫瑰花
ch <- "百合花" // 开始放入百合花
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
非阻塞通道
非阻塞通道即为有缓冲区的通道。
只要通道的容量大于零,则代表该缓冲区中能够去存放值。
非阻塞通道相较于阻塞通道,它的使用其实更加符合人类逻辑
阻塞通道必须要先接收再存入
非阻塞通道可以先存入再接收
并且,非阻塞通道中的值可以不必取尽
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
fmt.Println(len(ch)) // 获取元素数量 1 代表还剩下一个没取
fmt.Println(cap(ch)) // 获取容量 10 代表最多可以放10个
fmt.Println(rose)
}
func main(){
wg.Add(1)
ch := make(chan string,10)
ch <- "玫瑰花" // 放入玫瑰花
ch <- "百合花" // 放入百合花
go f1(ch)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
单向通道
单向通道即是只能取,或者只能发。
上面的通道都是双向通道,可能造成阅读不明确的问题,故此Go
还提供了单向通道。
在函数传参中,可以将双向通道转换为单项通道,这也是最常用的方式。
通道标识 | 说明 |
---|---|
ch <- string | 代表只能写入string类型的值 |
<- ch string | 代表只能取出string类型的值 |
package main
import (
"sync"
"fmt"
)
var wg sync.WaitGroup
func recv(ch <-chan string) { // 只能取
defer wg.Done()
rose := <- ch
fmt.Println(rose)
}
func send(ch chan<- string) { // 只能放
defer wg.Done()
ch <- "玫瑰花"
}
func main() {
wg.Add(2)
ch := make(chan string, 10)
go send(ch)
go recv(ch)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
常见情况
以下是通道的使用常见情况。
关闭已经关闭的channel也会引发panic。
任务池
多个goroutine
的切换会带来性能损耗问题。
所以我们可以通过做一个goroutine
的池来解决这种问题,当一个goroutine
的任务结束后,它不会kill
掉该goroutine
,而是让它继续的取下一个任务。
所以我们需要与chan
结合进行构造一个简单的任务池。
如下示例,构建了一个简单的任务池并且开启了3个goroutine
,并且放了6个任务在task
这个chen
中交由run
进行处理。
处理结果放在result
这个chen
中。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func run(id int, task <-chan string, result chan<- string) {
defer wg.Done()
for {
t, ok := <-task
if !ok {
fmt.Println("处理完了所有任务")
break
}
time.Sleep(time.Second * 2)
t += fmt.Sprintf(":已由%d处理", id)
result <- t
}
}
func main() {
task := make(chan string, 10)
result := make(chan string, 10)
wg.Add(3)
for i := 0; i < 3; i++ {
go run(i, task, result) // 开三个goroutine来处理
}
urlRequeste := []string{
"www.baidu.com",
"www.google.com",
"www.cnblog.com",
"www.xinlang.com",
"www.csdn.com",
"www.taobao.com",
}
for _, url := range urlRequeste {
task <- url // 开启了六个任务
}
close(task)
for i := 0; i < len(urlRequeste); i++ {
fmt.Println(<-result)
}
close(result)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
// www.google.com:已由2处理
// www.cnblog.com:已由1处理
// www.baidu.com:已由0处理
// 处理完了所有任务
// 处理完了所有任务
// 处理完了所有任务
// www.taobao.com:已由0处理
// www.xinlang.com:已由2处理
// www.csdn.com:已由1处理
// 主goroutine运行完毕
select多路复用
类似于事件循环,我们来监听多个通道。
当一个通道可用时就来操纵该通道。
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
这个示例还是要在具体的应用场景中比较常见,并且一般的库都已经写好了。
只要知道其中理论就行,没必要白手写select
,除非你要做开源框架或公司框架等。
可处理一个或多个channel的发送/接收操作。
如果多个case同时满足,select会随机选择一个。
对于没有case的select{}会一直等待,可用于阻塞main函数。
小例子:
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch: // 允许赋值
fmt.Println("可以读了,已经读出了:", x) // 可读
case ch <- i: // 可写
fmt.Println("可以写了,已经写入了:", i)
}
}
}
锁相关
锁是为了解决资源同步的问题。
但是对于多个goroutine
通信应该是去使用channel
,而不是用锁进行解决。
互斥锁
如下代码,会产生资源竞争问题。致使结果不正确:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num ++
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num --
}
}()
wg.Wait()
fmt.Println(num)
}
// 13966
// 7578
// 9475
此时添加互斥锁即可,让其变为串行执行:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁
num ++
lock.Unlock() // 解锁
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁
num --
lock.Unlock() // 解锁
}
}()
wg.Wait()
fmt.Println(num)
}
读写互斥锁
互斥锁是完全互斥,将并发执行转变为串行执行,性能损耗比较大。
但是在更多的场景中,我们则不需要完全互斥。
比如多个人访问统一资源但是并未对资源本身做修改时可以不加锁,但是当有人对资源做修改时其他人将无法访问。
以上场景使用读写锁更加合适,读写锁在读多写少的场景下非常高效。
读锁:我获取了读锁你不能去修改,必须等我释放
写锁:我获取了写锁你不能去读,必须等我释放
如下,写入200次,读取2000次的用时为1s左右。
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
var rwlock sync.RWMutex // 读写锁
var variety = 10
func read() {
defer wg.Done()
rwlock.RLock() // 加读锁
fmt.Println(variety)
rwlock.RUnlock() // 释放读锁
}
func write() {
defer wg.Done()
rwlock.Lock() // 加写锁
variety ++
fmt.Println(variety)
rwlock.Unlock() // 释放写锁
}
func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("运行时间:",end.Sub(start)) // 1s左右
}
如果单纯使用互斥锁,时间会更长:
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex // 互斥锁
var variety = 10
func read() {
defer wg.Done()
lock.Lock() // 加互斥锁
fmt.Println(variety)
lock.Unlock() // 释放互斥锁
}
func write() {
defer wg.Done()
lock.Lock() // 加互斥锁
variety ++
fmt.Println(variety)
lock.Unlock() // 释放互斥锁
}
func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("运行时间:",end.Sub(start)) // 2s左右
}
sync.Once
只执行一次,如果一个配置文件体积过于巨大,在初始化时进行加载会拖慢启动速度。
所以我们可以在要使用时进行加载(懒惰加载),如下示例,有10个goroutine
都需要用到配置文件。
该配置文件只会加载一次,之后便不会重复加载。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var once sync.Once
func load() {
fmt.Println("加载配置文件...")
}
func main() {
fmt.Println("运行代码逻辑...发现很多地方都要用配置文件了")
for i := 0; i < 10; i++ {
fmt.Printf("%v需要用到配置文件,开始加载\n", i)
wg.Add(1)
go func() {
defer wg.Done()
once.Do(load) // 只加载一次,并且该函数的格式必须是不能有参数与返回值
}()
}
wg.Wait()
}
sync.Map
Go
语言中内置的map
不是并发安全的。不要使用内置的map
进行数据传递,你应该使用channel
或者sync
给你提供的map
。该map
不用进行make
初始化内存。
sync
提供的map
有以下功能:
方法 | 描述 |
---|---|
Store(k,v) | 设置一组键值对 |
Load(k) | 根据k取出v |
LoadorStore(k,v) | 根据k取出v,如果没有该k则创建v |
Delete(k) | 删除一组键值对 |
Range | 循环遍历出k和v |
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var once sync.Once
var m = sync.Map{} // g安全的map
func out() {
defer wg.Done()
gift, _ := m.Load("礼物")
fmt.Println(gift)
}
func put() {
m.Store("礼物", "玫瑰花")
defer wg.Done()
}
func main() {
wg.Add(2)
go put()
go out()
wg.Wait()
}
原子操作
功能概述
对于多个goroutine
访问同一资源造成的并发安全问题,可以通过加锁来进行解决。
但是加锁会使性能降低,所以这里Go
语言中sync/atomic
包提供了原子操作来代替加锁。
常用方法
主要对数字类型的数据的加减乘除等。
方法 | 描述 |
---|---|
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) |
读取操作 |
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StoreUint32(addr *uint32, val uint32) func StoreUint64(addr *uint64, val uint64) func StoreUintptr(addr *uintptr, val uintptr) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) |
写入操作 |
func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) |
修改操作 |
func SwapInt32(addr *int32, new int32) (old int32) func SwapInt64(addr *int64, new int64) (old int64) func SwapUint32(addr *uint32, new uint32) (old uint32) func SwapUint64(addr *uint64, new uint64) (old uint64) func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) |
交换操作 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) |
比较并交换操作 |
示例演示
使用原子操作,速度较快。
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var wg sync.WaitGroup
func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, 1)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, -1)
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间:", end - start) // 981600
fmt.Println(num)
}
加锁操作,速度会慢一些:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var lock sync.Mutex
func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num++
lock.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num--
lock.Unlock()
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间:", end - start) // 1000300
fmt.Println(num)
}
Go 并发操作的更多相关文章
- 第五十四节,socketserver通讯模块实现并发操作,真多线程并发
socketserver通讯模块实现并发操作,基于select.epoll.socket.多线程,实现的正真多线程多并发 socketserver通讯模块底层调用的socket模块,只是它作了处理基于 ...
- session文件无法并发操作
session_start():打开服务器上的session文件. session_commit():会把$_SESSION数组的内容写入到服务器上的session文件中,但不会清空$_SESSION ...
- SQL Server并发操作单个表时发生在page页面级的死锁
最近遇到的死锁问题都发生在并发操作单张表上,比较有意思,就模拟了重现了一下.根据非聚集索引为条件,删除某一个表的数据,类似于这么一个语句,delete from table where noclust ...
- 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁
不要在 foreach 循环里进行元素的 remove/add 操作.remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁. 正例: Iterator&l ...
- HashMap在JDK1.8中并发操作,代码测试以及源码分析
HashMap在JDK1.8中并发操作不会出现死循环,只会出现缺数据.测试如下: package JDKSource; import java.util.HashMap; import java.ut ...
- Django中管理并发操作
上一篇我们说了,如何在Django中进行事务操作,数据的原子性操作 涉及了事务操作,我们不得不考虑的另一个问题就是:并发操作 还是那个用户转账的操作 我们使用事务操作解决的操作中途服务器宕机问题 但是 ...
- CMU-15445 LAB2:实现一个支持并发操作的B+树
概述 经过几天鏖战终于完成了lab2,本lab实现一个支持并发操作的B+树.简直B格满满. B+树 为什么需要B+树 B+树本质上是一个索引数据结构.比如我们要用某个给定的ID去检索某个student ...
- JAVA 1.5 局部特性(可变参数/ANNOTATION/并发操作)
1: 可变参数 可变参数意味着可以对某类型参数进行概括,例如十个INT可以总结为一个INT数组,当然在固定长度情况下用数组是很正常的 这也意味着重点是可变,不定长度的参数 PS1:对于继承和重写我没有 ...
- java并发操作
项目中常用的java并发操作 一.java8新特性java并发流操作(结合纳姆达表达式) List<String> list = new ArrayList<>(); list ...
- C# 异步并发操作,只保留最后一次操作
在我们业务操作时,难免会有多次操作,我们期望什么结果呢? 绝大部分情况,应该是只需要最后一次操作的结果,其它操作应该无效. 自定义等待的任务类 1. 可等待的任务类 AwaitableTask: // ...
随机推荐
- 跟着兄弟连系统学习Linux-【day03】
day03-20200529 p10.学习注意事项 linux严格区分大小写(与python有点像) Linux中所有内容都是通过文件形式保存,通过命令执行设置参数,写 ...
- Mybatis源码学习第七天(插件源码分析)
为了不把开发和源码分析混淆,决定分开写; 接下来分析一下插件的源码,说道这里老套路先说一个设计模式,他就是责任链模式 责任链模式:就是把一件工作分别经过链上的各个节点,让这些节点依次处理这个工作,和装 ...
- Linux:使用SecureCRT来上传和下载文件
SecureCRT自带的有几种上传下载功能,SecureCRT下的文件传输协议有以下几种:ASCII.Xmodem.Ymodem.Zmodem. ASCII:这是最快的传输协议,但只能传送文本文件. ...
- Playbook使用,编写YAML
YAML是什么? YAML是一个可读性高.用来表达数据序列的格式语言 YAML:YAML Ain't a Markup Language YAML以数据为中心,重点描述数据的关系和结构 YAML的格式 ...
- Java链接db2相关
端口一般是50000或者60000 后面跟的可能是库名(个人猜测) 还有db2jcc.jar安装db2的可以从相应目录下载未安装的可以…(啥时候有空再传上来)
- Oracle sqlplus中退格键、DEL键、上下左右键无法使用乱码问题
功能描述:Oracle sqlplus中退格键.DEL键.上下左右键无法使用乱码 1.安装readline-8.0 ①下载readline-8.0.tar.gz文件,百度网盘下载路径: https:/ ...
- 腾讯云COS对象存储 Web 端直传实践(JAVA实现)
使用 腾讯云COS对象存储做第三方存储云服务,把一些文件都放在上面,这里主要有三中实现方式:第一种就是在控制台去设置好,直接上传文件.第二种就是走服务端,上传文件,就是说,上传文件是从服务端去上传上去 ...
- JDK13环境变量配置
第一步:下载JDK(开发工具包) JDK分为OracleJDK和OpenJDK下面简要说明 OracleJDK 部分代码闭源.商业收费 OpenJDK 开放源码.商业免费 两者大部分代码是共用的(除闭 ...
- k8s运行容器之deployment(三)
deployment 我们已经知道k8s是通过各种controller来管理pod的生命周期.为了满足不同业务场景,k8s开发了Deployment.ReplicaSet.DaemonSet.Stat ...
- python中的画笔控制函数
蟒蛇绘制代码中的画笔控制函数 penup() ,pendown() ,pensize() , pencolor()函数 这里就将海龟想象成画笔 画笔控制函数,画笔操作后一直有效,一般成对出现 将画笔抬 ...