12.线程同步

12.线程同步

  • 1. 为什么需要线程同步
  • 2. 互斥锁
    • 2.1 互斥锁初始化
      • 2.1.1 PTHREAD_MUTEX_INITIALIZER 宏初始化
      • 2.1.2 使用函数初始化
    • 2.2 加锁和解锁
    • 2.3 pthread_mutex_trylock()
    • 2.4 销毁互斥锁
    • 2.5 互斥锁死锁
    • 2.6 互斥锁的属性
  • 3. 条件变量
    • 3.1 条件变量初始化
    • 3.2 通知和等待条件变量
    • 3.3 条件变量的判断条件
  • 4. 自旋锁
    • 4.1 初始化
    • 4.2 加锁和解锁
  • 5. 读写锁
    • 5.1 初始化
    • 5.2 加锁和解锁
    • 5.3 属性
  • 6. 线程安全
    • 6.1 线程栈
    • 6.2 可重入函数
    • 6.3 线程安全函数
    • 6.4 一次性初始化
    • 6.5 线程特有数据
      • 6.5.1 pthread_key_create()
      • 6.5.2 pthread_setspecific()
      • 6.5.3 pthread_getspecific()
      • 6.5.4 pthread_key_delete()
    • 6.6 线程局部存储

1. 为什么需要线程同步

线程的主要优势在于资源的共享性,但是多个线程并发访问共享数据所导致的数据不一致问题。**线程同步是为了对共享资源的访问进行保护。保护的目的是为了解决数据一致性的问题。**只有当多个线程都可以修改或获取这个变量的时候,就需要进行同步。出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问。
线程同步就是同一时间只允许一个线程访问该变量,防止出现并发访问。下面介绍的就是 Linux 系统中用于实现同步的机制。

2. 互斥锁

在访问共享资源之前对互斥锁进行上锁,访问完后进行解锁。上锁后,其他线程就不允许访问这一部分共享资源。如果解锁时有一个以上的线程阻塞,那么这些被阻塞的线程就会被唤醒,都会尝试对互斥锁进行加锁,加锁成功后,其他线程进行阻塞等待。
互斥锁用 pthread_mutex_t 类型表示,使用之前,需要先进行初始化。

2.1 互斥锁初始化

2.1.1 PTHREAD_MUTEX_INITIALIZER 宏初始化

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

这种方法只适用在定义的时候初始化,对于其他情况就不行,譬如先定义然后再进行初始化,或者在堆中动态分配的互斥锁。

2.1.2 使用函数初始化

#include 
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);

attr 指向的对象是互斥锁的属性,如果设置为 NULL,表示属性为默认值。函数成功调用就返回 0,失败返回非 0 的错误码。

2.2 加锁和解锁

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

进行上锁时,如果处于未上锁状态,就调用成功,函数立即返回;如果互斥锁已经被锁定,那么函数会阻塞,直到该互斥锁解锁,然后锁定并返回。
进行解锁时,不能对未处于锁定状态的互斥锁进行解锁,也不能解锁其它线程锁定的互斥锁。如果有多个线程处于阻塞状态等待被解锁,当互斥锁被当前锁定它的线程解锁后,这些等待的线程都能够进行上锁,但是无法确定具体是哪一个线程。

2.3 pthread_mutex_trylock()

如果在互斥锁被锁时,不希望线程被阻塞,可以使用该函数。该函数尝试对互斥锁进行加锁,如果互斥锁处于未锁状态,那么函数会上锁并立马返回。如果已经被锁,就会加锁失败,但不会被阻塞,而是返回错误码 EBUSY

#include 
int pthread_mutex_trylock(pthread_mutex_t *mutex);

2.4 销毁互斥锁

当不需要互斥锁时,应该将其销毁。

#include 
int pthread_destroy(pthread_mutex_t *mutex);

注意:不能销毁还没有解锁的互斥锁,也不能销毁还未初始化的互斥锁。销毁后,就不能再进行上锁或解锁,要重新进行初始化才能够使用

2.5 互斥锁死锁

如果一个线程试图对同一个互斥锁加锁两次,该线程就会陷入死锁状态,一直被阻塞永远出不来,除此之外,还有其它很多种方式也能产生死锁。譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。
为了避免上诉的情况,就需要定义层级关系,这里就不介绍了。

2.6 互斥锁的属性

