******* 线程的几种锁及基本操作

您所在的位置:网站首页 互斥锁的原理及作用是什么 ******* 线程的几种锁及基本操作

******* 线程的几种锁及基本操作

2023-05-03 01:46| 来源: 网络整理| 查看: 265

我们先来看一段代码:

#include #include #include //创建两个线程,分别对两个全变量进行++操作,判断两个变量是否相等,不相等打印 int a = 0; int b = 0; // 未初始化 和0初始化的成员放在bbs pthread_mutex_t mutex; void* route() { while(1) //初衷不会打印 { a++; b++; if(a != b) { printf("a =%d, b = %d\n", a, b); a = 0; b = 0; } } } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, route, NULL); pthread_create(&tid2, NULL, route, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }

段代码的运行结果优点出乎我们的预料:

******* 线程的几种锁及基本操作_加锁

我们预计的结构应该是不会打印的,而这里去打印出了我们意想不到的结果。连相等的数据都打印了出来,为什么会出现这样的情况呢?

解释:两个线程互相抢占CPU资源,一个线程对全局变量做了++操作之后,还没来得及比较输出操作,另一个线程抢占CPU,进行比较打印输出。为了避免这样的情况,就需要用到下面介绍的互斥锁。

互斥量(锁):用于保护关键的代码段,以确保其独占式的访问。

1.定义互斥量: pthread_mutex_t mutex; 2.初始化互斥量: pthread_mutex_init(&mutex, NULL); //第二个参数不研究置NULL;          //初始化为 1 (仅做记忆) 3.上锁      pthread_mutex_lock(&mutex);   1->0;    0   等待 4.解锁           pthread_mutex_unlock(&mutex);   置1 返回

5.销毁           pthread_mutex_destroy(&mutex);  

返回值:若成功返回0,若出错返回错误编号。

说明: 互斥锁,在多个线程对共享资源进行访问时,在访问共享资源前对互斥量进行加锁,在访问完再进行解锁,在互斥量加锁后其他的线程将阻塞,直到当前的线程访问完毕并释放锁。如果释放互斥锁时有多个线程阻塞,所有阻塞线程都会变成可运行状态,第一个变成可运行状态的线程可以对互斥量加锁。这样就保证了每次只有一个线程访问共享资源。

至此,我们好像能通过互斥锁解决上面的问题:

#include #include #include int a = 0; int b = 0; // 未初始化 和0初始化的成员放在bbs pthread_mutex_t mutex; void* route() { while(1) //初衷不会打印 { pthread_mutex_lock(&mutex); a++; b++; if(a != b) { printf("a =%d, b = %d\n", a, b); a = 0; b = 0; } pthread_mutex_unlock(&mutex); } } int main() { pthread_t tid1, tid2; pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, route, NULL); pthread_create(&tid2, NULL, route, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); return 0; }

现有如下场景:线程1和线程2,线程1执行函数A,线程2执行函数B,现只使用一把锁,分别对A,B函数的执行过程加锁和解锁。

#include #include #include #include //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞 pthread_mutex_t mutex; void* odd(void* arg) { int i = 1; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } void* even(void* arg) { int i = 0; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1, t2; pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, even, NULL); pthread_create(&t2, NULL, odd, NULL); //pthread_create(&t3, NULL, even, NULL); sleep(3); pthread_cancel(t2); //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); return 0; }

一种极限情况是:线程2的取消发生在线程2的解锁之前,那么就会导致因为锁没解开,而线程1无法继续运行。

解决这样的问题我们可以用到下面的宏函数:

宏: //注册线程回调函数,可用来防止线程取消后没有解锁的问题 void pthread_cleanup_push(void (*routine)(void *), //回调函数 void *arg); //回调函数的参数 //回调函数执行时机 1.pthread_exit 2.pthread_cancel 3.cleanaup_pop参数不为0,当执行到cleaup_pop时,调用回调函数void pthread_cleanup_pop(int execute);

 

#include #include #include #include //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞 pthread_mutex_t mutex; void callback(void* arg) //在cancel中进行解锁 { printf("callback\n"); sleep(1); pthread_mutex_unlock(&mutex); } void* odd(void* arg) { int i = 1; for(; ; i+=2) { pthread_cleanup_push(callback, NULL);//因为调用了cancel函数,从而触发了回调函数。 pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); pthread_cleanup_pop(0); } } void* even(void* arg) { int i = 0; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1, t2; pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, even, NULL); pthread_create(&t2, NULL, odd, NULL); //pthread_create(&t3, NULL, even, NULL); sleep(3); pthread_cancel(t2); //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前 //pthread_mutex_unlock(&mutex); 有问题,如果执行even的程序有两个,而一个取消线程的函数执行时正好t3函数阻塞,就会导致t3和t1同时在执行even pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); return 0; }

