主要方式: 管道、信号、信号量、消息队列、共享内存、套接字
又分为有名管道和无名管道,管道都是半双工的
#include
int pipe(int pipefd[2]);
数组中存着前者是管道的读端,后者是写端,一个进程可以关闭某一个端口来实现自己到底是读还是写。
如fork出一个子进程,想要父写子读的话,父进程就关闭读端 close(pipefd[0]) ,子进程就关闭写端close(pipefd[1]); 创建好管道之后就可以通过read 和 write来进行读写操作。
write(pipefd[1],buf,strlen(buf)); read(pipefd[0],buf,strlen(buf));
样例代码参照底部
fifo创建一个命名管道,可以解决非血缘关系的进程之间的通信。
管道的是实现方式也是通过文件来实现的。
首先命令行 mkfifo filename就可以创建一个管道文件。
然后在不同进程里传入文件名即可进行读写,open,write,close open,read,close
信号就是由用户、系统和进程发送给目标进程的信息,通知目标进程中某个状态的改变或者异常。
信号的产生分为硬中断和软中断
函数原型: sighandler_t signal(int signum,sighandler_t handler),handler一般是一个函数,这个函数传入信号的数值,根据信号数值表设计判断就可以进行不同的处理了。
SIGINT Term 键盘输入以终端进程(ctrl + C)
将磁盘文件的一部分直接映射到进程的内存中,mmap设置了两种机制:共享和私有
共享映射:内存中对文件进行修改,那么磁盘中对应的文件也会被修改。
私有映射:内存中的文件和磁盘中的文件是独立关系的,两者进行修改都不会对对方造成影响
函数原型
#include
void *mmap(void* addr,size_t length, int prot , int flags ,int fd, off_t offset);
int munmap(void* addr,size_t length);
mmap存在一个问题
当flags为MAP_SHARED的时候,即使修改了内存,并不能保证映射文件能够马上更新,映射文件的更新是由内核虚拟内存调度算法进行的。因此如果两个进程同时写会导致映射文件内容的不可预知性。
需要进行同步
int shmget(key_t key, size_t size, int shmflg);//创建共享内存,key命名
void *shmat(int shm_id, const void *shm_addr, int shmflg);//启动共享内存,把共享内存连接到当前进程地址空间,因为是一个void*指针,所以可以用任意对象指针来改变。如 shared = (string *)shm; 然后就可以对shared进行修改就是修改共享内存。
int shmdt(const void *shmaddr);//分离共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);//控制共享内存
消息队列是在内存中独立于进程的一段内存区域,创建了消息队列,任何进程只要有访问权限就可以访问消息队列。
#include
#include
#include
int msgget(key_t key, int msgflg);//创建一个消息队列 返回msqid
int msgctl(int msqid,int cmd,struct msqid_ds *buf);// 获取和设置消息队列的属性
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//发送到消息队列, msgp就是发送的数据,数据结构中第一个字段必须为long类型
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);//接受消息。
int semget(key_t key,int nsems,int flags);//第二个参数是信号量集的个数
int semctl(int semid,int semnum,int cmd,su);//根据cmd来判断是SETVAL还是GETVAL
semop(int id,struct sembuf sb[],int len);// sembuf是一个结构体
struct sembuf{ short sem_num, //信号量的下标
short sem_op, // 1表示V操作,-1表示P操作
short sem_flg //一般填0,表示如果信号量为0就阻塞
};
因为线程是共享进程的内存空间的,因此线程基本上是不需要进行数据交换,主要要做的是线程之间的同步。
出现这个的原因是 sums++/sums+=1/sums=sums+1 这些操作都不是原子操作,都是通过操作符函数来实现的,操作符函数是对地址的值或者临时变量上加1 然后返回数值;这是分两步走的,所以多线程下会出现不可预知性。
要解决上面这种情况就要多线程之间的互斥锁
{
std::mutex mylock;
std::lock_guard<std::mutex> mylock_guard(mylock);//当对象被创建的时候上锁,被销毁的时候解锁; 这个对象只有构造和析构函数两个函数。
}
{
std::mutex mylock;
std::unique_lock<mutex> ulock(mylock);//当对象被创建的时候上锁,被销毁的时候解锁; 这个对象只有构造和析构函数两个函数。
}
unique_lock相比于lock_guard来的复杂一些,lock_gurad只有两个函数,而unique_lock可用的函数更多,因此从操作上来说更加的灵活。
condition_variable是一个类,通常搭配互斥量mutex来用。 条件变量一般是在消费者生产者中使用,因为可以用条件变量来唤醒。
需要知道的两个函数是wait和notify_*函数。
notify_one每次只会唤醒一个线程。如果用notify_all就会唤起所有线程,但是每次只有一个线程能够继续后面的工作,剩下的线程又只能继续等待,这样多个线程等待一个唤醒的情况就是惊群效应。
惊群效应消耗了什么资源?
Linux会对每一个线程(进程)进行调度、上下文切换。 上下文切换过高使得CPU就像一个搬运工,在寄存器和运行队列中奔波。
直接的消耗包括 CPU 寄存器要保存和加载(例如程序计数器)、系统调度器的代码需要执行。间接的消耗在于多核 cache 之间的共享数据。
为了确保只有一个进程(线程)得到资源,需要对资源操作进行加锁保护,加大了系统的开销。
参考代码见样例代码。
实现方法上有两种,一种是C14的新特性,一种是POSIX下的pthread中实现的读写锁的机制。
C14
C14中提供了一个新的锁方式 std::shared_lock 能够以共享模式进行锁定,其实就是读锁。 但是mutex是不可以多次加锁的因此C14中还提供了std::shared_timed_mutex
std::lock_guardstd::shared_timed_mutex writerLock(shared_mutex);//以排他性方式上锁unique_lock可以这么做。
std::shared_lockstd::shared_timed_mutex readerLock(shared_mutex)//;以共享所有权方式上锁
上面两者都是退出作用域就可以自动解锁。
pthread
初始化读写锁 pthread_rwlock_init 语法
读取读写锁中的锁 pthread_rwlock_rdlock 语法
读取非阻塞读写锁中的锁pthread_rwlock_tryrdlock 语法
写入读写锁中的锁 pthread_rwlock_wrlock 语法
写入非阻塞读写锁中的锁pthread_rwlock_trywrlock 语法
解除锁定读写锁 pthread_rwlock_unlock 语法
销毁读写锁 pthread_rwlock_destroy 语法
自旋锁是一种用于保护多线程共享资源的锁,与一般的互斥锁(mutex)不同之处在于当自旋锁尝试获取锁的所有权时会以忙等待(busy waiting)的形式不断的循环检查锁是否可用。在多处理器环境中对持有锁时间较短的程序来说使用自旋锁代替一般的互斥锁往往能提高程序的性能。
CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,这种操作提供了硬件级别的原子操作(通过锁总线的方式)
bool CAS(V,A,B)
{
if(V==A){
swap(V,B);
return true;
}
return false;
}
自旋锁的用途,本质上是希望使得一个线程在不满足情况下,一直处于轮询状态;x,那么伪代码逻辑
b=true
while(CAS(flag,false,b)==false);//如果flag==true那么就一直循环,如果flag==false就把flag=b,同时退出循环
//do something
flag=false;
#include
#include
#include
#include
#include
#include
int main(void){
char buf[1024] = "Hello Child\n";
char str[1024];
int fd[2];
if(pipe(fd) == -1){
perror("pipe");
exit(1);
}
pid_t pid = fork();
// 父写子读 0写端 1读端
if(pid > 0){
printf("parent pid\n");
close(fd[0]); // 关闭读端
sleep(5);
write(fd[1], buf, strlen(buf)); // 在写端写入buf中的数据
wait(NULL);
close(fd[1]);
}
else if(pid == 0){
close(fd[1]); // 关闭写端
int len = read(fd[0], str, sizeof(str)); // 在读端将数据读到str
write(STDOUT_FILENO, str, len);
close(fd[0]);
}
else {
perror("fork");
exit(1);
}
return 0;}
2.信号量
#include
#include
#include
#include
#include
using namespace std;
void sig_handler(int signum)
{
if(0 > signum)
{
fprintf(stderr,"sig_handler param err. [%d]\n",signum);
return;
}
if(SIGINT == signum)
{
printf("Received signal [%s]\n",SIGINT==signum?"SIGINT":"Other");
}
if(SIGQUIT == signum)
{
printf("Received signal [%s]\n",SIGQUIT==signum?"SIGQUIT":"Other");
}
return;
}
int main(int argc,char **argv)
{
printf("Wait for the signal to arrive.\n ");
/*登记信息*/
signal(SIGINT,sig_handler);
signal(SIGQUIT,sig_handler);
pause();
pause();
signal(SIGINT,SIG_IGN);
return 0;
}
#include
#include
#include
#include
#include
#include
std::mutex mtx; // 全局互斥锁
std::queue<int> que; // 全局消息队列
std::condition_variable cr; // 全局条件变量
int cnt = 1; // 数据
void producer() {
while(true) {
{
std::unique_lock<std::mutex> lck(mtx);//在这个作用域内lck可以使用,创建即上锁,销毁即解锁。
// 在这里也可以加上wait 防止队列堆积 while(que.size() >= MaxSize) que.wait();
que.push(cnt);
std::cout << "向队列中添加数据:" << cnt ++ << std::endl;
// 这里用大括号括起来了 为了避免出现虚假唤醒的情况 所以先unlock 再去唤醒
}
cr.notify_all(); // 唤醒所有wait
}}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lck(mtx);
while (que.size() == 0) { // 这里防止出现虚假唤醒 所以在唤醒后再判断一次
cr.wait(lck);
}
int tmp = que.front();
std::cout << "从队列中取出数据:" << tmp << std::endl;
que.pop();
}}
int main(){
std::thread thd1[2], thd2[2];
for (int i = 0; i < 2; i++) {
thd1[i] = std::thread(producer);
thd2[i] = std::thread(consumer);
thd1[i].join();
thd2[i].join();
}
return 0;}