线程同步和互斥介绍
相交进程之间的关系主要有两种,同步与互斥。
所谓互斥,
是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它 们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。
所谓同步,
是指散布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的 某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
互斥锁和条件变量
用于保护临界区,以保证任何时刻只有一个线程在执行其中的代码.
初始化
/** Static mutex variable **/ pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER; //pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。 /** Static or dynamic mutex variable **/ int pthread_mutex_init(pthread_mutex_t *restrict_mutex,const pthread_mutextattr_t*restrict attr); //使用互斥变量之前必须初始化,用于多线程中互斥锁的初始化
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //调用线程将阻塞直到互斥量被解锁 int pthread_mutex_trylock(pthread_mutex_t *mutex); //线程不被阻塞的方式加锁 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout); //在超时指定愿意等待的绝对时间后(与相对时间对比而言,指定在时间x之前可以阻塞等待,而不是说愿意阻塞Y秒), //pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEDOUT。 //这个超时时间是用timespec结构来表示的,它用秒和纳秒来描述时间。
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:
若成功,返回0
若失败,返回出错编号
- 条件变量的作用
用来自动阻塞一个线程,直到某特殊情况发生为止。
通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,
主要包括两个动作:
一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。
如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。
如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
初始化
/** Static mutex variable **/ pthread_cond_t mycond=PTHREAD_COND_INITIALIZER; //可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量. /** Static or dynamic mutex variable **/ int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr); //使用条件变量之前必须初始化 //在释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理.
attr:为NULL时,表示默认属性
返回值:
若成功,返回0
若失败,返回出错编号
摧毁
int pthread_cond_destroy(pthread_cond_t *cond); //使用互斥变量之前必须初始化
返回值:
若成功,返回0
若失败,返回出错编号
等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); //阻塞等待 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime); //超时等待,如果给定的时间内条件不能满足,那么返回错误的变量
说明:
等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数.
函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的.
这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.当pthread_cond_wait返回时, 互斥量再次被锁住.
abstime:绝对时间,指向timespec结构体
返回值:
若成功,返回0
若失败,返回出错编号
起初,`pthread_cond_timedwait`只支持`CLOCK_REALTIME`。但随着时间的推移,为了提高灵活性,很多系统开始支持使用`CLOCK_MONOTONIC`作为时间源。为了允许这种选择,`pthread_condattr_setclock`和`pthread_condattr_getclock`这两个函数被引入。
通过`pthread_condattr_setclock`,你可以为条件变量设置一个特定的时钟,使其不受系统时间变化的影响。
例如,在glibc 2.3.3及以后的版本中,支持使用`CLOCK_MONOTONIC`作为`pthread_cond_timedwait`的时间源。但在此之前,这个函数只支持`CLOCK_REALTIME`。
要确定你的系统上的实现是否支持使用`CLOCK_MONOTONIC`,你可以查看glibc的版本和/或参考其文档和源代码。此外,`pthread_condattr_setclock`函数的man页面通常会提供关于支持的时钟和其行为的详细信息。
解除阻塞
int pthread_cond_signal(pthread_cond_t *cond); //至少能唤醒一个等待该条件的线程 int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒等待该条件的所有线程
这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号.
必须注意, 一定要在改变条件状态以后再给线程发送信号.
返回值:
若成功,返回0
若失败,返回出错编号
互斥锁属性:
- 进程共享属性(pshared)
允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。
就像多个线程访问共享数据一样,多个线程访问共享数据也需要同步(互斥).
PTHREAD_PROCESS_PRIVATE(默认值):
变量/只能在与初始化变量的线程相同的进程中创建的线程进行操作.
- 健壮属性(robust)
健壮属性与多个进程间共享的互斥量有关,这意味着,当持有互斥量的进程终止时,需要解决互斥量状态恢复的问题。
在这种情况发生时,互斥量处于锁定状态,恢复起来很苦难。其他阻塞在这个锁的进程将会一直阻塞下去.
PTHREAD_MUTEX_STALLED(默认值):
如果互斥体的所有者在持有互斥锁时终止,则不采取特殊操作。这可能导致死锁,因为没有其他线程可以解锁互斥体.
- 类型属性(type)
类型互斥量属性控制着互斥量的锁定特性
PTHREAD_MUTEX_DEFAULT (默认值):
提供默认的特性和行为 .
- 协议属性(protocal)
线程调度和互斥锁的关系
PTHREAD_PRIO_NONE(默认值):
线程拥有PTHREAD_PRIO_NONE协议属性的互斥体时,其优先级和调度不受其互斥量所有权的影响.
条件变量属性:
- 进程共享属性(pshared)
条件变量与互斥量的共享属性的用法一样
- 时钟属性(clock)
时钟属性决定pthread_cond_timedwait函数的时钟参数使用何种时钟
CLOCK_REALTIM(默认值)
以实时系统时间为时钟
属性的初始化和销毁
/** Mutex attributes **/ int pthread_mutexattr_init(pthread_mutexattr_t *attr); //初始化mutex属性对象attr,并使用属性的默认值填充它。 int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); //销毁一个mutex属性对象,该对象在重新初始化之前不能被重用。 /** Condition variable attributes **/ int pthread_condattr_init(pthread_condattr_t *attr); //初始化已毁坏的attr属性对象;否则引用对象的结果被破坏后是未定义的。 //将初始化条件变量属性对象attr与实现定义的所有属性的默认值。 int pthread_condattr_destroy(pthread_condattr_t *attr); //破坏一个条件变量attributes对象;该对象实际上未初始化。 //实现可能导致pthread_condattr_destroy()将attr引用的对象设置为无效值。
在条件变量属性对象用于初始化一个或多个条件变量之后,影响属性对象(包括破坏)的任何函数都不会影响任何先前初始化的条件变量。
返回值:
若成功,返回0
若失败,返回出错编号
互斥锁和条件变量共享属性的获取和设置
/** Mutex attributes **/ int pthread_mutexattr_getpshared(pthread_mutexattr_t *restrict mutext,int *pshared); // 获取进程共享属性 int pthread_mutextattr_setpshared(pthread_mutexattr_t *restrict mutext, int pshared) // 修改进程共享属性 /** Condition variable attributes **/ int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,int *restrict pshared); //从attr引用的属性对象获取进程共享属性的值。 //将attr的进程共享属性的值存储到由pshared参数引用的对象中。 int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared); //在attr引用的初始化属性对象中设置进程共享属性。
pshared参数说明:
进程共享属性设置为PTHREAD_PROCESS_SHARED,则以允许变量由任何可访问变量分配的内存的线程操作,即使变量分配在由多个进程共享的内存中。
如果进程共享属性为PTHREAD_PROCESS_PRIVATE,则变量/只能在与初始化变量的线程相同的进程中创建的线程进行操作;
如果不同进程的线程尝试对这样的变量进行操作,则行为是未定义的。 属性的默认值为PTHREAD_PROCESS_PRIVATE。
返回值:
若成功,返回0
若失败,返回出错编号
获取/设置 互斥量属性健壮值
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust); int pthread_mutexattr_getrobust(pthread_mutexattr_t *attr, int *robust); //将attr的robust属性的值存储到由robust参数引用的对象中。
被通知的线程然后可以尝试恢复由互斥体保护的状态,如果成功,可以通过调用pthread_mutex_consistent()将状态标记为一致。
在继续成功调用pthread_mutex_unlock之后,互斥锁将被释放,并可被其他线程正常使用。
如果未经pthread_mutex_consistent()调用互斥锁,则它将处于永久不可用状态,并且锁定互斥体的所有尝试都将失败,并显示ENOTRECOVERABLE错误。
这种互斥体唯一允许的操作是pthread_mutex_destroy。
robust:
PTHREAD_MUTEX_STALLED(默认值)
如果互斥体的所有者在持有互斥锁时终止,则不采取特殊操作。这可能导致死锁,因为没有其他线程可以解锁互斥体。
PTHREAD_MUTEX_ROBUST
如果强制互斥体的拥有线程在保持互斥锁时终止,或者包含稳健互斥体的拥有线程的进程正常或异常终止,
或者如果包含互斥体所有者的进程会取消映射包含互斥体的内存或执行exec(2)函数之一,获取互斥体的下一个线程将从锁定函数返回值EOWNERDEAD通知。
返回值:
若成功,返回0
若失败,返回出错编号
恢复锁的一致性
int pthread_mutex_consistent(pthread_mutex_t *mutex); //如果mutex是不一致状态下的稳健互斥体,此函数可用于将由互斥体引用的互斥体保护的状态标记为再次一致。
如果强制互斥的所有者在持有互斥体时终止,或者包含互斥体所有者的进程解除映射包含互斥体的内存或执行其中一个exec 函数,则互斥量将变得不一致,
并且获取的下一个线程通过返回值EOWNERDEAD将互斥锁通知状态。在这种情况下,互斥体在状态被标记为一致之前,不会再正常使用。
pthread_mutex_consistent()函数只负责通知系统保护互斥体的状态已被恢复,并且可以恢复使用互斥体的正常操作。应用程序的责任是恢复状态,以便可以重复使用。
如果应用程序无法执行恢复,则可以通过调用pthread_mutex_unlock 通知系统该事件无法通过pthread_mutex_consistent()进行调用,
在这种情况下,尝试锁定互斥体的后续线程将会无法获取锁并返回ENOTRECOVERABLE。
如果在调用pthread_mutex_consistent()或pthread_mutex_unlock()之前,获取了返回值EOWNERDEAD的互斥锁的线程终止,
那么获取互斥锁的下一个线程将通过返回值EOWNERDEAD通知互斥体的状态。
返回值:
若成功,返回0
若失败,返回出错编号
获取/设定互斥锁类型的属性
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type); // 检索attr中mutex kind属性的当前值,并将其存储在按类型指向的位置。 int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); // 将attr中的mutex类型属性设置为由type指定的值。
参数:
type:类型都有:
PTHREAD_MUTEX_NORMAL
此互斥锁类型不做任何特殊的错误检查或死锁检测
PTHREAD_MUTEX_ERRORCHECK
此互斥量类型提供错误检查
PTHREAD_MUTEX_DEFAULT (缺省值)
此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。
PTHREAD_MUTEX_RECURSIVE
此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。
所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量将依然处于加锁状态,对它再次解锁以前不能释放该锁 .
返回值:
若成功,返回0
若失败,返回出错编号
获取/设置互斥锁的协议属性
int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *restrict attr, int *restrict protocol); // int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr,int protocol);
参数:
protocal:
PTHREAD_PRIO_NONE
线程拥有PTHREAD_PRIO_NONE协议属性的互斥体时,其优先级和调度不受其互斥量所有权的影响。
PTHREAD_PRIO_INHER IT
当拥有一个或多个具有PTHREAD_PRIO_INHERIT协议属性的互斥体,当一个线程阻塞较高优先级的线程时,它将以优先级较高的优先级或最高优先级线程的优先级执行,等待该线程拥有的任何互斥体并初始化用这个协议。
PTHREAD_PRIO_PROTECT
当线程拥有一个或多个使用PTHREAD_PRIO_PROTECT协议初始化的互斥体时,它将以其优先级较高或者由该线程拥有的所有互斥体的优先级高优先级最高执行,并使用此属性初始化,而不管其他线程是否为阻塞任何这些互斥体或不。
说明:
虽然线程正在保存已经使用PTHREAD_PRIO_INHERIT或PTHREAD_PRIO_PROTECT协议属性初始化的互斥体,
但是如果它的原始优先级被更改,则它不会被优先地移动到调度队列的尾部,例如通过 调用sched_setparam()。
同样地,当一个线程解锁已经用PTHREAD_PRIO_INHERIT或PTHREAD_PRIO_PROTECT协议属性初始化的互斥体时,
如果它的原始优先级被改变,它将不会被优先地移动到调度队列的尾部。
如果线程同时拥有使用不同协议初始化的多个互斥体,则它将以每个这些协议获得的优先级最高执行。
获取并设置时钟选择条件变量属性
int pthread_condattr_getclock(const pthread_condattr_t *restrict attr,clockid_t *restrict clock_id); //将从attr引用的属性对象中获取时钟属性的值。 //将attr的clock属性的值存储在由clock_id参数引用的对象中。 int pthread_condattr_setclock(pthread_condattr_t *attr,clockid_t clock_id); //应该在由attr引用的初始化属性对象中设置clock属性。 如果使用引用CPU时钟的clock_id参数调用pthread_condattr_setclock(),则调用失败。
时钟属性是用于测量pthread_cond_timedwait()的超时服务的时钟的时钟ID。 clock属性的默认值是指系统时钟。
restrict 有以下类型:
CLOCK_REALTIME // 实时系统时间
CLOCK_MONONIC // 不带负跳数的实时系统时间
CLOCK_PROCESS_CPUTIME_ID // 调用进程的CPU时间
CLOCK_THREAD_CPUTIME_ID // 调用线程的CPU时间返回值:
若成功,返回0
若失败,返回出错编号
pthread_cond_signal函数的使用(避免上锁冲突)
pthread_cond_signal 只发信号,内部不会解锁,
cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗.
在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列.
pthread_cond_wait 先解锁,等待,有信号来,上锁,执行while检查防止另外的线程更改条件
- 使用方式一:
可能会出现低级别的线程(正在等待mutex锁)抢占高级别的线程(cond_wait的线程)。
/****************** 代码段 ******************/ pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t mycond = PTHREAD_COND_INITIALIZER; pthread_mutex_lock(&mymutex); pthread_mutex_unlock(&mymutex); pthread_cond_singal(&mycond);
- 使用方式二:
如果需要可预见的调度行为,那么调用pthread_cond_signal 的线程必须锁住该互斥锁.
/****************** 代码段 ******************/ pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t mycond = PTHREAD_COND_INITIALIZER; pthread_mutex_lock(&mymutex); pthread_cond_singal(&mycond); pthread_mutex_unlock(&mymutex);
虚假唤醒
虚假唤醒是指一个线程在没有收到条件变量通知的情况下仍然从等待状态唤醒。虚假唤醒可能发生在任何平台,包括 ARM 系统。虽然虚假唤醒在实际中相对罕见,但仍然需要考虑它们以确保代码的正确性。
虚假唤醒的具体原因可能因系统和库的实现而异。以下是一些可能导致虚假唤醒的原因:
- 操作系统实现:操作系统在实现线程调度时,可能会因为内部调度策略、优化或者错误而导致虚假唤醒。在某些情况下,操作系统可能允许条件变量在没有实际通知的情况下唤醒线程,以确保系统的活跃度和响应能力。
- 信号处理:如果在等待条件变量时,线程收到一个信号(例如,由于操作系统中断或其他线程发送的信号),线程可能会因为处理信号而唤醒。在这种情况下,唤醒并不是由于条件变量的通知,而是由于信号处理导致的。
- 库实现:虚假唤醒有时可能是库实现的副作用。例如,pthread_cond_wait 的实现可能会因为内部实现细节、优化或错误而导致虚假唤醒。
为了应对虚假唤醒,我们需要在等待条件变量时使用一个变量来判断,是否是真的被条件变量唤醒.
通常使用这种手段
pthread_mutex_lock(&mutex); while (!condition_is_met) { pthread_cond_wait(&cond_var, &mutex); } pthread_mutex_unlock(&mutex);
在这个示例中,我们将 `pthread_cond_wait` 放在一个循环中,并检查 `condition_is_met` 是否为 `true`。只有当条件满足时(`condition_is_met == true`),线程才会继续执行。这样,即使发生虚假唤醒,线程仍然会检查条件是否满足,并在条件不满足时重新进入等待状态。这与 C++ `std::condition_variable` 使用谓词的方法类似。
所以,POSIX 线程库中的条件变量也可能会遇到虚假唤醒的问题,但是我们可以通过在循环中检查条件是否满足的方法来避免受到虚假唤醒的影响。
其他使用示例
#include <stdio.h> #include <time.h> #include <string.h> #include <pthread.h> #define _LOCK_DEBUG 1 //用于打印当前时间 static void printnowtime(void) { time_t timer; //time_t就是long int 类型 struct tm tmp_tm; char buf[512]={0}; timer = time(NULL); if(localtime_r(&timer ,&tmp_tm)!=NULL) strftime(buf, sizeof(buf), "%r", &tmp_tm); printf("The current time is %s\n", buf); } int main() { pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER; //初始化锁 struct timespec twait = {0, 0}; //包含秒和纳秒的结构体 int ret = 0; #if _LOCK_DEBUG pthread_mutex_lock(&mylock); printf("mutex is locked.\n"); #endif clock_gettime(CLOCK_REALTIME, &twait);//获取系统实时时间的值 twait.tv_sec += 7; //延迟7s printnowtime(); ret= pthread_mutex_timedlock(&mylock, &twait); printnowtime(); if(ret==0) { //After acquiring the lock... pthread_mutex_unlock(&mylock); } else { printf("Waiting for lock timeout\n"); #if _LOCK_DEBUG pthread_mutex_unlock(&mylock); #endif return -1; } pthread_mutex_destroy(&mylock); return 0; }
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/times.h> #include <pthread.h> static void printclock(clockid_t id) { printf("At this point, the condition variable attribute "); if (CLOCK_REALTIME == id) printf("clockid_t = CLOCK_REALTIME\n"); else if (CLOCK_MONOTONIC == id) printf("clockid_t = CLOCK_MONOTONIC\n"); else if (CLOCK_PROCESS_CPUTIME_ID == id) printf("clockid_t = CLOCK_PROCESS_CPUTIME_ID\n"); else if (CLOCK_THREAD_CPUTIME_ID == id) printf("clockid_t = CLOCK_THREAD_CPUTIME_ID\n"); else printf("clockid_t undefined\n"); return; } int main(void) { pthread_condattr_t attr; clockid_t clocktype; pthread_condattr_init(&attr); pthread_condattr_getclock(&attr, &clocktype); printf("default:\n"); printclock(clocktype); // CLOCK_REALTIME if (0 != pthread_condattr_setclock(&attr, CLOCK_REALTIME)) // 实时系统时间 perror("ERROR:set CLOCK_REALTIME"); if(pthread_condattr_getclock(&attr, &clocktype)==0) printclock(clocktype); // CLOCK_REALTIME if (0 != pthread_condattr_setclock(&attr, CLOCK_MONOTONIC)) // 不带负跳数的实时系统时间 perror("ERROR:set CLOCK_MONOTONIC"); // ERROR:set CLOCK_MONOTONIC if(pthread_condattr_getclock(&attr, &clocktype)==0) printclock(clocktype); // CLOCK_MONOTONIC if(0 != pthread_condattr_setclock(&attr, CLOCK_PROCESS_CPUTIME_ID)) // 调用进程的CPU时间 perror("ERROR:set CLOCK_PROCESS_CPUTIME_ID"); if(pthread_condattr_getclock(&attr, &clocktype)==0) printclock(clocktype); // CLOCK_MONOTONIC if(0 != pthread_condattr_setclock(&attr, CLOCK_THREAD_CPUTIME_ID)) // 调用线程的CPU时间 perror("ERROR:set CLOCK_THREAD_CPUTIME_ID"); if(pthread_condattr_getclock(&attr, &clocktype)==0) printclock(clocktype); // CLOCK_MONOTONIC return 0; }