首页 > 其他分享 >Golang - 并发同步更新全局切片失败的原因以及解决方案

Golang - 并发同步更新全局切片失败的原因以及解决方案

时间:2024-04-24 20:22:55浏览次数:27  
标签:wg 切片 协程 failedList fmt sync Golang 并发

当多个协程同时访问和修改同一个共享资源(如切片)时,如果没有适当的同步机制,可能会导致数据竞争和不一致的结果。

package main

import (
    "fmt"
    "sync"
)

func processChunk(chunk []int64, wg *sync.WaitGroup, failedList []int64) {
    defer wg.Done()
    fmt.Println("failedList======open1======", failedList)

    for _, uid := range chunk {
        // 将失败的UIDs添加到failedList
        failedList = append(failedList, uid)
    }
    fmt.Println("failedList======open2======", failedList)
}

func main() {
    // 假设UID数组
    uidArr := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    chunkSize := 500
    var (
        wg         sync.WaitGroup
        failedList []int64 // 发送失败的全局uids
    )
    fmt.Println("failedList======start=======", failedList)
    // 每500个UID启动一个协程进行数据更新
    for i := 0; i < len(uidArr); i += chunkSize {
        end := i + chunkSize
        if end > len(uidArr) {
            end = len(uidArr)
        }
        wg.Add(1)

        go processChunk(uidArr[i:end], &wg, failedList)
    }

    fmt.Println("测试前")
    wg.Wait() // 等待所有协程执行完毕
    fmt.Println("测试后,长度为:", len(failedList))
    if len(failedList) != 0 {
        fmt.Println("failedList======end======", failedList)
    }
}

原因:

在 Go 语言中,切片(slice)是引用类型,当将一个切片作为参数传递给一个函数时,实际上传递的是该切片的副本,包括它的底层数组、长度和容量。但是,这个副本的底层数组指针仍然指向原始的底层数组。
在提供代码中,failedList 切片被传递给 processMailChunk 函数。在 processMailChunk 函数内部,试图通过 append 函数修改 failedList。然而,这个修改仅影响了函数内部的 failedList 副本,并没有影响到函数外部的原始 failedList 切片。
这是因为 append 函数可能会返回一个新的切片,如果原切片没有足够的容量来容纳新的元素。在代码中,每次 append 都会导致 failedList 副本指向一个新的底层数组(或者至少是修改长度后的同一个底层数组的不同部分)。
要修复这个问题,需要在 processMailChunk 函数中返回一个修改后的 failedList,然后在主函数中更新全局的 failedList。但是,由于使用了多个协程,并且每个协程都试图修改同一个切片,这会导致竞态条件。
一个更好的方法是使用 sync.Map 或者 sync.Mutex 来保护对共享资源的访问。由于 failedList 是一个切片,使用 sync.Map 可能不太方便,所以我们可以使用 sync.Mutex 来保护对 failedList 的访问。
【processMailChunk 函数接收一个指向 failedList 的指针(*[]int64),以及一个指向 sync.Mutex 的指针】----通过传递地址实现共享资源的线程安全访问

正解:

package main

import (
    "fmt"
    "sync"
)

func processChunk(chunk []int64, wg *sync.WaitGroup, failedList *[]int64) {
    defer wg.Done()
    fmt.Println("failedList======open1======", failedList)

    for _, uid := range chunk {
        // 将失败的UIDs添加到failedList
        *failedList = append(*failedList, uid) // 关键位置
    }
    fmt.Println("failedList======open2======", failedList)

}

func main() {
    // 假设UID数组
    uidArr := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    chunkSize := 500
    var (
        wg         sync.WaitGroup
        failedList []int64 // 发送失败的全局uids
    )
    fmt.Println("failedList======start=======", failedList)
    // 每500个UID启动一个协程进行数据更新
    for i := 0; i < len(uidArr); i += chunkSize {
        end := i + chunkSize
        if end > len(uidArr) {
            end = len(uidArr)
        }
        wg.Add(1)

        go processChunk(uidArr[i:end], &wg, &failedList)
    }

    fmt.Println("测试前")
    wg.Wait() // 等待所有协程执行完毕
    fmt.Println("测试后")
    if len(failedList) != 0 {
        fmt.Println("failedList======end======", failedList)
    }
}

