为什么要线程同步
线程的主要优势在于资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题,从而引申出线程同步概念。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
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++)
{
l_count = g_count;
l_count++;
g_count = l_count;
}
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]);//该函数是将字符串转换为整型数。
/* 创建 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);
exit(0);
}
从这里可以得到,当一个数据量不大的时候是不会出现数据不一致的情况,因为在第二个线程还没开始访问资源时,该线程就已经执行完了,对于数据大那么采用并发模式的话,就会出现改线程还没执行完,但该数据已被更改为不是最终值,而下一个线程访问的数据是没有更改的值,这也就出现了数据不一致的问题
线程 A 读取变量的值,然后再给这个变量赋予一个新的值,但写操作需要 2 个时钟周期(这里只是假设);当线程 B 在这两个写周期中间读取了这个变量,它就会得到不一致的值, 这就出现了数据不一致的问题。
- 所以采用线程同步技术来实现同一时间只允许一个线程访问该变量,防止出现并发访问的情况、消除数据不一致的问题。
互斥锁
举一个非常简单容易理解的例子,就拿卫生间(共享资源)来说,当来了一个人(线程)看到卫生间没人,然后它进去了、并且从里边把门锁住(互斥锁上锁)了;
此时又来了两个人(线程),它们也想进卫生间方便,发生此时门打不开(互斥锁上锁失败),因为里边有人,所以此时它们只能等待(陷入阻塞);
当里边的人方便完了之后(访问共享资源完成),把锁(互斥锁解锁)打开从里边出来,此时外边有两个人在等,当然它们都迫不及待想要进去(尝试对互斥锁进行上锁),自然两个人只能进去一个,进去的人再次把门锁住,另外一个人只能继续等待它出来
初始化,上锁,解锁,销毁
/*
*@brief :互斥锁初始化,上锁,解锁,销毁
*@param[mutex] :一个 pthread_mutex_t 类型指针, 指向需要进行初始化操作的互斥锁对象;
*@param[attr] :一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性(在 12.2.6 小计中介绍),若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为默认值,
在这种情况下其实就等价于 PTHREAD_MUTEX_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。
*@return :成功返回 0;失败将返回一个非 0 的错误码。
*@others :上锁:
调用 pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;
如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
解锁:
调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:
- 对处于未锁定状态的互斥锁进行解锁操作;
- 解锁由其它线程锁定的互斥锁。
如果有多个线程处于阻塞状态等待互斥锁被解锁 ,当互斥锁被当前锁定它的线程调用pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会如愿以偿!
销毁:
- 不能销毁还没有解锁的互斥锁,否则将会出现错误;
- 没有初始化的互斥锁也不能销毁。
被 pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用。
*/
/*
互斥锁初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
以非阻塞方式上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
*/
/* 互斥锁初始化
1.
# define PTHREAD_MUTEX_INITIALIZER { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
所以由此可知,使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁的操作如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2.
使用 pthread_mutex_init()函数对互斥锁进行初始化示例:
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_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;
调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex_trylock()将会锁住互斥锁并立马返回,
如果互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
static pthread_mutex_t mutex;
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_mutex_lock(&mutex); //互斥锁上锁
// while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁a
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex); //互斥锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
if (argc < 2)
{
loops = 10000000;
}
else
{
loops = atoi(argv[1]);
}
//初始化互斥锁
ret = pthread_mutex_init(&mutex, NULL);
if (ret)
{
fprintf(stderr, "pthread_mutex_init error:%s\n", strerror(ret));
exit(-1);
}
//创建1号线程
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret)
{
fprintf(stderr, "pthread_create 1 error:%s\n", strerror(ret));
exit(-1);
}
//创建2号线程
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret)
{
fprintf(stderr, "pthread_create 2 error:%s\n", strerror(ret));
exit(-1);
}
//回收1号线程
ret = pthread_join(tid1, NULL);
if (ret)
{
fprintf(stderr, "pthread_join 1 error:%s\n", strerror(ret));
exit(-1);
}
//回收2号线程
ret = pthread_join(tid2, NULL);
if (ret)
{
fprintf(stderr, "pthread_join 2 error:%s\n", strerror(ret));
exit(-1);
}
printf("g_count=%d\n", g_count);
//销毁互斥锁
pthread_mutex_destroy(&mutex);
exit(0);
}
死锁
死锁产生条件:
- 试图对同一个互斥锁加锁两次,情况就是该线程会陷入死锁状态,一直被阻塞永远出不来;
- // 线程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 线程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);
避免死锁:
- 最简单的方式就是定义互斥锁的层级关系,譬如在上述场景中,如果两个线程总是先锁定 mutex1 在锁定mutex2,死锁就不会出现。
- 复杂情况譬如使用 pthread_mutex_trylock()以不阻塞的方式尝试对互斥锁进行加锁,在这种方案中,线程先使用函数pthread_mutex_lock()锁定第一个互斥锁,然后使用 pthread_mutex_trylock()来锁定其余的互斥锁。
如果任一pthread_mutex_trylock()调用失败(返回 EBUSY),那么该线程释放所有互斥锁,可以经过一段时间之后从头再试。
与第一种按照层级关系来避免死锁的方法变比,这种方法效率要低一些,因为可能需要经历多次循环。
互斥锁的属性
互斥锁的类型属性控制着互斥锁的锁定特性, 一共有 4 中类型:
- 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 类型相仿。
/*
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
*/
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
/* 初始化互斥锁属性对象 */
pthread_mutexattr_init(&attr);
/* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, &attr);
......
/* 使用完之后 */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
条件变量
条件变量是线程可用的另一种同步机制。
条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。
使用条件变量主要包括两个动作:
- 一个线程等待某个条件满足而被阻塞;
- 另一个线程中,条件满足时发出“信号”。
/*
*@brief :条件变量初始化,销毁
*@param[cond] :一个 pthread_cond_t 类型指针, 指向需要进行初始化操作的互斥锁对象;
*@param[mutex] :一个 pthread_mutex_t 类型指针, 指向需要进行初始化操作的互斥锁对象;
*@param[attr] :可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,与使用 PTHREAD_COND_INITIALIZER 宏相同。
*@return :成功返回 0;失败将返回一个非 0 的错误码。
*@others :对于初始化与销毁操作,有以下问题需要注意:
- 在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或者函数 pthread_cond_init()都行;
- 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
- 对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
- 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
- 经 pthread_cond_destroy()销毁的条件变量, 可以再次调用 pthread_cond_init()对其进行重新初始化。
*/
/*
条件变量初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
条件变量销毁
int pthread_cond_destroy(pthread_cond_t *cond);
可向指定的条件变量发送信号
二者对阻塞于 pthread_cond_wait()的多个线程对应的处理方式不同,
pthread_cond_signal()函数至少能唤醒一个线程,
而 pthread_cond_broadcast()函数则能唤醒所有线程。
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
将线程设置为等待状态(阻塞)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
*/
#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); //上锁
//如果这里没有没有这2句,那cpu将一直查询,这样浪费cpu性能,而使用条件变量的话,这里会阻塞
//必须使用 while 循环,而不是 if 语句,这是一种通用的设计原则:当线程从 pthread_cond_wait()返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。
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);
}
pthread_cond_wait()返回后,并不能确定判断条件是真还是假,其理由如下:
- 当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁, 率先醒来获取到互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。如果有两个或更多个消费者线程, 当其中一个消费者线程从 pthread_cond_wait()返回后,它会将全局共享变量 g_avail 的值变成 0, 导致判断条件的状态由真变成假。
- 可能会发出虚假的通知。
自旋锁
自旋锁与互斥锁的区别
- 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
- 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠) ,直到获取到锁时被唤醒;而获取不到自旋锁会在原地一直处于运行状态“自旋”,直到获取到锁;
休眠与唤醒开销是很大的, 所以互斥锁的开销要远高于自旋锁、 自旋锁的效率远高于互斥锁;
但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。 - 使用场景的区别: 自旋锁在用户态应用程序中使用的比较少, 通常在内核代码中使用比较多;
因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占) , 一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!
/*
*@brief :自旋锁的初始化,加锁与解锁
*@param[lock] : lock 指向了需要进行初始化或销毁的自旋锁对象
*@param[pshared] :PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
PTHREAD_PROCESS_PRIVATE: 私有自旋锁。只有本进程内的线程才能够使用该自旋锁。
*@param[attr] :可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,与使用 PTHREAD_COND_INITIALIZER 宏相同。
*@return :成功返回 0;失败将返回一个非 0 的错误码。
*@others :如果自旋锁处于未锁定状态,调用 pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁。
*/
/*
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *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);
}
替换为自旋锁之后,程序运行所耗费的时间明显变短了,说明自旋锁确实比互斥锁效率要高
读写锁
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读 写锁。因此可知,读写锁比互斥锁具有更高的并行性!
/*
*@brief :读写锁的初始化,加锁与解锁
*@param[rwlock] :rwlock 指向需要进行初始化或销毁的读写锁对象
*@param[attr] :可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,PTHREAD_RWLOCK_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。
*@return :成功返回 0;失败将返回一个非 0 的错误码。
*@others :PTHREAD_PROCESS_SHARED: 共享读写锁。该读写锁可以在多个进程中的线程之间共享;
PTHREAD_PROCESS_PRIVATE: 私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值
*/
/*
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
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);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
*/
#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 < 5; 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 < 5; 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 ret;
int i;
/* 初始化读写锁*/
pthread_rwlock_init(&rwlock, NULL);
/* 创建 5 个读 g_count 变量的线程 */
for (i = 0; i < 5; i++)
{
pthread_create(&tid[i], NULL, read_thread, &nums[i]);
}
/* 创建 5 个写 g_count 变量的线程 */
for (i = 0; i < 5; i++)
{
pthread_create(&tid[i + 5], NULL, write_thread, &nums[i]);
}
/* 等待线程结束 */
for (i = 0; i < 10; i++)
{
pthread_join(tid[i], NULL);
}
/* 销毁读写锁 */
pthread_rwlock_destroy(&rwlock);
exit(0);
}