0%

Linux线程(三)线程同步

线程同步

互斥锁

互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问 完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁

初始化

  • 互 斥锁使 用 pthread_mutex_t 数 据类型 表示, pthread_mutex_t 其 实是一个 结构体 类型, 而宏 PTHREAD_MUTEX_INITIALIZER 其实是一个对结构体赋值操作的封装
  • 使用宏定义初始化互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 使用函数初始化互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;
  • attr:参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用 于定义互斥锁的属性,若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为 默认值,在这种情况下其实就等价于 PTHREAD_MUTEX_INITIALIZER 这种方式初始化,而不同之处在于, 使用宏不进行错误检查。
  • 返回值:成功返回 0;失败将返回一个非 0 的错误码。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

//或者
pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);

互斥锁的属性

  • 如果不使用默认属性,在调用 pthread_mutex_init()函数时,参数 attr 必须要指向一个 pthread_mutexattr_t 对象,而不能使用 NULL。当定义 pthread_mutexattr_t 对象之后,需要使用 pthread_mutexattr_init()函数对该 对象进行初始化操作,当对象不再使用时,需要使用 pthread_mutexattr_destroy()将其销毁
#include <pthread.h>
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()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大 灵活性, Linux 上 , PTHREAD_MUTEX_DEFAULT 类 型 互 斥 锁 的 行 为 与 PTHREAD_MUTEX_NORMAL 类型相仿。

互斥锁加锁和解锁

  • 互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互 斥锁,而调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。
#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 如果互斥锁处于未锁定状态,则此次调用会上锁成 功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直 阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回

  • 调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:

    • 对处于未锁定状态的互斥锁进行解锁操作;
    • 解锁由其它线程锁定的互斥锁。
  • 如果线程加锁的时候不希望被阻塞,可以使用 pthread_mutex_trylock()函数

    • 如果 互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

销毁互斥锁

  • 调用 pthread_mutex_destroy()函数来销毁互斥锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 不能销毁还没有解锁的互斥锁,否则将会出现错误;
  • 没有初始化的互斥锁也不能销毁。

互斥锁死锁

有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过 一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用 一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞 状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程 拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。

条件变量

条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条 件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。条件变量包括

  • 一个线程等待某个条件满足而被阻塞
  • 另一个线程中,条件满足时发出“信号”

条件变量初始化

  • 使用宏定义初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • pthread_cond_init()函数原型如下所示
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 使用这些函数需要包含头文件,使用 pthread_cond_init()函数初始化条件变量,当不再 使用时,使用 pthread_cond_destroy()销毁条件变量。

  • 参数 cond 指向 pthread_cond_t 条件变量对象,对于 pthread_cond_init()函数,类似于互斥锁,在初始化 条件变量时设置条件变量的属性,参数 attr 指向一个 pthread_condattr_t 类型对象,pthread_condattr_t 数据类 型用于描述条件变量的属性。可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,与使 用 PTHREAD_COND_INITIALIZER 宏相同。

  • 注意事项

    • 在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或 者函数 pthread_cond_init()都行;
    • 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
    • 对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
    • 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
    • 经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。

通知和等待

  • 发送信号操作即是通知一个或多个处于等待状态 的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检 查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
  • 函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多 个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_signal()函数至少能唤醒一个线程,而 pthread_cond_broadcast()函数 则能唤醒所有线程。
  • 使用 pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,但函数 pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • 当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待 状态(阻塞)。

  • cond:指向需要等待的条件变量,目标条件变量;

  • mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;前面开头便给大家介绍 了,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥 锁的保护下进行的,也就是说条件本身是由互斥锁保护的

    • 在 pthread_cond_wait()函数内部会对参数 mutex 所指定的互斥锁进行操作,通常情况下,条件判断以及 pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。调用 pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表 上,然后将互斥锁解锁;当 pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁
  • 如果调用 pthread_cond_signal()和 pthread_cond_broadcast()向指定条件变量发送信号时,若无任何线程等待该条件变量, 这个信号也就会不了了之。

  • 当调用 pthread_cond_broadcast()同时唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁 失败又会陷入阻塞。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