总结:

  这个问题与在Go中使用sync.WaitGroup时传递其地址是类似的。在Go中,当使用sync.WaitGroup来等待一组协程完成时,需要传递*sync.WaitGroup(即WaitGroup的地址)给这些协程,以便它们能够调用Add和Done方法来增加或减少等待计数。这是因为Add和Done方法都是修改WaitGroup的内部状态,而这个状态是共享的。如果直接传递WaitGroup的值而不是它的地址,那么每个协程将会操作它自己的副本,而不是全局的WaitGroup实例,这将导致主函数无法正确等待所有协程完成。同样地,当需要在多个协程之间共享和修改一个切片时,也需要传递这个切片的地址(即*[]int64),以便所有协程都能操作同一个切片实例。否则,每个协程可能会操作它自己的切片副本,从而导致主函数无法看到其他协程所做的修改。
  为了解决这个问题并确保线程安全,需要做两件事:
1) 传递共享资源的地址(无论是*sync.WaitGroup还是*[]int64)给协程,以便它们能够操作同一个实例。
2) 使用某种同步机制(如sync.Mutex)来保护对共享资源的访问,以避免数据竞争。

标签:wg,切片,协程,failedList,fmt,sync,Golang,并发
From: https://www.cnblogs.com/beatle-go/p/18156231

相关文章

  • golang 实现文件下载
    golang实现文件下载packagemainimport("fmt""html/template""io""io/fs""mime""net/http""os""path/filepath""regexp"&qu......
  • 并发编程(Semaphore)
    Semaphore,信号量,它保存了一系列的许可(permits),每次调用acquire()都将消耗一个许可,每次调用release()都将归还一个许可特性Semaphore通常用于限制同一时间对共享资源的访问次数上,也就是常说的限流。下面我们一起来学习Java中Semaphore是如何实现的。类结构Semaphore中包含了一......
  • 并发编程(CyclicBarrier)
    CyclicBarrier是一个同步器,允许一组线程相互之间等待,直到到达某个公共屏障点(commonbarrierpoint),再继续执行CyclicBarrier与CountDownLatch异同都可以阻塞一组线程等待被唤醒CyclicBarrier是最后一个线程到达后会自动唤醒,而CountDownLatch需要显式调用countDown方法Cyc......
  • golang通过sock进行通信
    只是demo,生产环境要防止粘包。可以作为多进程之间通讯。。。。serverpackagemainimport( "fmt" "net" "os" "os/signal" "sync" "syscall")//客户端连接结构typeClientstruct{ Conn*net.UnixConn}varclients=make(map......
  • Java并发工具类之LongAdder原理总结
    出处: Java并发工具类之LongAdder原理总结LongAdder实现原理图                                高并发下N多线程同时去操作一个变量会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发......
  • 并发编程(ReentrantReadWriteLock)
    ReentrantReadWriteLock是一个可重入读写锁,内部提供了读锁和写锁的单独实现。其中读锁用于只读操作,可被多个线程共享;写锁用于写操作,只能互斥访问ReentrantReadWriteLock尤其适合读多写少的应用场景读多写少:在一些业务场景中,大部分只是读数据,写数据很少,如果这种场景下依然使用......
  • python 基础习题2--字符串切片技术
    1. 有如下字符串str='123456789'字符串切片技术,例如,返回输出从第三个开始到第六个的字符(不包含)即得到:345利用字符串切片技术,代码可以这么写:print(str[2:5])如果想返回如下八行结果,利用字符串切片技术,如何编写代码?12345678912345678134534567892412345678912345678......
  • c# 通过消息队列处理高并发请求实列
    网站面对高并发的情况下,除了增加硬件,优化程序提高以响应速度外,还可以通过并行改串行的思路来解决。这种思想常见的实践方式就是数据库锁和消息队列的方式。这种方式的缺点是需要排队,响应速度慢,优点是节省成本。演示一下现象创建一个在售产品表CREATETABLE[dbo].[product]([......
  • 并发编程(ReentrantLock)
    ReentrantLock是独占锁,每次只能有一个线程能获取到锁(支持重入)。其他未获取锁的线程会放入的CLH队列中,等待当前线程唤醒;主要分为公平锁和非公平锁,由内部类FairSync和NoFairSync来实现。主要的区别在于非公平锁每次都会尝试竞争,竞争不到锁才会放入到CLH队列中NonfairSync类......
  • day20-并发编程(下)
    1.多进程开发进程是计算机中资源分配的最小单元;一个进程中可以有多个线程,同一个进程中的线程共享资源;进程与进程之间则是相互隔离。Python中通过多进程可以利用CPU的多核优势,计算密集型操作适用于多进程。1.1进程介绍importmultiprocessingdeftask(): passif__name......