接上一篇 Posix线程(1)
文章目录
- ①线程同步
- 线程为什么要同步?
- ②互斥锁
- 互斥量原语
- 参数释义
- 互斥量使用
- 死锁
- ③条件变量
- 条件变量原语
- 条件变量与互斥锁
- 注意事项
- 虚假唤醒与唤醒丢失
- ⑴虚假唤醒
- ⑵唤醒丢失
- 使用条件变量
- ③线程池
- ④Pthread API函数
①线程同步
线程为什么要同步?
做个小实验吧,两个线程计数。如果最后加起来是20万那就不用往下看了。
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
int count = 0;//声明全局变量,等下就看看它了
void *run(void *arg)
{
int i = 0;
for(i = 0;i < 100000; i++)
{
count++;
printf("Count:%d\n",count);
usleep(2);
}
return (void*)0;
}
int main(int argc,char **argv)
{
pthread_t tid1,tid2;
int err1,err2;
err1 = pthread_create(&tid1,NULL,run,NULL);
err2 = pthread_create(&tid2,NULL,run,NULL);
if(err1==0 && err2==0)//俩线程都成功创建出来
{
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
return 0;
}
好,为什么要线程同步,那就心照不宣了
算了,官方话还是要说一说的
1、共享资源,多个线程都可以对共享资源进行操作
2、线程操作共享资源的先后顺序不一定
3、处理器对存储器的操作一般不是原子操作
②互斥锁
什么叫互斥量,顾名思义就是咱这么多人,只能有一个使用这个资源,就像共享小单车,一次只能给一个人用,一个人下车锁车了,另一个人才能去扫码开锁。
互斥量原语
pthread_mutex_t mutex = PTHREAD_MUREX_INITALIZER //用于初始化互斥锁,后面简称锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化锁,和上面那个一个意思。
//初始化一个互斥锁(互斥量)–>初值可看做1
int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁
int pthread_mutex_unlok(pthread_mutex_t *mutex); //解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试上锁
参数释义
<这里只释义那个init>
参数1:传出参数,调用时应传&mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
参数2:互斥属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享).
静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex, NULL);
attr对象用于设置互斥量对象的属性,使用时必须声明为pthread_mutextattr_t类型,默认值可以是NULL。Pthreads标准定义了三种可选的互斥量属性:
协议(Protocol): 指定了协议用于阻止互斥量的优先级改变
优先级上限(Prioceiling):指定互斥量的优先级上限
进程共享(Process-shared):指定进程共享互斥量
注意所有实现都提供了这三个可选的互斥量属性。
Q:有多个线程等待同一个锁定的互斥量,当互斥量被解锁后,那个线程会第一个锁定互斥量?
A:除非线程使用了优先级调度机制,否则,线程会被系统调度器去分配,那个线程会第一个锁定互斥量是随机的。
互斥量使用
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int count = 0;
void *run(void *arg)
{
int i = 0;
for(i = 0;i < 100000; i++)
{
pthread_mutex_lock(&mutex);
count++;
printf("Count:%d\n",count);
usleep(2);
pthread_mutex_unlock(&mutex);
}
return (void*)0;
}
int main(int argc,char **argv)
{
pthread_t tid1,tid2;
int err1,err2;
err1 = pthread_create(&tid1,NULL,run,NULL);
err2 = pthread_create(&tid2,NULL,run,NULL);
if(err1==0 && err2==0)
{
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
return 0;
}
拿去执行,如果不是20万也可以不用往下看了。
死锁
为什么我要强调上锁和解锁一定要放在一起写,就是防止出现人为失误导致死锁
死锁嘛,解不开了。
要么是你忘了解开,别人也就没得用了
要么就是几个线程互相掐着关键数据导致谁也没办法完成任务,结果谁也没办法解锁。
这种情况下只有销毁掉代价最小的那个锁,让任务执行下去,不过后面要记得把那个被销毁的任务重新运作。
③条件变量
- 条件变量提供了另一种同步的方式。互斥量通过控制对数据的访问实现了同步,而条件变量允许根据实际的数据值来实现同步。
- 没有条件变量,程序员就必须使用线程去轮询(可能在临界区),查看条件是否满足。这样比较消耗资源,因为线程连续繁忙工作。条件变量是一种可以实现这种轮询的方式。
- 条件变量往往和互斥一起使用
使用条件变量的代表性顺序如下:
条件变量原语
//初始化条件变量:
//本人还是喜欢静态初始化,省事儿
pthread_cont_t cont = PTHREAD_COND_INITIALIZER;
//好,再看看动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//参数释义:cond:用于接收初始化成功管道条件变量
//attr:通常为NULL,且被忽略
//有初始化那肯定得有销毁
int pthread_cond_destroy(pthread_cond_t *cond);
//既然说条件变量是用来等待的,那就更要看看这等待的特殊之处了
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); //无条件等待
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t mytex,const struct timespec *abstime); //计时等待
//好,加入等待唤醒大军了,那得看看怎么去唤醒了
int pthread_cond_signal(pthread_cond_t *cptr); //唤醒一个等待该条件的线程。存在多个线程是按照其队列入队顺序唤醒其中一个
int pthread_cond_broadcast(pthread_cond_t * cptr); //广播,唤醒所哟与等待线程
条件变量与互斥锁
在服务器编程中常用的线程池,多个线程会操作同一个任务队列,一旦发现任务队列中有新的任务,子线程将取出任务;这里因为是多线程操作,必然会涉及到用互斥锁保护任务队列的情况(否则其中一个线程操作了任务队列,取出线程到一半时,线程切换又取出相同任务)。但是互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。设想,每个线程为了获取新的任务不断得进行这样的操作:锁定任务队列,检查任务队列是否有新的任务,取得新的任务(有新的任务)或不做任何操作(无新的任务),释放锁,这将是很消耗资源的。
而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。对应于线程池的场景,我们可以让线程处于等待状态,当主线程将新的任务放入工作队列时,发出通知(其中一个或多个),得到通知的线程重新获得锁,取得任务,执行相关操作。
注意事项
(1)必须在互斥锁的保护下唤醒,否则唤醒可能发生在锁定条件变量之前,照成死锁。
(2)唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定
(3)如果没有线程被阻塞在调度队列上,那么唤醒将没有作用。
(4)以前不懂事儿,就喜欢广播。由于pthread_cond_broadcast函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数。
虚假唤醒与唤醒丢失
⑴虚假唤醒
在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应成为”虚假唤醒”(spurious wakeup)
Linux帮助里面有
为什么不去修正,性价比不高嘛。
所以通常的标准解决办法是这样的:
⑵唤醒丢失
无论哪种等待方式,都必须和一个互斥量配合,以防止多个线程来打扰。
互斥锁必须是普通锁或适应锁,并且在进入pthread_cond_wait之前必须由本线程加锁。
在更新等待队列前,mutex必须保持锁定状态. 在线程进入挂起,进入等待前,解锁。(好绕啊,我已经尽力断句了)
在条件满足并离开pthread_cond_wait前,上锁。以恢复它进入cont_wait之前的状态。
为什么等待会被上锁?
以免出现唤醒丢失问题。
这里有个大神解释要不要看:https://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex
做事做全套,源码也给放这儿了:https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html
在放些咱能看懂的中文解释:将线程加入唤醒队列后方可解锁。保证了线程在陷入wait后至被加入唤醒队列这段时间内是原子的。
但这种原子性依赖一个前提条件:唤醒者在调用pthread_cond_broadcast或pthread_cond_signal唤醒等待者之前也必须对相同的mutex加锁。
满足上述条件后,如果一个等待事件A发生在唤醒事件B之前,那么A也同样在B之前获得了mutex,那A在被加入唤醒队列之前B都无法进入唤醒调用,因此保证了B一定能够唤醒A;试想,如果A、B之间没有mutex来同步,虽然B在A之后发生,但是可能B唤醒时A尚未被加入到唤醒队列,这便是所谓的唤醒丢失。
在线程未获得相应的互斥锁时调用pthread_cond_signal或pthread_cond_broadcast函数可能会引起唤醒丢失问题。
唤醒丢失往往会在下面的情况下发生:
一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;
另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;
没有线程正在处在阻塞等待的状态下。
使用条件变量
//例子演示了使用Pthreads条件变量的几个函数。主程序创建了三个线程,两个线程工作,根系“count”变量。第三个线程等待count变量值达到指定的值。
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 3
#define TCOUNT 10
#define COUNT_LIMIT 12
int count = 0;
int thread_ids[3] = {0,1,2};
pthread_mutex_t count_mutex;
pthread_cond_t count_threshold_cv;
void *inc_count(void *idp)
{
int j,i;
double result=0.0;
int *my_id = idp;
for (i=0; i<TCOUNT; i++) {
pthread_mutex_lock(&count_mutex);
count++;
if (count == COUNT_LIMIT) {
pthread_cond_signal(&count_threshold_cv);
printf("inc_count(): thread %d, count = %d Threshold reached./n",
*my_id, count);
}
printf("inc_count(): thread %d, count = %d, unlocking mutex/n",
*my_id, count);
pthread_mutex_unlock(&count_mutex);
for (j=0; j<1000; j++)
result = result + (double)random();
}
pthread_exit(NULL);
}
void *watch_count(void *idp)
{
int *my_id = idp;
printf("Starting watch_count(): thread %d/n", *my_id);
pthread_mutex_lock(&count_mutex);
if (count<COUNT_LIMIT) {
pthread_cond_wait(&count_threshold_cv, &count_mutex);
printf("watch_count(): thread %d Condition signal
received./n", *my_id);
}
pthread_mutex_unlock(&count_mutex);
pthread_exit(NULL);
}
int main (int argc, char *argv[])
{
int i, rc;
pthread_t threads[3];
pthread_attr_t attr;
pthread_mutex_init(&count_mutex, NULL);
pthread_cond_init (&count_threshold_cv, NULL);
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_create(&threads[0], &attr, inc_count, (void *)&thread_ids[0]);
pthread_create(&threads[1], &attr, inc_count, (void *)&thread_ids[1]);
pthread_create(&threads[2], &attr, watch_count, (void *)&thread_ids[2]);
for (i=0; i<NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf ("Main(): Waited on %d threads. Done./n", NUM_THREADS);
pthread_attr_destroy(&attr);
pthread_mutex_destroy(&count_mutex);
pthread_cond_destroy(&count_threshold_cv);
pthread_exit(NULL);
}
③线程池
线程池