文章目录
前言
线程同步和互斥是多线程编程中的两个重要概念,它们都是为了解决多线程环境下的资源共享和访问问题。
**线程同步:**线程同步是指多个线程按照一定的顺序执行,以确保在访问共享资源时,不会出现数据不一致的问题。同步机制主要包括条件变量和信号量。
**线程互斥:**线程互斥是指在同一时刻,只允许一个线程访问共享资源,以防止数据不一致和竞争条件。互斥锁是实现线程互斥的主要工具。
在操作系统中,锁、条件变量和信号量都是用来实现线程同步与互斥的机制。
锁(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.