互斥锁(mutex)是一种用于多线程编程中,以防止多个线程同时访问共享资源的同步机制。其主要作用是保证在同一时间内,只有一个线程能够访问到共享资源或执行特定的代码段。
1.互斥锁的主要作用包括:
-
防止数据竞争: 当多个线程尝试同时读写同一数据时,可能会导致数据不一致或损坏。互斥锁可以确保一次只有一个线程能够访问该数据。
-
保证操作的原子性: 某些操作可能由多个步骤组成,如果这些步骤被其他线程中断,可能会导致错误。互斥锁可以保证这些操作的原子性,即操作要么全部完成,要么全部不做。
-
协调线程对共享资源的访问: 互斥锁可以用来控制线程对共享资源(如文件、数据库连接等)的访问,确保资源在同一时间内只被一个线程使用。
-
避免死锁: 虽然互斥锁可以防止数据竞争,但如果不恰当使用,也可能导致死锁。死锁是指两个或多个线程互相等待对方释放资源,从而导致程序无法继续执行的情况。因此,合理设计锁的获取和释放机制是避免死锁的关键。
-
实现线程间的同步: 互斥锁可以与其他同步机制(如条件变量)配合使用,实现线程间的同步。例如,一个线程可以在满足特定条件之前等待,直到另一个线程通过互斥锁通知它条件已满足。
2.使用互斥锁的注意事项:
-
避免锁的过度使用:过度使用互斥锁可能会导致性能下降,因为线程需要等待锁的释放。
-
避免锁的嵌套:嵌套锁(即在一个已锁定的代码块中尝试获取另一个锁)可能导致死锁。
-
确保锁的释放:确保在函数退出或异常发生时,锁能够被正确释放。
-
使用锁的粒度:选择适当的锁粒度,即锁保护的代码范围。过大或过小的锁粒度都可能影响性能。
3.应用场景
1.创建一个虚拟购票系统,多线程来执行,让ticket数量--
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<unistd.h>
#include<pthread.h>
int ticket = 10000;
void *routine(void *arg)
{
char *id = (char*)arg;
while(1)
{
if(ticket > 0)
{
usleep(1000);
printf("%s sells ticket: %d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, routine, (void *)"thread 1");
pthread_create(&t2, nullptr, routine, (void *)"thread 2");
pthread_create(&t3, nullptr, routine, (void *)"thread 3");
pthread_create(&t4, nullptr, routine, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
2.未使用锁,会造成票变成负数
3.ticket变成负数的原因
四个线程(t1, t2, t3, t4)几乎同时运行,每个线程都试图访问和修改 ticket
变量。由于没有使用互斥锁(mutex)或其他同步机制来保护对 ticket
的访问,以下情况可能发生:
-
检查-修改-写入(Check-Modify-Write)竞态条件:
-
线程1检查
ticket
(假设为 5)。 -
线程2也检查
ticket
(也是 5)。 -
线程1 将
ticket
减 1,变为 4,并写回。 -
线程2 也将
ticket
减 1,但使用的是旧值 5,结果写回的是 4(而不是预期的 3)。 -
这导致
ticket
被多次减 1,最终可能变为负数。
-
解决方案
为了解决这个问题,需要确保每次只有一个线程可以修改 ticket
。这可以通过使用互斥锁(mutex)来实现。
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<unistd.h>
#include<pthread.h>
int ticket = 10000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *routine(void *arg)
{
char *id = (char*)arg;
while(1)
{
pthread_mutex_lock(&lock);
if(ticket > 0)
{
usleep(1000);
printf("%s sells ticket: %d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, routine, (void *)"thread 1");
pthread_create(&t2, nullptr, routine, (void *)"thread 2");
pthread_create(&t3, nullptr, routine, (void *)"thread 3");
pthread_create(&t4, nullptr, routine, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
4.加了锁之后为什么运行速度变慢了
1. 线程阻塞和唤醒的开销
当一个线程尝试获取一个已经被其他线程持有的锁时,该线程会被阻塞,直到锁被释放。线程的阻塞和唤醒涉及到操作系统的上下文切换,这是一个相对昂贵的操作,因为它需要保存和加载线程的执行状态。
2. 减少并行性
锁的存在限制了程序的并行性。当线程必须等待获取锁时,它不能执行,这意味着在多核处理器上可能有很多核心没有被充分利用。如果多个线程频繁地请求同一把锁,它们可能会相互阻塞,导致程序运行效率降低。
3. 锁竞争
如果多个线程频繁地访问同一把锁,可能会导致锁竞争。锁竞争会增加系统的开销,因为线程需要不断地检查锁的状态(是否可用)。在高竞争的情况下,线程可能大部分时间都在等待锁,而不是执行实际的工作。
4. 死锁风险
虽然死锁不直接导致性能问题,但它会使程序完全停止执行。为了避免死锁,程序可能需要实现更复杂的逻辑来管理锁的获取和释放,这会增加代码的复杂性和执行时间。
5. 锁的粒度
锁的粒度(即锁保护的代码范围)也会影响性能。细粒度锁(保护小范围代码)可以减少锁的竞争,但可能需要更多的锁,增加了管理的复杂性。粗粒度锁(保护大范围代码)可以减少锁的数量,但增加了线程阻塞的可能性。
优化策略
为了减少锁对性能的影响,可以考虑以下策略:
-
减少锁的粒度:尽量缩小锁保护的代码范围,减少线程等待锁的时间。
-
使用无锁编程技术:如果可能,使用原子操作或其他并发控制机制来避免使用锁。
-
锁分离:将一个大锁分解为多个小锁,减少锁竞争。
-
锁的选择:根据具体情况选择合适的锁类型,如读写锁(读写操作分离)可以提高读操作的并行性。
-
避免锁的嵌套:嵌套锁可能导致死锁,应尽量避免。
-
性能分析:使用性能分析工具来识别性能瓶颈,优化锁的使用。
5.锁的原理
(1)硬件:把时间中断
(2)软件:汇编指令swap或exchange,使寄存器和内存单元的数据交换