目录
线程间通信方式
1.线程通信简介
2.全局变量方式
3.参数传递方式
4.消息传递方式
5.线程同步法
5.1 锁机制
a> 互斥锁
b> 自旋锁
c> 读写锁
d> 条件变量
5.2 信号量机制
e> 信号量
线程间通信方式
1.线程通信简介
一般而言,在一个应用程序(即进程)中,一个线程往往不是孤立存在的,常常需要和其它线程通信,以执行特定的任务。如主线程和次线程,次线程与次线程,工作线程和用户界面线程等。这样,线程与线程间必定有一个信息传递的渠道。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的。线程间的通信涉及到4个问题:
a.线程间如何传递信息;
b.线程之间如何同步,以使一个线程的活动不会破坏另一个线程的活动,以保证计算结果的正确合理;
c.当线程间具有依赖关系时,如何调度多个线程的处理顺序;
d.如何避免死锁问题。
线程间的通信一般采用四种方式:全局变量方式、消息传递方式、参数传递方式和线程同步法。
2.全局变量方式
由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,我们建议使用volatile修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。
(需要注意:在一个线程使用全局变量时,有可能其他线程也在访问该数据,那么某一线程使用的数据就可能遭到破坏。)
3.参数传递方式
该方式是线程通信的官方标准方法,多数情况下,主线程创建子线程并让其子线程为其完成特定的任务,主线程在创建子线程时,可以通过传给线程函数的参数和其通信。所传递的参数是一个32位的指针,该指针不但可以指向简单的数据,而且可以指向结构体或类等复杂的抽象数据类型。
4.消息传递方式
应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息变的非常简单。我们可以在一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。
(例如进程之间的通讯方式:管道,信号量,信号,消息队列,共享内存,套接字)
5.线程同步法
还可以通过线程同步来实现线程间通信。例如有两个线程,线程A写入数据,线程B读出线程A准备好的数据并进行一些操作。这种情况下,只有当线程A写好数据后线程B才能读出,只有线程B读出数据后线程A才能继续写入数据,这两个线程之间需要同步进行通信。关于线程同步的方法和控制是编写多线程的核心和难点,方法也比较多
5.1 锁机制
包括互斥锁/量(mutex)、读写锁(reader-writer lock)、自旋锁(spin lock)、条件变量(condition)
(1)互斥锁/量(mutex):提供了以排他方式防止数据结构被并发修改的方法。
(2)读写锁(reader-writer lock):允许多个线程同时读共享数据,而对写操作是互斥的。
(3)自旋锁(spin lock)与互斥锁类似,都是为了保护共享资源。互斥锁是当资源被占用,申请者进入睡眠状态;而自旋锁则循环检测保持者是否已经释放锁。
(4)条件变量(condition):可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
a> 互斥锁
1)加锁和解锁,确保同一时间只有一个线程访问共享资源;
2)访问共享资源之前加锁,访问完成后释放锁;
3)如果某线程持有锁,其他线程形成等待队列;
互斥锁操作函数
pthread_mutex_t mutex; // 声明锁
int pthread_mutex_init(); // 初始化锁
int pthread_mutex_lock(); // 等待并加锁
int pthread_mutex_trylock(); // 尝试加锁不等待
int pthread_mutex_timedlock(); // 带超时机制的加锁
int pthread_mutex_unlock(); // 解锁
int pthread_mutex_destroy(); // 销毁锁
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int var;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 宏声明并初始化互斥锁互斥锁
void *thmain(void *arg); // 线程主函数int main(int argc, char* argv[])
{pthread_t thid1,thid2; // 线程id// 函数初始化锁// pthread_mutex_init(&mutex,NULL); // 第一个参数锁的id,第二个锁的属性,下面说// 创建线程,参数1:线程id,参数2:线程属性,参数3:线程主函数,参数4:线程主函数参数if(pthread_create(&thid1,0,thmain,0)!=0) {printf("create failed.\n"); return -1; }if(pthread_create(&thid2,0,thmain,0)!=0) {printf("create failed.\n");return -1;}// 等待线程退出--第一个参数线程id,第二个参数线程退出状态,这里表示不关心pthread_join(thid1,NULL); pthread_join(thid2,NULL);printf("var=%d\n",var);// 退出程序前销毁锁pthread_mutex_destroy(&mutex); // 一个参数锁的idreturn 0;
}
// 两个线程共用这一个线程主函数
void *thmain(void *arg)
{for(int ii=0;ii<1000000;ii++){pthread_mutex_lock(&mutex); // 加锁var++; // 加锁之后操作变量pthread_mutex_unlock(&mutex); // 解锁}
}编译
g++ -g -o demo demo.cpp -lpthread
互斥锁的属性
PTHREAD_MUTEX_TIMED_NP,这个是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁(递归锁),允许同一个线程对同一个锁成功获取多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,解锁后,请求锁的线程重新竞争。
b> 自旋锁
自旋锁的功能和互斥锁一样,但是互斥锁在等待锁的过程中会进行休眠,不消耗cpu,而自旋锁在等待锁的过程中会不断地循环检查锁是否可用。
另外,自旋锁没有带超时机制的加锁,因为一般自旋锁都是使用在加锁时间极短的场景。
自旋锁操作函数
pthread_spinlock_t mutex; // 声明锁
int pthread_spin_init(); // 初始化锁
int pthread_spin_lock(); // 加锁
int pthread_spin_trylock(); // 尝试枷锁
int pthread_spin_unlock(); // 解锁
int pthread_spin_destroy(); // 销毁锁
注意,自旋锁不能再使用宏进行初始化了,只能使用函数,并且自旋锁的初始化锁函数和互斥锁有点不同
互斥锁:pthread_mutex_init(锁的id,锁的属性)
自旋锁:pthread_spin_init(锁的id,锁的共享标志);
自旋锁的属性
初始化函数的第二个参数用于设置属性(共享标志)
PTHREAD_PROCESS_SHARED // 共享
PTHREAD_PROCESS_PRIVATE // 私有
实际开发中可能会有多个进程中每个进程又创建了多个线程的场景,如果spin自旋锁的第二个参数,共享标志设置为共享SHARED,就代表多进程的其他进程可以使用这个进程的自旋锁。如果共享标志设置为PRIVATE私有的,就代表只有创建这个自旋锁的进程可以使用该自旋锁。一般情况下我们设置为私有PTHREAD_PROCCESS_PRIVATE
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int var;
// 注意下面的初始化是spinlock不是spin
pthread_spinlock_t spin; // 自旋锁只能用函数初始化,不可以使用宏
void *thmain(void *arg); // 线程主函数int main(int argc, char* argv[])
{// 设置为私有,其他进程不可共享当前进程的自旋锁pthread_spin_init(&spin,PTHREAD_PROCESS_PRIVATE);pthread_t thid1,thid2; // 线程id// 创建线程,参数1:线程id,参数2:线程属性,参数3:线程主函数,参数4:线程主函数参数if(pthread_create(&thid1,0,thmain,0)!=0) {printf("create failed.\n"); return -1; }if(pthread_create(&thid2,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }// 等待线程退出--第一个参数线程id,第二个参数线程退出状态,这里表示不关心pthread_join(thid1,NULL);pthread_join(thid2,NULL);printf("var=%d\n",var);// 退出程序前销毁锁pthread_spin_destroy(&spin); // 一个参数锁的idreturn 0;
}// 两个线程共用这一个线程主函数
void *thmain(void *arg)
{for(int ii=0;ii<1000000;ii++){pthread_spin_lock(&spin); // 加锁var++; // 加锁之后操作变量pthread_spin_unlock(&spin); // 解锁}
}
c> 读写锁
读写锁允许更高的并发性
三种状态:读模式加锁(读锁)、写模式加锁(写锁)、不加锁
读写锁的特点
只要没有线程持有写锁,任意线程都可以成功申请读锁
只有在所有线程都没有锁的情况下,申请写锁才能成功
读写锁的属性
读写锁的属性也是共享标志,分为PTHREAD_PROCESS_PRIVATE(私有)和PTHREAD_PROCESS_SHARED(共享),和自旋锁设置属性的方式一样,参上,不过读写锁有专门设置锁的属性的函数
读写锁的操作函数
pthread_rwlock_t mutex; // 声明读写锁rw--read-write
int pthread_rwlock_init(); // 初始化读写锁
int pthread_rwlock_destroy(); // 销毁锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; // 声明并初始化读写锁
int pthread_rwlock_rdlock(); // 申请读锁
int pthread_rwlock_tryrdlock(); // 尝试申请读锁,不会产生阻塞
int pthread_rwlock_timedrdlock(); // 带超时机制的申请读锁
int pthread_rwlock_wrlock(); // 申请写锁
int pthread_rwlock_trywrlock(); // 尝试申请写锁,不产生阻塞
int pthread_rwlock_timedwrlock(); // 申请写锁,带有超时机制
int pthread_rwlock_unlock(); // 解锁函数,读锁写锁通用
int pthread_rwlockattr_getpshared(); // 获取读写锁的属性
int pthread_rwlockattr_setpshared(); // 设置读写锁的属性
代码示例:
demo06.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; // 声明读写锁并初始化void *thmain(void *arg); // 线程主函数
void handle(int sig); // 信号15的处理函数。int main(int argc, char* argv[])
{signal(15,handle); // 当程序接收到15的信号会调用该函数。pthread_t thid1,thid2,thid3;// 创建线程,参数1:线程id,参数2:线程属性,参数3:线程主函数,参数4:线程主函数参数if(pthread_create(&thid1,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }sleep(1); // 线程之间sleep 1秒,这样能保证三个线程会一直拥有锁if(pthread_create(&thid2,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }sleep(1);if(pthread_create(&thid3,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }// 等待线程退出--第一个参数线程id,第二个参数线程退出状态,这里表示不关心pthread_join(thid1,NULL); pthread_join(thid2,NULL); pthread_join(thid3,NULL);// 退出程序前销毁锁pthread_rwlock_destroy(&rwlock); // 一个参数锁的idreturn 0;}
// 三个线程共用这一个线程主函数
void *thmain(void *arg)
{for(int ii=0;ii<100;ii++){printf("线程%lu开始申请读锁...\n",pthread_self());pthread_rwlock_rdlock(&rwlock); // 第一个线程申请时肯定没有写锁,所以任意线程都能申请读锁printf("线程%lu申请读锁成功...\n\n",pthread_self());sleep(5);pthread_rwlock_unlock(&rwlock); // 解锁printf("线程%lu已经释放读锁...\n\n",pthread_self());}
}void handle(int sig)
{printf("开始申请写锁...\n");pthread_rwlock_wrlock(&rwlock); // 加锁printf("申请写锁成功...\n");sleep(10);pthread_rwlock_unlock(&rwlock); // 解锁printf("写锁已经解除...\n\n");
}
解释一下代码:
上面代码中关于信号处理的方法以后我会讲,这里大家只需要知道15的信号宏名为SIGTERM,也就是我们的killall命令,他就是15的信号。当进程运行时,killall这个进程,就会调用信号处理函数,也就是你使用killall命令杀这个程序的时候,进程收到命令就会开始申请写锁。
这里的代码和之前的略有不同
首先就是创建线程之间会延迟一秒,这里可以先看一下线程主函数中的内容
主函数首先申请了读写锁,因为三个线程中没有线程申请写锁,所以申请一定会成功,那么第一个创建的线程就会持有读锁,然后过了一秒第二个线程又持有了读锁,再过一秒第三个线程也持有了读锁,此时再过两秒之后第一个线程释放了锁,但是还有两个线程持有锁,等线程2释放锁的时候,线程1和线程3还会持有读锁,这就能保证程序运行的时候三个线程会一直持有读锁,所以申请写锁不会成功。
假设我们不延时,三个线程同时(相差时间忽略不计)被创建,最后又同时释放锁,这个时候,因为你一旦发送15的信号,他开始申请写锁就会一直在申请,哪怕你只有几毫秒的时间没有持有锁,写锁也会申请成功。
我们运行程序,发送killall命令,如果申请写锁一直不成功,就说明线程一直持有读锁。
运行如下:
发送15的信号:
结果:
可以看到写锁一直没有申请成功,这足以证明两点:
1、当线程一直持有锁的时候,写锁不会申请成功
2、这个程序是不间断持有读锁的
如果实际开发中这样让程序一直持有读锁,不就造成写入线程饿死的情况了吗?
所以我们实际开发中肯定不能让线程一直持有读锁。
d> 条件变量
条件变量操作函数
pthread_cond_t cond; // 生命条件变量
int pthread_cond_init(); // 初始化条件变量
int pthread_cond_destroy(); // 销毁条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 声明并初始化int pthread_cond_wait(); // 等待被唤醒
int pthread_cond_timedwait(); // 等待被唤醒,带超时机制int pthread_cond_signal(); // 唤醒一个等待中的线程
int pthread_cond_broadcast(); // 唤醒全部等待中的线程// 设置条件变量的共享属性,也就是在多个进程的线程之间是否共享条件变量
int pthread_condattr_getpshared(); // 获取共享属性
int pthread_condattr_setpshared(); // 设置共享属性// 设置条件变量的时钟属性,一般用不到
int pthread_condattr_getclock(); // 获取时钟属性
int pthread_condattr_setclock(); // 设置时钟属性
注意事项
首先大家不要被条件变量的名称给误导了,大家就把它当作是一种特殊的锁就行了
另外条件变量必须搭配互斥锁使用,至于为什么以后会讲,大家也可以先猜测猜测是为什么
条件变量的wait()函数会产生阻塞
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>// 条件变量需要搭配互斥锁使用
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 声明并初始化条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 声明并初始化互斥锁void* thmain(void *arg); // 线程主函数
void handle(int sig); // 信号处理函数int main(int argc, char* argv[])
{signal(15,handle); // 设置信号15的处理函数pthread_t thid1,thid2,thid3;// 创建线程if(pthread_create(&thid1,0,thmain,0)!=0){ printf("create failed.\n"); return -1; }if(pthread_create(&thid2,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }if(pthread_create(&thid3,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }// 等待子线程的退出pthread_join(thid1,NULL); pthread_join(thid2,NULL); pthread_join(thid3,NULL);// 销毁锁和条件变量pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex);return 0;
}void *thmain(void* arg)
{while(true){printf("线程%lu开始等待条件信号...\n",pthread_self()); // 获取当前线程号pthread_cond_wait(&cond,&mutex); // 等待条件信号,注意参数printf("线程%lu等待条件信号成功...\n\n",pthread_self());}
}void handle(int sig)
{printf("发送条件信号...\n");pthread_cond_signal(&cond); // 唤醒等待条件信号的一个线程。/* pthread_cond_broadcast(&cond); // 唤醒等待条件信号的全部线程。 */
}
创建了三个线程,然后设置了15(killall命令)的信号处理函数。在线程主函数中,每一个线程都是一直在等待条件信号,如果使用一次killall就会调用一次handle()函数,handle函数每调用一次就会激活一个等待中的线程
**测试时**
发送4次15信号
**结果:**
我们可以看到,我们发送了四次15的信号,每一次都会调用一次信号处理函数,然而调用一次信号处理函数就会激活一个等待中的线程,但是这里激活线程并不是随机的,而是按照等待的顺序去激活的,仔细观看线程编号就可以发现。
激活全部线程的函数代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>// 条件变量需要搭配互斥锁使用
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 声明并初始化条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 声明并初始化互斥锁
void* thmain(void *arg); // 线程主函数
void handle(int sig); // 信号处理函数int main(int argc, char* argv[])
{signal(15,handle); // 设置信号15的处理函数pthread_t thid1,thid2,thid3;// 创建线程if(pthread_create(&thid1,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }if(pthread_create(&thid2,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }if(pthread_create(&thid3,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }// 等待子线程的退出pthread_join(thid1,NULL); pthread_join(thid2,NULL); pthread_join(thid3,NULL);// 销毁锁和条件变量pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex);return 0;
}void *thmain(void* arg)
{while(true){printf("线程%lu开始等待条件信号...\n",pthread_self()); // 获取当前线程号pthread_cond_wait(&cond,&mutex); // 等待条件信号,注意参数printf("线程%lu等待条件信号成功...\n\n",pthread_self());}
}void handle(int sig)
{printf("发送条件信号...\n");/* pthread_cond_signal(&cond); // 唤醒等待条件信号的一个线程。*/pthread_cond_broadcast(&cond); // 唤醒等待条件信号的全部线程。
}
5.2 信号量机制
e> 信号量
信号量操作函数
更多内容可以看:https://blog.csdn.net/Bossking321/article/details/135293443?spm=1001.2014.3001.5502
sem_t *sem; // 声明信号量
int sem_init(); // 初始化信号量
int sem_destroy(); // 销毁信号量
int sem_wait(sem_t *sem); // 信号量的P操作
int sem_trywait(sem_t *sem); // 信号量的P操作,不阻塞
int sem_timedwait(); // 信号量的P操作,带有超时机制int sem_post(sem_t *sem); // 信号量的V操作
int sem_getvalue(); // 获取信号量的值
解释一下,我们这里使用的是二元信号量,也就是信号值只有0和1,0代表不可用,1代表可用,信号量的初始值我们可以通过init函数设置,一般设置为1。当我们使用P操作的时候,信号值就会减1,变为0--不可用(类似于加锁的操作),然后当我们使用了V操作的时候,信号量就会加1,变为1--可用(类似于解锁的操作)。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>int var;
sem_t sem; // 声明信号量,使用函数初始化
void *thmain(void *arg); // 线程主函数int main(int argc, char* argv[])
{pthread_t thid1,thid2; // 线程idsem_init(&sem,0,1); // 第一个参数,信号量id,第二个先固定填0,第三个信号量初始值// 创建线程,参数1:线程id,参数2:线程属性,参数3:线程主函数,参数4:线程主函数参数if(pthread_create(&thid1,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }if(pthread_create(&thid2,0,thmain,0)!=0) { printf("create failed.\n"); return -1; }// 等待线程退出--第一个参数线程id,第二个参数线程退出状态,这里表示不关心pthread_join(thid1,NULL); pthread_join(thid2,NULL);printf("var=%d\n",var);// 退出程序前销毁锁sem_destroy(&sem); // 一个参数锁的idreturn 0;
}// 两个线程共用这一个线程主函数
void *thmain(void *arg)
{for(int ii=0;ii<1000000;ii++){sem_wait(&sem); // 信号量的P操作(加锁)var++; // 加锁之后操作变量sem_post(&sem); // 信号量的V操作(解锁)}
}