注意:

1.不要销毁一个已经加锁的互斥量,销毁的互斥量确保后面不会再有线程使用。

2.上锁和解锁函数要成对的使用

3.选择合适的锁的粒度(数量)。如果粒度太粗,就会出现很多线程阻塞等待相同锁,源自并发性的改善微乎其微。如果锁的粒度太细,那么太多的锁的开销会使系统的性能受到影响,而且代码会变得相当复杂。

4.加锁要加最小(范围)锁,减少系统负担

使用互斥锁一定要注意避免死锁:《Linux高性能服务器编程》  14.5.3 介绍了两个互斥量因请求顺序产生死锁问题

          如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,使用互斥量时,还有其他更不明显的方式也能产生死锁。例如,程序中使用多个互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,这时就会发生死锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。

          可以通过小心地控制互斥量加锁的顺序来避免死锁的发生。例如,假设需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前锁住互斥量A,那么使用这两个互斥量不会产生死锁(当然在其他资源上仍可能出现死锁);类似地,如果所有的线程总是在锁住互斥量A之前锁住互斥量B,那么也不会发生死锁。只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。

         为了应对死锁,在实际的编程中除除了加上同步互斥量之外,还可以通过以下三原则来避免写出死锁的代码:

1>短:写的代码尽量简洁

2>平:代码中没有复杂的函数调用

3>快:代码的执行速度尽可能快

自旋锁:  应用在实时性要求较高的场合(缺点:CPU浪费较大)

pthread_mutex_spin;

pthread_spin_lock() ; //得不到时,进入忙等待,不断向CPU进行询问请求

pthread_spin_unlock(); 

 pthread_spin_destroy(pthread_spinlock_t *lock);

pthread_spin_init(pthread_spinlock_t *lock, int pshared);

读写锁(共享-独占锁):应用场景---大量的读操作  较少的写操作

注意:读读共享, 读写互斥,写优先级高(同时到达)

1. pthread_rwlock_t rwlock;//定义

2.int pthread_rwlock_init()//初始化

3.pthread_rwlock_rdlock()//pthread_rwlock_wrlock//读锁/写锁

4.pthread_rwlock_unlock()  // 解锁

5.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁锁

返回值:成功返回0,出错返回错误编号

说明:不管什么时候要增加一个作业到队列中或者从队列中删除作业,都用写锁,

不管何时搜索队列,首先获取读模式下的锁,允许所有的工作线程并发的搜索队列。在这样的情况下只有线程

搜索队列的频率远远高于增加或删除作业时,使用读写锁才可能改善性能。

#include #include #include #include //创建8个线程,3个写线程,5个读线程 pthread_rwlock_t rwlock; int counter = 0; void* readfunc(void* arg) { int id = *(int*)arg; free(arg); while(1) { pthread_rwlock_rdlock(&rwlock); printf("read thread %d : %d\n", id, counter); pthread_rwlock_unlock(&rwlock); usleep(100000); } } void* writefunc(void* arg) { int id = *(int*)arg; free(arg); while(1) { int t = counter; pthread_rwlock_wrlock(&rwlock); printf("write thread %d : t= %d, %d\n", id, t, ++counter); pthread_rwlock_unlock(&rwlock); usleep(100000); } } int main() { pthread_t tid[8]; pthread_rwlock_init(&rwlock, NULL); int i = 0; for(i = 0; i < 3; i++) { int* p =(int*) malloc(sizeof(int)); *p = i; pthread_create(&tid[i], NULL, writefunc, (void*)p); } for(i = 0; i < 5; i++) { int* p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[3+i], NULL, readfunc, (void*)p); } for(i = 0; i < 8; i++) { pthread_join(tid[i], NULL); } pthread_rwlock_destroy(&rwlock); return 0; }

条件变量:  如果说互斥锁是用于同步线程对共享数据的访问的化,那么条件变量这是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程       

1.定义条件变量  pthread_cond_t cond; 2.初始化        pthread_cond_init(&cond, NULL); 3.等待条件      pthread_cond_wait(&cond, &mutex);                                  mutex :如果没有在互斥环境,形同虚设                                  在互斥环境下:wait函数将mutex置1,wait返回,mutex恢复成原来的值 4.修改条件      pthread_cond_signal(&cond); 5.销毁条件      pthread_cond_destroy(&cond);  

