线程同步与互斥锁

2023-05-16

相比多进程模型,多线程模型最大的优势在于数据共享非常方便,同一进程内的多个线程可以使用相同的地址值访问同一块内存数据。但是,当多个线程对同一块内存数据执行“读−处理−更新”操作时,会由于线程的交叉执行而造成数据的错误。

例如以下代码段,当 thread_func() 同时在多个线程中执行时,更新到 glob_value 中的值就会互相干扰,产生错误结果。

#define LOOP_COUNT   1000000
int glob_value = 0;

void * thread_func(void * args)
{
    int counter = 0;
    while(counter++ < LOOP_COUNT)
    {
        int local = glob_value;
        local++;
        glob_value = local;
    }
}

解决这类问题的关键在于,当一个线程正在执行“读−处理−更新”操作时,保证其他线程不会中途闯入与其交叉执行。不可被打断的执行序列称为临界区,保证多个线程不会交叉执行同一临界区的技术称为线程同步。

1 互斥锁的使用

最常用的线程同步技术是互斥锁,Linux 线程库中的相关函数有:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

这里pthread的p代表POSIX线程

所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号。

pthread_mutex_lock() 负责在进入临界区之前对临界区加锁;
pthread_mutex_unlock() 负责在执行完临界区处理时给临界区解锁。

当某个线程试图给一个已经处在加锁状态的临界区再次加锁时,该线程就会被临时挂起,一直等到该临界区被解锁后,才会被唤醒并继续执行。

如果同时有多个线程等待某个临界区解锁,那下次被唤醒的进程取决于内核的调度策略,并没有固定的顺序。

静态分配的 mutex 变量在使用之前应该被初始化为 PTHREAD_MUTEX_INITIALIZER,而动态分配的 mutex 需要调用 pthread_mutex_init() 进行初始化,且只被某个线程初始化一次,可以利用 pthread_once() 函数方便完成。

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

多个线程在临界区上的执行是串行的,开发者应该尽量减少程序在临界区内的停留时间,以提高程序的并行性。因此,临界区不应该包含任何非必须的逻辑,以及任何可能带来高延迟的 IO 等操作

2 互斥锁的保护范围和使用顺序

对互斥锁加锁的不恰当使用会造成线程的死锁,比如下面这两种情况。

  1. 典型的情况是,两个线程执行时都需要锁定互斥锁 A 和 B,在一个线程中,锁定顺序是先锁定 A,后锁定 B,而另一个线程的锁定顺序是先锁定 B,再锁定 A。这种情况下,当一个线程已经锁定了 A 而另一个线程恰好锁定了 B 时,双方因互相争用对方已锁定的互斥锁,谁也不让步,而陷入死锁状态。
  1. 另一种情况是,一个线程已经锁定了互斥锁 A,但在其后的处理逻辑中试图再次锁定 A,这时该线程会让自己陷入睡眠状态,再也等不到被唤醒的时候。

因此,开发者需要仔细规划互斥锁保护范围和使用顺序

3 避免死锁的两个加锁函数

为了避免出现死锁问题,可以使用另外两种变体的锁定函数,如下所示:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

前者可以在锁定失败后立即返回,后者可以在一段超时时间后返回,

应用这两个函数可以处理这种错误情况,而避免陷入无限的死锁中。

在 Linux 中,实现互斥锁采用的是 Futex(Fast Userspace Mutex)方案。在该实现中,只有发生了锁的争用才需要陷入到内核空间中处理,否则所有的操作都可以在用户空间内快速完成。在大多数情况下,互斥锁本身的效率很高,其平均开销大约相当于几十次内存读写和算数运算所花费的时间。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程同步与互斥锁 的相关文章

随机推荐