定义 pthread_mutexattr_t 对象之后,需要进行初始化,不再使用时,需要将其销毁.

#include 
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

属性有很多种,这里只讨论类型属性。

  • PTHREAD_MUTEX_NORMAL: 标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定性。
  • PTHREAD_MUTEX_ERRORCHECK: 会提供错误检查。。譬如这三种情况都会导致返回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
  • PTHREAD_MUTEX_RECURSIVE: 此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
  • PTHREAD_MUTEX_DEFAULT : 此类 互 斥锁提供默认的行为和特性。使 用 宏PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁,或 者调用参数 arg 为 NULL 的 pthread_mutexattr_init() 函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性
#include 
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
// 调用成功,将互斥锁类型属性保存在参数 type 所指向的内存中,通过它返回出来
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
// attr 指向的对象类型属性设置为 type 指定的类型。

3. 条件变量

条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止。通常情况下,条件变量和互斥锁一起搭配使用的,使用条件变量主要包括两个动作:一个线程等待某个条件满足而被阻塞;另一个线程中,条件满足时发出信息。也就是常说的生产消费模型。
条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就会引发线程不安全问题。

3.1 条件变量初始化

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;	// 使用宏进行初始化

#include 
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond,const pthread_condaddr_t *attr);

注意:

  • 对已经初始化的条件变量再进行初始化,将可能导致未定义行为;
  • 对没有进行初始化的条件变量进行销毁,也可能导致未定义行为;
  • 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的

3.2 通知和等待条件变量

条件变量的主要操作就是发送信号和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知之前一直处于阻塞状态。

#include 
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

前两个函数都是向指定的条件变量发送信号,第一个是唤醒所有线程,第二个只需要至少有一个线程被唤醒。
等待函数是一个阻塞式函数,当判断程序中条件变量不满足时,调用该函数将线程设置为阻塞状态。在函数内部会对 mutex 进行操作,在调用函数时,会把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表上,然后将互斥锁解锁;当该函数返回时,会再次锁住互斥锁。就算是唤醒全部线程,互斥锁也只能被一个线程锁住,其它线程进入阻塞状态。
条件变量并不保存信息,只是传递应用程序状态信息的一种通讯机制。

3.3 条件变量的判断条件

在判断条件时,使用的是 while() 循环,而不是 if,因为当函数返回时,并不能确定条件的成立与否,要立即重新检查判断条件,如果条件不满足,就继续休眠等待。因为如果有多个线程在等待条件变量,任何线程都有可能率先醒来获取互斥锁,就可能会对条件变量进行修改,改变判断条件的状态

4. 自旋锁

自旋锁和互斥锁相似,事实上,互斥锁是由自旋锁实现的。
如果在获取自旋锁时处于未锁定状态,就立即获得锁,如果已经上锁,就在原地自旋,直到该自旋锁的持有者释放了锁。由此可知,自旋锁一直占用 CPU,如果不能短时间内获得锁,就会使 CPU 效率降低。
自旋锁在用户态中使用的比较少,通常使用在内核代码中。因为自旋锁可以在中断服务函数中使用,而互斥锁不行,在执行中断服务函数时要求不能休眠,不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了 CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就导致了死锁。

4.1 初始化

#include 
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

pshared 表示自旋锁的进程共享属性,PTHREAD_PROCESS_SHARED 共享自旋锁,可以在多个进程中的线程之间共享;PTHREAD_PROCESS_PRIVATE 私有自旋锁,只有本进程内的线程才能够使用该自旋锁。

4.2 加锁和解锁

#include 
int pthread_spin_lock(pthread_spinlock_t *lock);
// 一直自旋,直到获取到
int pthread_spin_trylock(pthread_spinlock_t *lock);
// 如果未能获取,就立即返回错误,错误码 EBUSY
int pthread_spin_unlock(pthread_spinlock_t *lock);

5. 读写锁

读写锁有三种状态,读加锁、写加锁和不加锁状态,一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。
当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作的线程都会被阻塞;
处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功,但是以写模式的线程会被阻塞,直到所有持有读模式的线程释放它们的锁为止。

5.1 初始化

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
#include 
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

5.2 加锁和解锁

#include 
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

