目录:
10.1、线程为什么要同步
10.2、互斥量
10.3、死锁
10.4、读写锁
10.1 线程为什么要同步
做个小实验吧
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
int count = 0;//声明全局变量,等下就看看它了
void *run(void *arg)
{
int i = 0;
for(i = 0;i < 5000; i++)
{
count++;
}
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)//俩线程都成功创建出来
{
sleep(1000);
printf("count:%d\n",count); //这个换行呐,不能漏掉
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
return 0;
}
我临时写的,如果结果是10000那就不用往下看了。
好,为什么要线程同步,那就心照不宣了
算了,官方话还是要说一说的
1、共享资源,多个线程都可以对共享资源进行操作
2、线程操作共享资源的先后顺序不一定
3、处理器对存储器的操作一般不是原子操作
10.2互斥量
鉴于上面的实验,我们知道了线程需要同步
那怎么同步?互斥量就是一种办法。
什么叫互斥量,顾名思义就是咱这么多人,只能有一个使用这个资源,就像共享小单车,一次只能给一个人用,一个人下车锁车了,另一个人才能去扫码开锁。
10.2.1 临界区
临界区用来保证在同一时刻只有一个线程可以访问到资源,对于临界区进行操作的函数有两个:
EnterCriticalSection();
LeaveCriticalSection();
临界区最大的特色是其同步速度很快,但是其只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
10.2.2 互斥量 (注(1))
互斥量和临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,互斥量比临界区负责,并且互斥量是可以命名的,因此互斥量不仅仅可以用于同一应用程序不同线程中资源的同步,也可以用于不同应用程序的线程之间实现对资源的同步。
因此互斥量可以在整个系统中被任意进程的任意线程访问到,但它严格限定只有获取了互斥量的线程才能释放该互斥量。
10.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,用都麻烦
pthread_mutex_t 类型,其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待。
<这里只释义那个init>
参数1:传出参数,调用时应传&mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
参数2:互斥属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享).
静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex, NULL);
好,接下来我们给开头那段代码上锁
看看在哪里上锁啊
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
pthread_mutex_t mutex = PTHREAD_MUREX_INITALIZER; //这样多好
int count = 0;//声明全局变量,等下就看看它了
void *run(void *arg)
{
int i = 0;
pthread_mutex_lock(&mutex); //好,搁这儿上锁
for(i = 0;i < 5000; i++)
{
count++;
}
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)//俩线程都成功创建出来
{
sleep(1000);
printf("count:%d\n",count); //这个换行呐,不能漏掉
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
return 0;
}
好,再拿去执行以下,如果不是10000也不用往下看了
10.3死锁
为什么我要强调那俩一定要放在一起写,就是防止出现人为失误导致死锁
死锁嘛,解不开了。
要么是你忘了解开,别人也就没得用了
要么就是几个线程互相掐着关键数据导致谁也没办法完成任务,结果谁也没办法解锁。
这种情况下只有销毁掉代价最小的那个锁,让任务执行下去,不过后面要记得把那个被销毁的任务重新运作。
10.4 读写锁
读时共享,写时复制
读写锁是效率更高的互斥量(在大多数条件下)。
读写锁工作机理:
读写锁分为三种状态
读锁、写锁、不加锁
读写锁特性(12字):写锁优先级高,写独占,读共享
读锁:当某文件添加读锁时,如果要对此文件再申请读操作,可以。如果要对文件执行写操作,阻塞。如果同时要对文件添加读写操作,也阻塞,等这个读操作结束之后先执行写操作,不能让写操作阻塞太久。
写锁:阻塞外部请求
读写锁非常适合对数据读的次数比写的次数多。
读写锁原语:
初始化读写锁
// pthread_rwlock_t类型 , 用于定义一个读写锁变量
//pthread_rwlock_init 初始化一把读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //这个方法好
int pthread_rwolck_init(pthread_rwlock_t*restrict rwlock,const pthread_rwlockattr_t*restrict attr); //还是用上面那个吧
参数释义:restrict关键字:只用于限制指针,所有修改该指针指向内存中内容的操作,只能通过本指针来完成,不能通过除本指针之外的其它变量或指针修改。
参数2:attr表读写属性,通常使用NULL,表示默认属性
销毁读写锁
//销毁一把读写锁:pthread_rwlock_destroy
int pthread_rwlock_destroy(pthread_rwlock_t * rwlock);
尝试加锁
//以读方式请求加锁:pthread_rwlock_rdlock
int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock);
//以写方式请求加锁:pthread_rwlock_wrlock
int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock);
解锁
int pthread_rwlock_unlock(pthread_rwlock_t * rwlock);
尝试加锁
//非阻塞请求读锁:pthread_rwlock_tryrdlock
int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
//非阻塞请求写锁:pthread_rwlock_trywrlock
int pthread_rwlock_trywrock(pthread_rwlock_t * rwlock);
栗子就不加了,把上面那个代码里面的锁可以改成写锁。
一个线程可以同时拥有n个读锁,不过解锁也需要使用n个解锁。