for (;;)
{
pthread_mutex_lock(&mutex); //上锁
while (0 >= g_avail)
pthread_cond_wait(&cond, &mutex); //等待条件满足
while (0 < g_avail)
g_avail--; //消费
pthread_mutex_unlock(&mutex); //解锁
}
return (void *)0;
}
/* 主线程(生产者) */
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
/* 初始化互斥锁和条件变量 */
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
/* 创建新线程 */
ret = pthread_create(&tid, NULL, consumer_thread, NULL);
if (ret)
{
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
for (;;)
{
pthread_mutex_lock(&mutex); //上锁
g_avail++; //生产
pthread_mutex_unlock(&mutex); //解锁
pthread_cond_signal(&cond); //向条件变量发送信号
}
exit(0);
}

  • 上面的程序实现的功能是主线程对一个变量上互斥锁,+1,解锁互斥锁,然后发送信号唤醒子线程,线程对变量进行加锁,等待信号量,然后对变量-1,然后在解锁。

自旋锁

如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋 锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。 由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁 在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者 是否已经释放了锁,“自旋”一词因此得名。

  • 自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋), 所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。

  • 自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使 得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使 用自旋锁,效率高!

  • 区别

    • 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
    • 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自 旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自 旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低, 故自旋锁不适用于等待时间比较长的情况。
    • 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为 自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能 被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了 CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

初始化

  • 使用 pthread_spin_init()函数对其 进行初始化,当不再使用自旋锁时,调用 pthread_spin_destroy()函数将其销毁
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
  • 参数 lock 指向了需要进行初始化或销毁的自旋锁对象,参数 pshared 表示自旋锁的进程共享属性,可以 取值如下:
    • PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
    • PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁。

加锁和解锁

  • 可以使用 pthread_spin_lock()函数或 pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时 一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。不管以何种方式加锁, 自旋锁都可以使用 pthread_spin_unlock()函数对自旋锁进行解锁
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
  • 如果自旋锁处于未锁定状态,调用 pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋 锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_spinlock_t spin; //定义自旋锁
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++)
{
pthread_spin_lock(&spin); //自旋锁上锁

l_count = g_count;
l_count++;
g_count = l_count;
pthread_spin_unlock(&spin); //自旋锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为 1000 万次
else
loops = atoi(argv[1]);
/* 初始化自旋锁(私有) */
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
/* 创建 2 个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret)
{
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret)
{
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret)
{
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret)
{
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
printf("g_count = %d\n", g_count);
/* 销毁自旋锁 */
pthread_spin_destroy(&spin);
exit(0);
}

读写锁

  • 读写锁有如下两个规则:

    • 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读 模式加锁还是以写模式加锁)的线程都会被阻塞。
    • 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以 写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
  • 所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态 时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于 读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。

初始化

  • 使用宏定义初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
  • 其他方式可以使用 pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要调用 pthread_rwlock_destroy()函数将其销毁
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
  • 若将参数 attr 设置为 NULL,则表示将读写锁的属性设置为默认值,在 这种情况下其实就等价于 PTHREAD_RWLOCK_INITIALIZER 这种方式初始化,而不同之处在于,使用宏 不进行错误检查。

上锁和解锁

  • 以读模式对读写锁进行上锁,需要调用 pthread_rwlock_rdlock()函数;以写模式对读写锁进行上锁,需 要调用 pthread_rwlock_wrlock()函数。不管是以何种方式锁住读写锁,均可以调用 pthread_rwlock_unlock()函 数解锁
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 当读写锁处于写模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()或 pthread_rwlock_wrlock()函数 均会获取锁失败,从而陷入阻塞等待状态;当读写锁处于读模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用 pthread_rwlock_wrlock()函数则不能获取到锁,从 而陷入阻塞等待状态。

  • 如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock()来尝试加锁, 如果不可以获取锁时。这两个函数都会立马返回错误,错误码为 EBUSY。

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

使用例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock; //定义读写锁
static int g_count = 0;
static void *read_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++)
{
pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
printf("读线程<%d>, g_count=%d\n", number + 1, g_count);
pthread_rwlock_unlock(&rwlock); //解锁
sleep(1);
}
return (void *)0;
}
static void *write_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++)
{
pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
printf("写线程<%d>, g_count=%d\n", number + 1, g_count += 20);
pthread_rwlock_unlock(&rwlock); //解锁
sleep(1);
}
return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
pthread_t tid[10];
int j;
/* 对读写锁进行初始化 */
pthread_rwlock_init(&rwlock, NULL);
/* 创建 5 个读 g_count 变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, read_thread, &nums[j]);
/* 创建 5 个写 g_count 变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j + 5], NULL, write_thread, &nums[j]);
/* 等待线程结束 */
for (j = 0; j < 10; j++)
pthread_join(tid[j], NULL); //回收线程
/* 销毁自旋锁 */
pthread_rwlock_destroy(&rwlock);
exit(0);
}