当读写锁处于写模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()或 pthread_rwlock_wrlock()函数均会获取锁失败,从而陷入阻塞等待状态;当读写锁处于读模式加锁状态时,其它线程调用 pthread_rwlock_rdlock() 函数可以成功获取到锁,如果调 pthread_rwlock_wrlock()函数则不能获取到锁,从而陷入阻塞等待状态。
如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock() 和 pthread_rwlock_trywrlock() 来尝试加锁,如果不可以获取锁时。这两个函数都会立马返回错误,错误码为 EBUSY。

5.3 属性

#include 
// 属性的定义和销毁函数
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
// 属性的获取和设置函数
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

读写锁只有一个共享属性。get 时将属性存放在 pshared 所指向的内存中,set 时设置为内存中指定的指,可取值如下:

  • PTHREAD_PROCESS_SHARED: 共享读写锁。该读写锁可以在多个进程中的线程之间共享;
  • PTHREAD_PROCESS_PRIVATE: 私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值。

6. 线程安全

6.1 线程栈

进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈。每个线程运行过程中所定义的局部变量都是分配在自己的线程栈中,不会互相干扰。

6.2 可重入函数

可重入函数就是被同一个进程的多个不同执行流并行调用,并且每次调用都能产生正确的结果,就叫做可重入函数。

6.3 线程安全函数

一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流) 同时调用
时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。 线程安全函数包括可重入函数, 也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数。

6.4 一次性初始化

#include 
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

保证init_routine函数只执行一次,但是不能保证在哪个线程中执行,是由内核调度决定的。
在调用 pthread_once() 函数之前,需要定义一个 pthread_once_t 类型的静态变量,通常使用宏pthread_once_t once_control = PTHREAD_ONCE_INIT;进行初始化。
如果参数 once_control 指向的 pthread_once_t 类型变量,其初值不是 PTHREAD_ONCE_INIT,pthread_once() 的行为将是不正常的;
如果在一个线程调用 pthread_once() 时,另外一个线程也调用了,则该线程将会被阻塞等待, 直到第一个完成初始化后返回。换言之,当调用 pthread_once 成功返回时,调用总是能够保证所有的状态已经初始化完成了。

6.5 线程特有数据

也称为线程私有数据,就是为每个调用线程分别维护一份变量的副本,每个线程通过特有数据键(key)访问时,这个特有数据键都会获取到本线程绑定的变量副本,这样就可以避免变量成为多个线程间的共享数据。

6.5.1 pthread_key_create()

#include 
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

在为线程分配私有数据区之前,需要调用该函数创建一个特有数据键,并且只需要在首个调用的线程中创建一次即可。通过 key 所指向的缓冲区返回给调用者,destructor 是一个自定义析构函数,通常用于释放与特有数据键关联的线程私有数据区占用的内存空间,当使用线程特有数据的线程终止时,析构函数会自动调用。

6.5.2 pthread_setspecific()

#include 
int pthread_setspecific(pthread_key_t key, const void *value);

创建特有数据键之后,需要为调用线程分配私有数据缓冲区,每个调用线程分配一次,且只会在线程初次调用此函数时分配。首先保存指向线程私有数据缓冲区的指针,并将其与特有数据键以及当前调用线程关联起来。
value 就是一块由调用者分配的内存,作为线程的私有数据缓冲区,当线程终止时,会自动调用参数 key 指定的特有数据键对应的析构函数来释放这一块动态申请的内存空间。

6.5.3 pthread_getspecific()

#include 
void *pthread_getspecific(pthread_key_t key);

可以用来获取调用线程的私有数据区。返回值是一个指针,指向这一块缓冲区。如果没有进行关联,返回值为 NULL,所以如果是初次调用该函数,则必须为该线程分配私有数据缓冲区。

6.5.4 pthread_key_delete()

#include 
int pthread_key_delete(pthread_key_t key);

删除特有数据键,但是不会检查当前是否有线程正在使用该键所关联的线程私有数据缓冲区,所以不会触发析构函数,也就不会释放资源。所以,调用该函数前,要保证所有线程已经释放了私有数据区(显示调用析构函数或线程终止),key 指定的特有数据键将不再使用。

6.6 线程局部存储

线程局部存储在定义全局或静态变量时,使用 __thread 修饰变量,此时,每个线程都会拥有一份对该变量的拷贝,线程局部存储中的变量将一直存在,直到线程终止,届时会自动释放这一存储。比如```static __thread char c;``线程布局存储也是一个变量,可以对其取地址操作或赋初值。

你可能感兴趣的:(嵌入式Linux应用开发,嵌入式硬件)