规范写法: pthread_mutex_lock(); while(条件不满足) pthread_cond_wait(); //为什么会使用while? //因为pthread_cond_wait是阻塞函数,可能被信号打断而返回(唤醒),返回后从当前位置向下执行, 被信号打断而返回(唤醒),即为假唤醒,继续阻塞 pthread_mutex_unlock(); pthread_mutex_lock(); pthread_cond_signal(); //信号通知 ---- 如果没有线程在等待,信号会被丢弃(不会保存起来)。 pthread_mutex_unlock();#include #include #include #include //创建两个线程一个wait print,一个signal sleep() pthread_cond_t cond; pthread_mutex_t mutex; void* f1(void* arg) { while(1) { pthread_cond_wait(&cond, &mutex); printf("running!\n"); } } void* f2(void* arg) { while(1) { sleep(1); pthread_cond_signal(&cond); } } int main() { pthread_t tid1, tid2; pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, f1, NULL); pthread_create(&tid2, NULL, f2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }

System V //基于内核持续性

信号量:      POSIX    //基于文件持续性的信号量 1.定义信号量: sem_t sem; 2,初始化信号量:    sem_init(sem_t* sem,                                                 int shared,   //0表示进程内有多少个线程使用                                                 int val);     //信号量初值 3.PV操作        int sem_wait(sem_t* sem);   //sem--;如果小于0,阻塞    P操作                       int sem_post(sem_t* sem);   //sem++;                 V操作 4.销毁            sem_destroy(sem_t* sem); 信号量实现生产者消费者模型:

#include #include #include #include #include //仓库中装产品编号,没装产品的位置,置为-1,装了的地方置为产品的编号 #define PRO_COUNT 3 #define CON_COUNT 2 #define BUFSIZE 5 sem_t sem_full; //标识可生产的产品个数 sem_t sem_empty; //表示可消费的产品个数 pthread_mutex_t mutex; //互斥量 int num = 0; //产品编号 int buf[BUFSIZE]; //仓库 int wr_idx; //写索引 int rd_idx; //读索引 void* pro(void* arg) { int i = 0; int id = *(int*)arg; free(arg); while(1) { sem_wait(&sem_full); //先判断仓库是否满 pthread_mutex_lock(&mutex); //互斥的访问具体的仓库的空闲位置 printf("%d生产者开始生产%d\n", id, num); for(i = 0; i < BUFSIZE; i++) { printf("\tbuf[%d]=%d", i, buf[i]); if(i == wr_idx) { printf(""); } printf("\n"); } int r = buf[rd_idx]; buf[rd_idx] = -1; rd_idx = (rd_idx+1)%BUFSIZE; sleep(rand()%4); printf("%d\n消费者消费完%d\n", id, r); pthread_mutex_unlock(&mutex); sem_post(&sem_full); sleep(rand()%2); } } int main() { pthread_t tid[PRO_COUNT+CON_COUNT]; pthread_mutex_init(&mutex, NULL); //初始化 sem_init(&sem_empty, 0, 0); sem_init(&sem_full, 0, BUFSIZE); srand(getpid()); int i = 0; for(i = 0; i < BUFSIZE; i++) //初始化仓库 -1表示没有品 buf[i] = -1; for(i = 0; i < PRO_COUNT; i++) //产生生产者 { int *p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[i], NULL, pro, p); } for(i = 0; i < CON_COUNT; i++) { int *p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[i+CON_COUNT], NULL, con, p); } for(i = 0; i < PRO_COUNT + CON_COUNT; i++) { pthread_join(tid[i], NULL); } pthread_mutex_destroy(&mutex); //销毁 sem_destroy(&sem_empty); sem_destroy(&sem_full); return 0; }

拓展学习:

乐观锁和悲观锁?

乐观锁:

     在关系数据库管理系统里,乐观并发控制(又名”乐观锁”,Optimistic Concurrency Control,缩写”OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

乐观并发控制的事务包括以下阶段:  1. 读取:事务将数据读入缓存,这时系统会给事务分派一个时间戳。  2. 校验:事务执行完毕后,进行提交。这时同步校验所有事务,如果事务所读取的数据在读取之后又被其他事务修改,则产生冲突,事务被中断回滚。 

3. 写入:通过校验阶段后,将更新的数据写入数据库。

优点和不足:

       乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

悲观锁:

    在关系数据库管理系统里,悲观并发控制(又名”悲观锁”,Pessimistic Concurrency Control,缩写”PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

优点和不足:悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

系统最多能够创建多少个线程? (一般以实测为准,但根据每次开辟的栈的大小不同,测试结果也会不同)。

一个是直接在命令行查看    cat /proc/sys/kernel/threads-max  我的电脑显示是 7572

另一个是自己计算 用户空间大小3G 即是3072M/8M栈空间  = 380     

第三个写程序:   跑到32754(理论值 32768)

#include #include #include #include //创建线程 void* foo(void* arg) { } int main() { int count = 0; pthread_t thread; while(1) { if(pthread_create(&thread, NULL, foo, NULL) != 0) return 1; count++; printf("MAX = %d\n", count); } return 0; }

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3