锁与信号量的深度总结与实现【c++与golang】【万字总结】


前言

线程同步和互斥是多线程编程中的两个重要概念,它们都是为了解决多线程环境下的资源共享和访问问题。
**线程同步:**线程同步是指多个线程按照一定的顺序执行,以确保在访问共享资源时,不会出现数据不一致的问题。同步机制主要包括条件变量和信号量。
**线程互斥:**线程互斥是指在同一时刻,只允许一个线程访问共享资源,以防止数据不一致和竞争条件。互斥锁是实现线程互斥的主要工具。
在操作系统中,锁、条件变量和信号量都是用来实现线程同步与互斥的机制。
锁(Lock)是一种互量,用于保护共享资源,防止多个线程同时访问。当一个线程获得锁时,其他线程必须等待该线程释放锁后才能访问共享资源。常见的锁包括互斥锁和读写锁。
条件变量(Condition Variable)是一种线程间通信的机制,用于等待某个条件的发生。当一个线程等待某个条件时,它会阻塞并释放锁,直到另一个线程发出信号通知该条件已经满足,该线程才会被唤醒并重新获得锁。
信号量(Semaphore)是一种计数器用于控制多个线程对共享资源的访问。当一个线程访问共享资源时它会尝试获取信号量,如果号量的值大于 0,则该线程可以访问共享资源并将信号量的值减 1;如果信号量的值等于 0,则该线程必须等待其他线程释放信号量后才能访问共享资源。

本文分别用c++与golang来使用各种锁,条件变量、信号量来实现线程间的同步和互斥

一、互斥锁和自旋锁

与单线程编程不同的是,在多线程编程中,多个线程之间可能会同时访问同一个变量、函数或对象等共享资源,因此需要采取措施来避免数据竞争等并发问题。

在 C++ 中,mutex 是一个同步原语,用于协调不同线程的访问和修改。C++11 引入了标准库中的 std::mutex 类,它提供了 lock() 和 unlock() 两个函数来控制互斥锁的状态。
std::mutex 的本质是一个操作系统提供的锁,它分为用户模式锁和内核模式锁。当线程试图去获取锁时,如果锁当前没有被其它线程持有,则该线程将获得这个锁,并且可以继续执行。否则,该线程将进入阻塞状态,直到锁被释放为止。
**在 Go 中,sync.Mutex 是互斥锁的实现。**它有两个方法:Lock 和 Unlock,用于控制互斥锁的状态。sync.Mutex 的底层实现是基于操作系统提供的 futex(fast userspace mutex)机制,它使得线程在获取锁时可以避免进入内核态,并且能够更加高效地进行线程上下文切换。这使得 Go 中的互斥锁在资源竞争较小、并发度较高的情况下有着良好的性能表现。

c++版本

我们首先定义了一个共享变量 num,并在 AddWithOutLock 和 AddWithLock 两个函数中分别对其进行累加操作。其中,AddWithOutLock 是不加锁的版本,而 AddWithLock 是使用互斥锁保护的版本。

在 main 函数中,我们使用 emplace_back 函数向线程数组 threads 中添加子线程对象,并分别传入 AddWithOutLock 和 AddWithLock 函数来创建十个不加锁和加锁的线程。接着,我们使用 join 函数阻塞主线程,等待所有线程执行完毕,并输出累加结果


mutex mtx;
int num = 0;
void AddWithLock() {
   
   for (int i = 0; i < 2000; i++) {
   
      mtx.lock();
      num++;
      mtx.unlock();
   }
}
void AddWithOutLock() {
   
   for (int i = 0; i < 2000; i++) {
   
      num++;
   }
}

int main() {
   
   vector<thread> threads;  // 创建线程数组
   for (int i = 0; i < 10; i++) {
   
      threads.emplace_back(
          AddWithOutLock);  // 用 emplace_back 函数向数组添加元素
   }
   for (auto& t : threads)  // 遍历线程数组
   {
   
      t.join();  // 阻塞当前线程,等待子线程结束
   }
   cout << "AddWithOutLock: " << num << endl;

   // 加锁版测试
   num = 0;          // 重置 num 值
   threads.clear();  // 清空线程数组
   for (int i = 0; i < 10; i++) {
   
      threads.emplace_back(AddWithLock);
   }
   for (auto& t : threads) {
   
      t.join();
   }
   cout << "AddWithLock: " << num << endl;

   return 0;
}

从运行结果可以看出,不加锁的版本出现了数据错误,而加锁的版本则能够正确地累加结果,说明使用互斥锁确实可以有效避免并发问题。
在这里插入图片描述

golang版本

在 main 函数中,我们创建了十个 AddWithOutLock 和 AddWithLock 的 goroutine,并且使用 wg.Add 和 wg.Wait 方法控制并发,保证所有 goroutine 都执行完毕后再输出结果。在 Goroutine 中,和C++代码一样,我们使用 sync.Mutex 来实现互斥锁的功能,保证 num 变量的线程安全。



var mtx sync.Mutex
var wg sync.WaitGroup
var num int
func AddWithLock() {
   
	defer wg.Done()
	for i:=0;i<2000;i++{
   
	mtx.Lock()
		num++
	mtx.Unlock()
   }
}
func AddWithOutLock() {
   
	defer wg.Done()
	for i:=0;i<2000;i++{
   
		num++
   }
}
func main() {
   
    wg.Add(10)
	for i:=0;i<10;i++{
   
		go AddWithOutLock()
	}
    wg.Wait()
	fmt.Println("AddWithOutLock:",num)
	
	
	num=0
		wg.
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值