多线程与互斥锁
Posted 西工大舞蹈机器人基地
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程与互斥锁相关的知识,希望对你有一定的参考价值。
例如以下代码段,当 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 互斥锁的保护范围和使用顺序
对互斥锁加锁的不恰当使用会造成线程的死锁,比如下面这两种情况。
典型的情况是,两个线程执行时都需要锁定互斥锁 A 和 B,在一个线程中,锁定顺序是先锁定 A,后锁定 B,而另一个线程的锁定顺序是先锁定 B,再锁定 A。这种情况下,当一个线程已经锁定了 A 而另一个线程恰好锁定了 B 时,双方因互相争用对方已锁定的互斥锁,谁也不让步,而陷入死锁状态。
另一种情况是,一个线程已经锁定了互斥锁 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);
前者可以在锁定失败后立即返回,后者可以在一段超时时间后返回,应用这两个函数可以处理这种错误情况,而避免陷入无限的死锁中。
以上是关于多线程与互斥锁的主要内容,如果未能解决你的问题,请参考以下文章