Go 和 C++ 中线程的区别

Him*_*dar 4 c++ multithreading mutex locking go

我有一个线程安全队列(在 Go 和 C++ 中实现),我在其中生成超过 100K 线程以将数据推送到共享队列中。我的笔记本电脑有 8 个核心和 16 个逻辑处理器以及 16GB RAM。

我在 Go 和 C++ 上都编写了相同的程序。在我的个人机器上,该程序在 Go 中的大量线程下运行良好,但当我使用 C++ 执行该程序时,程序崩溃并出现分段错误。我明白这是因为我有 8 个核心,但我正在产生大量线程。但我不明白 Go 是如何处理这种情况的,为什么 C++ 没有配置为以同样的方式处理这种情况?

Go 与 C++ 在内部处理线程方面有何不同?

Go 中的以下代码可以完美运行 100K 或以上线程。然而,C++ 甚至无法处理 5K 线程。

package main

import (
    "fmt"
    "math/rand"
    "sync"
)

type ConcurrentQueue struct {
    queue []int32
    lock  sync.Mutex
}

func (q *ConcurrentQueue) Enqueue(item int32) {
    q.lock.Lock()
    defer q.lock.Unlock()
    q.queue = append(q.queue, item)
}

func (q *ConcurrentQueue) Dequeue() int32 {
    q.lock.Lock()
    defer q.lock.Unlock()

    if len(q.queue) == 0 {
        panic("removing from an empty queue")
    }
    item := q.queue[0]
    q.queue = q.queue[1:]
    return item
}

func (q *ConcurrentQueue) Size() int {
    q.lock.Lock()
    defer q.lock.Unlock()
    return len(q.queue)
}

const NUM_THREADS int = 100000

func main() {
    queue := &ConcurrentQueue{
        queue: make([]int32, 0),
    }

    var wgE sync.WaitGroup
    var wgD sync.WaitGroup

    fmt.Println("size before enqueue:", queue.Size())
    for i := 0; i < NUM_THREADS; i++ {
        wgE.Add(1)
        go func() {
            queue.Enqueue(rand.Int31())
            wgE.Done()
        }()
    }

    wgE.Wait()
    fmt.Println("size after enqueue:", queue.Size())

    for i := 0; i < NUM_THREADS; i++ {
        wgD.Add(1)
        go func() {
            queue.Dequeue()
            wgD.Done()
        }()
    }

    wgD.Wait()

    fmt.Println("size after dequeue:", queue.Size())
}
Run Code Online (Sandbox Code Playgroud)

输出:

size before enqueue: 0
size after enqueue: 100000
size after dequeue: 0
Run Code Online (Sandbox Code Playgroud)

下面的 C++ 代码给出了 100K 线程的分段错误

C++

#include <iostream>
#include <queue>
#include <cstdlib>
#include <thread>
#include <mutex>
#include <condition_variable>

class ThreadSafeQueue
{
    private:
    std::queue<int> q;
    std::mutex qMutex;  
    std::condition_variable m_cond;
    public:
    ThreadSafeQueue() { }
    ThreadSafeQueue(const ThreadSafeQueue &) = delete ;
    ThreadSafeQueue& operator=(const ThreadSafeQueue &) = delete ;
    
    void Enqueue(int val);
    void Dequeue();
    size_t Size();
};

void ThreadSafeQueue::Enqueue(int val)
{
    std::lock_guard<std::mutex> guard(qMutex);
    q.push(val);
}

void ThreadSafeQueue::Dequeue()
{   
    std::unique_lock<std::mutex> lock(qMutex);

    m_cond.wait(lock, [this] {  return !q.empty(); }); 
    
    q.pop();
}

size_t ThreadSafeQueue::Size()
{
    return q.size();
}

void UpdateQueue(ThreadSafeQueue &qObj, int val)
{
    qObj.Enqueue(val);
}
void DeleteQueue(ThreadSafeQueue &qObj)
{
    qObj.Dequeue();
}

int main()
{
    int N;
    ThreadSafeQueue qObj;
    std::cout<<"How many values do you want to insert"<<std::endl;
    std::cin>>N;
    std::cout<<"Before inserting size of queue is "<<qObj.Size()<<std::endl;

    
    std::vector<std::thread> threads;
    for(int i = 0; i < N; i++)
    {
        threads.push_back(std::thread(UpdateQueue, std::ref(qObj), rand()));
    }

    for (auto& th : threads) th.join();
    threads.clear();
        
    std::cout<<"After inserting size of queue is "<<qObj.Size() <<std::endl;
    std::cout<<"Dequeuing using threads"<<std::endl;
    
    for(int i = 0; i < N; i++)
    {
        threads.push_back(std::thread(DeleteQueue, std::ref(qObj)));
    }
    for (auto& th : threads) th.join();
    std::cout<<"After dequeing size of queue is "<<qObj.Size()<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)

小智 9

Goroutines 与操作系统线程不同。C++ 中的线程实际上是操作系统线程。答案可能非常复杂,但简而言之:

  1. Go 有自己的运行时
  2. Go 可以在一个操作系统线程上运行多个 goroutine,它由 Go 调度程序管理。
  3. Go 调度程序正在窃取工作。您可以在此处找到更多详细信息:Go 中的调度:第二部分 - Go Scheduler
  4. Goroutines 是轻量级的。它们只有几个 kB 的堆栈(2-8kB,取决于 Go 版本,而且它也是动态的:可以通过 Go 增加/减少)。操作系统线程大小通常为几MB。
  5. 切换 Goroutine 时只涉及(保存/恢复)三个寄存器:程序计数器、堆栈指针和 DX。这就是为什么与操作系统线程相比它可以非常快地切换,以及为什么你可以比操作系统线程生成更多的 goroutine。