【协作式原创】查漏补缺之Go并发问题

主要回答一下几个问题
1.单核并发问题
2.多核并发问题
2.几个不正确的同步案例

1.单核并发问题

  • 先看一段go(1.11)代码: 单核CPU,1万个携程,每个携程执行100次+1操作, 思考n最终会打印多少?
package main
import (
    "fmt"
    "time"
    "runtime"
    "sync"
)
var n int
var wg sync.WaitGroup

func main() {
    runtime.GOMAXPROCS(1) //单核 
        // runtime.GOMAXPROCS(2) //多核 
    wg.Add(10000)
    for i:=0;i<10000;i++{
        go add()
    }
    wg.Wait()
    fmt.Println("累加结果:",n)
}
func add() {
    for i := 0; i < 100; i++ {
        n++
        time.Sleep(1)
    }
    wg.Done()
}
//output 单核
累加结果: 1000000
//output 多核
累加结果: 970820
  • 对比一段c语言多线程代码(单核运行),思考TestInteger会打印多少
// 编译: gcc main.c -o main -plthread
// 运行: ./main.exe
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 重定义数据类型
typedef signed   int    INT32;
typedef unsigned int    UINT32;
// 宏定义
#define THREAD_NUM     2              // 线程个数
UINT32 g_iTestInteger = 0;
// 函数声明
void ProcessTask(void *pParam);
int main(void) { 
    pthread_t MultiHandle  = 0;      // 多线程句柄
    UINT32    iLoopFlag    = 0;
    INT32     iRetVal      = 0;  // 创建线程函数的返回值
    // 循环创建线程
    for (iLoopFlag = 0; iLoopFlag < THREAD_NUM; iLoopFlag ++)
    {
        iRetVal = pthread_create(&MultiHandle, NULL, (void * (*)(void *))(&ProcessTask), (void *)iLoopFlag);
        if (0 != iRetVal)
        {
            printf("Create ProcessTask %d failed!\n", iLoopFlag);
            return -1;
        }
    }
    Sleep(2000);    /* windows 使用Sleep,参数为毫秒 */
    printf("In main, TestInteger = %d\n", g_iTestInteger);
    return 0;   
}
void ProcessTask(void *pParam){
    for (int i = 0;i<100;i++){
        g_iTestInteger ++;
        Sleep(1);    /* windows 使用Sleep,参数为毫秒 */
    }
}
//output
In main, TestInteger = 198

Q: 单核环境下,对于n++问题,go为什么没有并发问题,而c语言有并发问题?
A:

  1. n++对应的汇编指令是3条.
    1.1 加载: 加载n到寄存器,
    1.2 更新: 更新寄存器(n+1)
    1.3 存储(写回内存): 把寄存器的值存储到内存中n对应的内存地址中
    参考 <深入理解计算机系统第3版> 12.3小节的图12-18b:
  2. c语言的多线程调度是抢占式的,多线程的上下文切换可以发生在任何指令之间(TODO除了少数原子指令)。
    所以c语言是有并发问题的。

  3. go的非抢占式调度携程,上述代码只在sleep时发生携程切换,所以n++的3个指令可以一次执行完成,然后进入sleep才切换到另一个go携程,所以每个携程的n++是串行执行的,即使用1万个携程来测试也没有并发问题:
0++
sleep切换
1++
sleep切换
打印2,退出

总结:go在sleep时才发生协程切换,c语言的多线程切换可能发生在任何指令处,两者的切换粒度不一样。

TODO:go1.11具体是怎么一个非抢占式度。

Q: 多核为什么有并发问题?

A:

对于go

尽管携程是非抢占式调度的,但是如果有多核的话,就有多个P来同时执行携程。TODO

对于c

  1. 同时多个cpu读取到了相同的n,后提交的线程会把先提交的线程的n++结果覆盖掉,导致部分线程加1操作丢失。

Q: 加锁时如何解决c语言的多核多线程并发问题
A:

  1. 锁的两种底层原理
    总线锁:
    缓存锁:

https://studygolang.com/articles/18630

  1. window下搭建c语言运行环境
  2. vscode使用Code Runner插件运行程序
  3. C语言多线程中变量累加问题的分析

TODO

加锁的2种底层实现


然后加锁操作的话,对应图中就是对cpu总线加锁,使得同一时刻只有一个cpu能访问内存。但是这个效率比较低,于是有了基于cpu缓存的锁。
加锁的2种底层实现,我在这看的:https://mp.weixin.qq.com/s/RDEQSOjrSBVYVq6LV5MslQ

Q: 问:如何实现x++的原子性?

在单处理器上,如果执行x++时,禁止多线程调度,就可以实现原子。因为单处理的多线程并发是伪并发。
在多处理器上,需要借助cpu提供的Lock功能。锁总线。读取内存值,修改,写回内存三步期间禁止别的CPU访问总线。同时我估计使用Lock指令锁总线的时候,OS也不会把当前线程调度走了。要是调走了,那就麻烦了。
CPU中的原子操作

猜你喜欢

转载自www.cnblogs.com/yudidi/p/12298035.html