C、C++多线程编程

2023-05-16

本文的笔记来自于b站视频的爱编程的大丙,博客链接:https://subingwen.cn/,有做了相应的补充!

一、线程概述

        进程对应的虚拟地址空间的各个分区如图:

         每个进程的虚拟地址空间都是从0地址开始的,在程序中打印的变量也是在虚拟地址的,程序是无法直接访问物理内存的。虚拟地址空间中用户地址空间的范围为:0~3G,里边分为多个区块。

        线程是轻量级的进程(light weight process),在Linux环境下线程的本质仍然是进程,在计算机运行的程序是一组指令以及指令参数的组合,指令按照既定的逻辑控制计算运行,操作系统会以进程为单位,分配系统资源,可以这样理解,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

        先从概念上了解一下线程和进程之间的区别:

1、进程有自己独立的地址空间,多个线程共用同一个地址空间。

  (1)线程更加节省系统资源,效率不仅可以保持,而且能够更高;

  (2)在一个地址空间中多个线程独享:每个线程都有属于自己栈区,寄存器(内核中管理的);

  (3)在一个地址空间中多个线程共享:代码区,堆区,全局数据区,打开的文件(文件描述符)都是线程共享的;

2、线程是程序的最小执行单元,进程是操作系统中最小的资源分配单位。

  (1)每个进程拥有地址空间,一个进程只能抢一个CPU时间片;

  (2)一个地址空间中可以划分多个进程,在有效的资源基础上,能够抢到更多的CPU时间片;

  (3)CPU的调度和切换:线程的上下文切换比进程要快的多;

        上下文切换:线程、进程采用分时复用CPU时间片,在切换之前会将上一个任务的状态进程保存,下次切换会这个任务时会加载到这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换;

  (4)线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。

        在处理多任务程序的时候使用多线程比使用多进程更有优势,但是线程并不是越多越好!如何控制线程的个数?

  1. 文件IO操作:文件IO对CPU是使用率不高,因此可以分时复用CPU时间片,线程的个数=2*CPU核心数(效率最高);
  2. 处理复杂的算法(主要是CPU进程运算,压力大),线程的个数=CPU的核心数(效率最高);

二、创建线程

        每个线程都有唯一的线程ID,ID类型为pthread_t,这个ID是无符号长整形数,如果想要得到当前线程的线程ID,可以调用如下:

函数:

pthread_t pthread_self(void);  //返回当前线程的线程ID

        在一个进程中调用线程创建函数,就可以得到一个子线程,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。

C语言实现:

#include <pthread.h>
int pthread_create(pthread_t *thread,const pthread_attr_t* attr,void* 
(*start_routine)(void*),void* arg);

参数:

  • thread:传入参数,是无符号长整形数,线程创建成功,会将线程ID写入到这个指针指向的内存中;
  • attr:线程的属性,一般情况下使用默认属性即可,写入NULL;
  • start_routine:函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行;
  • arg:作为实参传入到start_routine指向的函数内部;

返回值:

  •  线程创建成功后返回值0,创建失败返回对应的错误码!

三、线程退出

        在编写多线程程序时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对主线程而言),就可以调用线程库中的线程退出函数,只要调用线程函数,当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

C语言实现:  

#include <pthread.h>
void pthread_exit(void* retval);

参数:

  • 线程退出的时候携带的数据,当前子线程的主线程会得到该数据,如果不需要使用,指定为NULL。

四、线程回收

        线程和进程一样,子线程退出的时候内核主要由主线程回收,线程库中提供的线程回收函数式叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出后,函数解除阻塞进行资源的回收,函数被调用一次只能回收一次,只能回收一个子线程,如果有多个子线程需要循环进程回收,类似于wait函数。

C语言实现:  

#include <pthread.h>
int pthread_join(pthread_t thread,void** retval);

参数:

  • thread:要回收的子线程的线程ID;
  • retval:二级指针,指向上一级指针的地址,是一个传出参数,这个地址中存储了pthread_exit()传递出的数据,如果不需要这个参数则设置为NULL。

返回值:

  • 线程创建成功后返回值0,创建失败返回对应的错误码!

五、线程分离

        在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程复杂子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务就不能被执行了。

        在线程库函数为我们提供了线程分离的函数pthread_detach(),调用这个函数之后指定的子线程可以和主线程分离,当子线程退出的时候,其占用的内核资源被系统的其他进程接管并回收了。线程分离之后再主线程中使用pthread_join()就回收不到子线程资源了。

C语言实现:

#include <pthread.h>
int pthread_detach(pthread_t thread);

参数:

  • thread:子线程的线程ID

六、其他线程函数

        线程取消的意思就是在某些特定是情况下在一个线程中杀死另外一个线程,使用这个函数杀死另外一个线程分两步:

  1. 在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候线程B死不了的;
  2. 在线程B中进程一次系统调用(从用户态到内核态),否则线程B可以一直运行;

C语言实现:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数:

  • thread:要杀死的线程ID;

返回值:

  • 线程创建成功后返回值0,创建失败返回对应的错误码!

七、C++线程类

        C++编程提供了线程类。

八、线程同步概念

        两个或多个线程操作数据的时候需要分时复用CPU时间片,并且测试程序中调用sleep()导致线程的CPU时间片还没有用完就被迫挂起了,这样就能让CPU的上下文切换(保存当前状态,下一次继续运行的时候需要加载保存的状态)更加频繁,更容易出现数据混乱的这个现象。

        CPU对应寄存器(寄存器在CPU中)、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被CPU处理完成需要再次被写入到物理内存中,物理内存数据也可以通过文件IO操作写入到磁盘中。

        在测试程序中两个线程共用number全局变量,当线程变成运行态之后开始操作数据,从物理内存加载数据,然后将输入放到CPU进行运算,最后将结果更新到物理内存中,如果操作数据的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。

        如果线程A在执行过程期间就失去了CPU时间片(因为每个线程分配一定的时间片,比如线程A先从内存中加载数据number,此时number的大小为1,先在寄存器+1,然后写入到内存,此时number为2;然后+1,然后写入到内存,此时number为3;然后+1,但此时,CPU的时间片已经用完了,这是线程A挂起,CPU进行上下文切换,记录下此时的线程A对应寄存器的状态;接着线程B开始运行,但是CPU从内存中读取到的number只是为3,这样就导致了重复操作问题。),线程A被挂起了最新的数据没能更新到物 理内存,线程B变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去了CPU时间片挂起。线程A得到了CPU时间片变成运行态,第一件事儿就是上次没更新到内存的数据更新到内存,但是这样会导致B已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会重复很多次。

 1、同步方式

         对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步,常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量,所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。

 C语言:   

//加锁
pthread_mutex_lock(&mutex);
/*这段代码称之为临界区*/
int cur=rand()%20;
if(number%2){
   printf("%d\n",number);
}
//解锁
pthread_mutex_unlock(&mutex);

        正如上面的案例,线程A在执行操作共享资源的时候,如果时间片用完,此时的锁还没释放先挂起,这时候线程B切换,但是当线程到达加锁的区域时候,由于线程A还没释放,因此线程B获取锁失败,而进入等待状态,从而就不会操作到共享资源,也就是上面的全局变量number。因此等一小会后,就切换回线程A,线程A,将数据写入内存,然后释放锁,这时候线程B才能有机会获取锁而进入临界区。

2、互斥锁

(1)互斥锁函数

互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行,不能并行处理,这样多线程访问共享资源数据混乱的问题就迎刃而解了,需要付出的代价就是执行效率的降低,因为临界区多个线程是可以并行处理的,现在只能串行执行。

在Linux中互斥锁的类型是:pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁:

pthread_mutex_t mutex;

在创建锁对象的时候保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID),一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能解除阻塞。一般情况下,每一个共享资源对应一把互斥锁,锁的个数与线程的个数无关!

Linux提供互斥锁操作函数如下,如果函数调用成功会返回0,调用失败会返回相应的错误号:

//restrict:是一个关键字,用于修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址,
//其它的不行
int pthread_mutex_init(pthread_mutex_t* restrict mutex,const 
pthread_mutex_mutexattr_t *restrict attr);

//释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t* mutex);

参数:

  • mutext-互斥锁变量的地址
  • attr-互斥锁的属性,一般使用默认属性即可,这个参数指定为NULL
//修饰互斥锁的状态,将其设定为锁定状态,这个状态被写入到参数mutex中
int pthread_mutex_lock(pthread_mutex_t* mutex)

这个函数被调用,首先会判断参数mutex互斥锁中状态是不是锁定状态:

  • 没有被锁定,是打开的,这个线程可以加锁成功,这个锁中会记录是哪个线程加锁成功了;
  • 如果被锁定了,其他线程加锁就是失败了,这些线程会阻塞在这把锁上;
  • 当这把锁被解开了,这些阻塞在锁上的线程解除阻塞了,并且这些线程是通过竞争的方式再获得对这把锁的使用权,没抢到的线程将继续阻塞;
//尝试加锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);

 调用这个函数对互斥锁变量还是两种情况:

  • 如果这把锁没有被锁定,而是打开的,那么线程就加锁成功;
  • 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号;
//对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);

不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功!

(2)互斥锁的使用

两个线程一共操作了同一个全局变量,因此需要添加一互斥锁,来控制这两个线程。具体例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 50

//全局变量
int number;

pthread_mutex_t mutex;

//线程处理函数
void* funcA_num(void* arg){
	for(int i=0;i<MAX;++i){
		pthread_mutex_lock(&mutex);
		int cur=number;
		cur++;
		usleep(10);//线程随眠,挂起,若线程A先拿到锁,而且在此休眠,线程B被阻塞
		number=cur;
		printf("Thread A,id=%lu,number=%d\n",pthread_self(),number);
		pthread_mutex_unlock(&mutex);
	}
	return NULL;
}

void* funcB_num(void* arg){
	for(int i=0;i<MAX;++i){
		pthread_mutex_lock(&mutex);
		int cur=number;
		cur++;
		usleep(5);
		number=cur;
		printf("Thread B,id=%lu,number=%d\n",pthread_self(),number);
		pthread_mutex_unlock(&mutex);
		usleep(5);//微秒
	}
	return NULL;
}

int main(int argc,char* argv[]){
	pthread_t p1,p2;
	//互斥锁的初始化
	pthread_mutex_init(&mutex,NULL);

	//创建两个子线程
	pthread_create(&p1,NULL,funcA_num,NULL);
	pthread_create(&p2,NULL,funcB_num,NULL);

    //阻塞,资源回收
	pthread_join(p1,NULL);
	pthread_join(p2,NULL);

	//释放互斥锁的资源
	pthread_mutex_destroy(&mutex);
	return 0;
}

3、死锁

当多个线程访问共享资源,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁则会造成的后果是:所有的线程被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。

造成死锁的情景有如下几种:

  • 加锁忘记解锁
// 场景1
void func(){
    for(int i=0; i<6; ++i){
        // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
        // 其余的线程也被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        // 忘记解锁
    }
}

// 场景2
void func(){
    for(int i=0; i<6; ++i){
        // 当前线程A加锁成功
        // 其余的线程被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        if(xxx){
            // 函数退出, 没有解锁(解锁函数无法被执行了)
            return ;
        }
        pthread_mutex_lock(&mutex);
    }
}
  • 重复加锁,造成死锁
void func(){
    for(int i=0; i<6; ++i){
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        // 锁被锁住了, A线程阻塞
        pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

// 隐藏的比较深的情况
void funcA(){
    for(int i=0; i<6; ++i){
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

void funcB(){
    for(int i=0; i<6; ++i){
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        funcA();		// 重复加锁
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}
  • 在程序中多个共享资源,因此很多把锁,导致相互被阻塞

场景描述:
  1. 有两个共享资源:X, Y,X对应锁A, Y对应锁B

  • 线程A访问资源X, 加锁A
  • 线程B访问资源Y, 加锁B

  2. 线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞

  •  线程A被锁B阻塞了, 无法打开A锁
  • 线程B被锁A阻塞了, 无法打开B锁

在使用多线程编程的时候,如何避免死锁呢?

  • 避免多次加锁,多检测;
  • 多共享资源访问完毕之后,一定要解锁,或者在加锁的使用trylock;
  • 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问)共享资源,但在有些情况下是做不到的, 另外也可以在其他互斥锁做加锁的操作之前,先释放当前线程拥有的互斥锁;
  • 项目程序中可以引入一些专门用于死锁检测的模块;

4、读写锁

(1)读写锁函数

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是可读的,那么读是可以并行的,但是使用互斥锁,读操作也是串行的。

读写锁是一把锁,锁的类型是pthread_rwlock_t,有了类型之后就可以创建一把互斥锁了:

pthread_rwlock_r rwlock;

之所以称为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作,

  • 锁的状态:锁定/打开
  • 锁定的是什么操作:读操作/写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然
  • 哪个线程将这把锁锁上了

读写锁的使用方式与互斥锁的使用方式完全一致:找共享资源,确定临界区,在临界区的开始位置进行加锁(读锁/写锁),临界区的结束位置解锁。

因为通过一把读写锁可以锁定读或者写操作,下面介绍一下关于读写锁的特点:

  1. 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的读锁是共享的
  2. 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的写锁是独占的
  3. 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高

如果说程序中所有的线程都对共享资源做写操作,使用读写锁的优势就没有了,和互斥锁一样的。如果程序中所有的线程对共享资源有读也有写操作,读操作越多越好!

Linux 提供的读写锁操作函数原型如下,如果函数调用成功返回 0,失败返回对应的错误号:

#include <pthread.h>
pthread_rwlock_t lock;
//初始化读写多
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,
const pthread_rwlockattr_t* restrict attr);

//释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);

参数:

  • rwlock:读写锁的地址,传出参数
  • attr:读写锁操作,一般使用默认属性,指定为NULL
//在程序中对读写锁加读锁,锁定的是读锁
int pthread_rwlock_rdlock(pthread_rwlock* rwlock);
  • 调用这个函数,如果读写操作是打开的,那么加锁成功;
  • 如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读是共享的;
  • 如果读写锁已经锁定了写操作,调用这个函数线程被阻塞;
//这个函数可以有效避免死锁
//如果加锁失败不会阻塞当前线程,直接返回错误码
int pthread_rwlock_tryrdlock(pthread_rwlock* rwlock);
  • 调用这个函数,如果读写操作是打开的,那么加锁成功;
  • 如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读是共享的
  • 如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的操作;
//在程序中对读写锁加写锁,锁的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
  • 调用这个函数,如果读写锁是打开的,那么加锁成功;
  • 如果读写锁已经锁定了读操作或者已经锁定了写操作,调用这个函数线程会被阻塞;
//解锁,不管是锁定了读还是写都可以解锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

(2)读写锁的使用

题目要求:8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 50

//全局变量
int number;//全局变量默认初始化为0,局部变量默认初始是任意值

pthread_rwlock_t rwlock;

//线程处理函数
void* read_num(void* arg){
	for(int i=0;i<MAX;++i){
		pthread_rwlock_rdlock(&rwlock);
		printf("Thread A,id=%lu,number=%d\n",pthread_self(),number);
		pthread_rwlock_unlock(&rwlock);
		usleep(rand()%5);
	}
	return NULL;
}

void* write_num(void* arg){
	for(int i=0;i<MAX;++i){
		pthread_rwlock_wrlock(&rwlock);
		int cur=number;
		cur++;
		usleep(5);
		number=cur;
		printf("Thread B,id=%lu,number=%d\n",pthread_self(),number);
		pthread_rwlock_unlock(&rwlock);
		usleep(5);//微秒,挂起
	}
	return NULL;
}

int main(int argc,char* argv[]){
	pthread_t p1[5],p2[3];
	//读写锁的初始化
	pthread_rwlock_init(&rwlock,NULL);

	//读线程5个,写线程3个
	for(int i=0;i<5;++i){
		pthread_create(&p1[i],NULL,read_num,NULL);
	}
	for(int i=0;i<3;++i){{
		pthread_create(&p2[i],NULL,write_num,NULL);
	}}

    //阻塞,资源回收
	for(int i=0;i<5;++i){
		pthread_join(p1[i],NULL);		
	}
	for(int i=0;i<3;++i){
		pthread_join(p2[i],NULL);
	}

	//释放读写锁的资源
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

5、条件变量

(1)条件变量函数

 如果在多线程程序中只使用条件变量,无法实现线程同步的,必须要配合互斥锁来使用,虽然条件变量和互斥锁都能阻塞线程,但是两者的效果不一致的,区别如下:

  • 假设有A-Z的26个线程,这26个线程共同访问同一把互斥锁,如果线程A加锁成功,那么其余B-Z的线程访问互斥锁都阻塞,所有的线程只能顺序访问临界资源;
  • 条件变量只有满足指定的条件下,才会阻塞线程,如果不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况会造成共享资源数据的混乱;

一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t,这样就可以定义一个条件变量类型的变量了:

pthread_cond_t* cond;

被条件变量阻塞的线程的线程信息会被记录这个变量中,以便达到解除阻塞的作用。

条件变量操作函数的原型:

#include <pthread.h>
pthread_cond_t cond;

//初始化
int pthread_cond_init(pthread_cond_t* restrict cond,
const pthread_condattr_t* restrict attr);

//销毁释放资源
int pthread_cond_destroy(pthread_cond_t* cond);
  • cond: 条件变量的地址
  • attr: 条件变量属性,一般使用默认属性,指定为 NULL
//线程阻塞函数,哪个线程调用这个函数,哪个线程就会阻塞
int pthread_cond_wait(pthread_cond_t* restrict cond,
pthread_mutex_t* restrict mutex):

该函数在阻塞线程的时候需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现共享资源的数据混乱的问题。该函数会对这个互斥锁做一下事情:

  • 在阻塞线程的时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做时为了避免出现死锁,说明当前不满足条件;
  • 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区;

简而言之,这个函数就是可以对互斥锁上锁和解锁,解锁就意味着阻塞,上锁意味着有一个线程可以使用锁,解除阻塞,进入临界区。 

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

 这个函数的前两个参数和 pthread_cond_wait 函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点:

time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s

 唤醒阻塞的线程:

//唤醒阻塞在条件变量上的线程,至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t* cond);

//唤醒阻塞在条件变量上的线程,被阻塞的线程全部被唤醒
int pthread_cond_broadcast(pthread_cond_t* cond);

调用上面两个函数中的任意一个,都可以换线被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程。

(2)生产者消费者模型

生产者消费者模型的组成:

  • 生产者线程若干个,生成商品或任务放到任务队列中,任务队列满了就工作,不满就阻塞,通过一个生产者的条件变量控制生产者线程的阻塞和非阻塞;
  • 消费者线程若干个,获取任务队列,将任务或数据取出,任务队列有数据就消费,没有数据就阻塞,通过一个消费者的条件变量控制消费者线程的阻塞和非阻塞;
  • 队列用于存储任务或数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存,可以是数组或链表,也可以是STL容器:queue/stack/list/vector

1564644834918

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
using namespace std;

pthread_cond_t cond;
pthread_mutex_t mutex;

//链表的节点类型
struct Node{
	int number;
	struct Node* next;
};

//链表的头节点
struct Node* head=NULL;

//生产者
void* producer(void* arg){
	while(true){
		pthread_mutex_lock(&mutex);
		//创建节点
		struct Node* newNode=(struct Node*)malloc(sizeof(struct Node));
		newNode->number=rand()%1000;
		newNode->next=head;//头插法
		head=newNode;
		printf("生产者, id: %ld, number: %ld\n",pthread_self(),newNode->number);
		pthread_mutex_unlock(&mutex);
		pthread_cond_broadcast(&cond);//解除阻塞,唤醒所有线程,但只有一个获得
		sleep(rand()%3);//秒
	}
	return NULL;
}

//消费者
void* consumer(void* arg){
	//每次从头部取出
	while(true){
		pthread_mutex_lock(&mutex);
		while(head==NULL){
			//阻塞消费者线程,并解锁,避免出现死锁,当唤醒一个之后才上锁,访问临界区资源
			pthread_cond_wait(&cond,&mutex);
		}
		struct Node* node=head;
		printf("消费者, id: %ld, number: %ld\n",pthread_self(),node->number);
		head=head->next;
		free(node);
		pthread_mutex_unlock(&mutex);
		sleep(rand()%3);
	}
	return NULL;
}


int main(int argc,char* argv[]){
	pthread_mutex_init(&mutex,NULL);//初始化
	pthread_cond_init(&cond,NULL);

	pthread_t t1[5],t2[5];//生产者,消费者
	for(int i=0;i<5;++i){
		pthread_create(&t1[i],NULL,producer,NULL);
	}
	for(int i=0;i<5;++i){
		pthread_create(&t2[i],NULL,consumer,NULL);
	}

	//线程资源的释放
	for(int i=0;i<5;++i){
		pthread_join(t1[i],NULL);
		pthread_join(t2[i],NULL);
	}

	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
	return 0;
}

分析一下pthread_cond_wait(&cond,&mutex)这段代码的前后:

这段代码前面:

  • 任务队列也就是链表节点已经没有节点可以消费了,消费者线程需要阻塞,线程加互斥锁成功,但是线程被阻塞在这一行代码上,锁还没解开,其它线程在访问这把锁的时候也会被阻塞,生产者也会被阻塞,这就形成了死锁,因此这个函数会将线程拥有的锁解开;

这段代码后面:

  • 当消费者解除阻塞之后(唤醒所有的线程),生产者中阻塞的线程会通过竞争方式获得锁,这时候其中一个线程会重新获得这把锁。

为什么用while(head==NULL)而不用if(head==NULL),

如果使用if会存在bug:

  • 当任务队列为空时,所有的消费者线程都会被这个线程阻塞,pthread_cond_wait(&cond, &mutex);
  • 当生产者生产一个节点后, 调用 pthread_cond_broadcast(&cond)会唤醒所有阻塞的线程,有一个消费者线程通过pthread_cond_wait()加锁成功,其余没有加锁成功的线程继续阻塞,加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁;
  • 没有加锁成功的线程解除阻塞继续抢占这把锁,另外一个消费者线程加锁成功,但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误;

因此为了避免这种情况将if改为while!

6、信号量

(1)信号量函数

信号量是在多线程多任务同步的,一个线程完成了某个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某些资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

信号量(信号灯)与互斥锁和条件变量的主要不同在于“灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量和条件变量一样处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。

#include <semaphore.h>
sem_t sem;

Linux 提供的信号量操作函数原型如下:

#include <semaphore.h>
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数            
int sem_destroy(sem_t *sem);
  • sem:信号量变量地址
  • pshared:0:线程同步;非 0:进程同步
  • value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
//参数sem就是sem_init()的第一个参数
//函数被调用sem中的资源会被消耗1个,资源数-1
int sem_wait(sem_t *sem);
  • 当线程调用这个函数的时候,并且sem中的资源数大于0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时,资源耗尽,因此线程也被阻塞了。
// 参数 sem 就是 sem_init() 的第一个参数  
// 函数被调用sem中的资源就会被消耗1个, 资源数-1

int sem_trywait(sem_t* sem);
  • 当线程调用这个函数,并且sem中的资源数大于0,线程不会阻塞,线程会占用一个资源,因此资源数-1,直到sem中的资源数减到0时,资源被耗尽,但是线程不会被阻塞,直接返回错误码,因此可以在程序中添加判断分支,用于处理获取资源失败后的情况。
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之
//后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。

//调用该函数给sem的资源数+1
int sem_post(sem_t* sem);

调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。

// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);

通过这个函数可以查看 sem 中现在拥有的资源个数,通过第二个参数 sval 将数据传出,也就是说第二个参数的作用和返回值是一样的。

(2)生产者消费者模型

由于生产者和消费者是两类线程,并且在还没有生成之前是不能进行消费的,在使用信号量处理这类问题的时候可以定义两个信号量,分别用于记录生产者和消费者线程拥有的总资源数。

// 生产者线程 
sem_t psem;
// 消费者线程
sem_t csem;

// 信号量初始化
sem_init(&psem, 0, 5);    // 5个生产者可以同时生产
sem_init(&csem, 0, 0);    // 消费者线程没有资源, 因此不能消费

// 生产者线程
// 在生产之前, 从信号量中取出一个资源
sem_wait(&psem);	
// 生产者商品代码, 有商品了, 放到任务队列
......	 
......
......
// 通知消费者消费,给消费者信号量添加资源,让消费者解除阻塞
sem_post(&csem);
	



// 消费者线程
// 消费者需要等待生产, 默认启动之后应该阻塞
sem_wait(&csem);
// 开始消费
......
......
......
// 消费完成, 通过生产者生产,给生产者信号量添加资源
sem_post(&psem);
  •  总资源数为 1

如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。资源数为1的情况下,不需要加锁!

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
using namespace std;

//生产者的信号量
sem_t semp;
//消费者的信号量
sem_t semc;

pthread_mutex_t mutex;

//链表的节点类型
struct Node{
	int number;
	struct Node* next;
};

//链表的头节点
struct Node* head=NULL;

//生产者
void* producer(void* arg){
	while(true){
		sem_wait(&semp);
		//创建节点
		struct Node* newNode=(struct Node*)malloc(sizeof(struct Node));
		newNode->number=rand()%1000;
		newNode->next=head;//头插法
		head=newNode;
		printf("生产者, id: %ld, number: %ld\n",pthread_self(),newNode->number);
		sem_post(&semc);//通知消费者
		sleep(rand()%3);//秒
	}
	return NULL;
}

//消费者
void* consumer(void* arg){
	//每次从头部取出
	while(true){
		sem_wait(&semc);
		struct Node* node=head;
		printf("消费者, id: %ld, number: %ld\n",pthread_self(),node->number);
		head=head->next;
		free(node);
		sem_post(&semp);//通知消费者
		sleep(rand()%3);
	}
	return NULL;
}


int main(int argc,char* argv[]){
	//生产者
	sem_init(&semp,0,1);
	//消费者->资源数量初始化为0,消费者线程启动就阻塞了
	sem_init(&semc,0,0);

	pthread_mutex_init(&mutex,NULL);//初始化

	pthread_t t1[5],t2[5];//生产者,消费者
	for(int i=0;i<5;++i){
		pthread_create(&t1[i],NULL,producer,NULL);
	}
	for(int i=0;i<5;++i){
		pthread_create(&t2[i],NULL,consumer,NULL);
	}

	//线程资源的释放
	for(int i=0;i<5;++i){
		pthread_join(t1[i],NULL);
		pthread_join(t2[i],NULL);
	}

	pthread_mutex_destroy(&mutex);
	sem_destroy(&semp);
	sem_destroy(&semc);
	return 0;
}

通过测试代码可以得到如下结论:如果生产者和消费者使用的信号量总资源数为 1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。

  • 总资源数大于 1

如果生产者和消费者线程使用的信号量对应的总资源数为大于 1,这种场景下出现的情况就比较多了:

  • 多个生产者线程同时生产
  • 多个消费者同时消费
  • 生产者线程和消费者线程同时生产和消费

以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,处理代码如下:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
using namespace std;

//生产者的信号量
sem_t semp;
//消费者的信号量
sem_t semc;

pthread_mutex_t mutex;

//链表的节点类型
struct Node{
	int number;
	struct Node* next;
};

//链表的头节点
struct Node* head=NULL;

//生产者
void* producer(void* arg){
	while(true){
		sem_wait(&semp);
		pthread_mutex_lock(&mutex);//在sem_wait下面才不会出现死锁
		//创建节点
		struct Node* newNode=(struct Node*)malloc(sizeof(struct Node));
		newNode->number=rand()%1000;
		newNode->next=head;//头插法
		head=newNode;
		printf("生产者, id: %ld, number: %ld\n",pthread_self(),newNode->number);
		pthread_mutex_unlock(&mutex);
		sem_post(&semc);//通知消费者
		sleep(rand()%3);//秒
	}
	return NULL;
}

//消费者
void* consumer(void* arg){
	//每次从头部取出
	while(true){
		sem_wait(&semc);
		pthread_mutex_lock(&mutex);//在sem_wait下面才不会出现死锁
		struct Node* node=head;
		printf("消费者, id: %ld, number: %ld\n",pthread_self(),node->number);
		head=head->next;
		free(node);
		pthread_mutex_unlock(&mutex);
		sem_post(&semp);//通知消费者
		sleep(rand()%3);
	}
	return NULL;
}


int main(int argc,char* argv[]){
	//生产者
	sem_init(&semp,0,5);
	//消费者->资源数量初始化为0,消费者线程启动就阻塞了
	sem_init(&semc,0,0);

	pthread_mutex_init(&mutex,NULL);//初始化

	pthread_t t1[5],t2[5];//生产者,消费者
	for(int i=0;i<5;++i){
		pthread_create(&t1[i],NULL,producer,NULL);
	}
	for(int i=0;i<5;++i){
		pthread_create(&t2[i],NULL,consumer,NULL);
	}

	//线程资源的释放
	for(int i=0;i<5;++i){
		pthread_join(t1[i],NULL);
		pthread_join(t2[i],NULL);
	}

	pthread_mutex_destroy(&mutex);
	sem_destroy(&semp);
	sem_destroy(&semc);
	return 0;
}

在上面的代码中,初始化状态下消费者线程没有任务信号量资源,假设某一个消费者线程先运行,调用 pthread_mutex_lock(&mutex); 对互斥锁加锁成功,然后调用 sem_wait(&csem); 由于没有资源,因此被阻塞了。其余的消费者线程由于没有抢到互斥锁,因此被阻塞在互斥锁上。对应生产者线程第一步操作也是调用 pthread_mutex_lock(&mutex);,但是这时候互斥锁已经被消费者线程锁上了,所有生产者都被阻塞,到此为止,多余的线程都被阻塞了,程序产生了死锁。

7、补充C++11的多线程编程

(1)面试题:两个线程交替打印A、B?

#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <atomic>

using namespace std;

condition_variable cond;
mutex mtx;
atomic_int k{0};

void funcA(){
	for(int i=0;i<10;++i){
		///条件变量只能和unique_lock才可以配合使用,lock_guard不行
		unique_lock<std::mutex> locker(mtx);
		cond.wait(locker,[](){
			return k%2==0;
		});
		k++;
		cout<<this_thread::get_id()<<" "<<"A"<<endl;
		cond.notify_one();
	}
}

void funcB(){
	for(int i=0;i<10;++i){
		///条件变量只能和unique_lock才可以配合使用,lock_guard不行
		unique_lock<std::mutex> locker(mtx);
		cond.wait(locker,[](){
			return k%2==1;
		});
		k++;
		cout<<this_thread::get_id()<<" "<<"B"<<endl;
		cond.notify_one();
	}
}


int main(int argc,char* argv[]){
	thread t1(funcA);
	thread t2(funcB);
	t1.join();
	t2.join();
	return 0;
}

C++线程thread是一种包装类,可以直接调用借口,注意这里面只涉及两个线程,因此可以通过条件变量唤醒等待的线程,此外里面通过lambda表达式判断是否满足条件从而实现交替打印!

若三个线程交替打印A、B、C?又如何实现呢?

#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <atomic>

using namespace std;

condition_variable cond;
mutex mtx;
atomic_int k{0};

void funcA(){
	for(int i=0;i<10;++i){
		///条件变量只能和unique_lock才可以配合使用,lock_guard不行
		unique_lock<std::mutex> locker(mtx);
		cond.wait(locker,[](){
			return k%3==0;
		});
		k++;
		cout<<this_thread::get_id()<<" "<<"A"<<endl;
		cond.notify_all();
	}
}

void funcB(){
	for(int i=0;i<10;++i){
		///条件变量只能和unique_lock才可以配合使用,lock_guard不行
		unique_lock<std::mutex> locker(mtx);
		cond.wait(locker,[](){
			return k%3==1;
		});
		k++;
		cout<<this_thread::get_id()<<" "<<"B"<<endl;
		cond.notify_all();
	}
}

void funcC(){
	for(int i=0;i<10;++i){
		///条件变量只能和unique_lock才可以配合使用,lock_guard不行
		unique_lock<std::mutex> locker(mtx);
		cond.wait(locker,[](){
			return k%3==2;
		});
		k++;
		cout<<this_thread::get_id()<<" "<<"C"<<endl;
		cond.notify_all();
	}
}
int main(int argc,char* argv[]){
	thread t1(funcA);
	thread t2(funcB);
	thread t3(funcC);
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

需要注意的是,条件变量唤醒应当为notify_all,因为若使用notify_one,等待的线程有两个,若有一个线程竞争到锁,当不满足条件,那么条件变量就会将其释放锁,让另外一个竞争锁,不然会造成死锁!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C、C++多线程编程 的相关文章

  • 算法导论->算法基础->2.1插入排序 (从小到大)

    1 伪代码 2 执行过程图 3 c语言实现完整代码 include lt stdio h gt include lt malloc h gt typedef struct MyArray int pbase int length MyArr
  • hihocoder图像算子(高斯消元)

    描述 在图像处理的技术中 xff0c 经常会用到算子与图像进行卷积运算 xff0c 从而达到平滑图像或是查找边界的效果 假设原图为H W的矩阵A xff0c 算子矩阵为D D的矩阵Op xff0c 则处理后的矩阵B大小为 H D 43 1
  • subs()函数()

    摘自matlab Utilities for obsolete MUTOOLS commands subs 既是目录也是函数 subs Symbolic substitution subs S OLD NEW replaces OLD wi
  • IEEE802

    IEEE 官方最新标准 xff1a Browse Standards Get Program IEEE Xplore https ieeexplore ieee org browse standards get program page s
  • 笔记本电脑3C认证要求的相关介绍

    作为CCC强制目录中的产品 xff0c 便携式计算机如果想在国内销售 xff0c 是必须要进行3C认证的 便携式计算机是什么 也就是笔记本电脑 xff0c 平板电脑等 官方的定义是以便携性为特点 xff0c 内置了输入输出设备 电池模块的微
  • 普通门禁卡及各类复制卡相关知识

    转自 xff1a https nfctool cn 42 本文带你了解M1卡的数据结构 xff0c 为以后的破解提供理论基础 同时带你了解各种IC卡 xff0c 让你对破解和复制有更清晰的目标 请注意 xff0c ID卡没有密码 xff0c
  • 用XDS510-V4专业版仿真器连接CCS3.3与28335问题记录

    今天用仿真器连接28335一直没连上 xff0c 错误有 xff1a 1 xff0c 断开仿真器用ccs3 3连接的时候显示为 xff08 不接仿真器 xff0c 空连接 xff09 Error connecting to the targ
  • NVIDIA Jetson TX2 通过vnc 桌面控制

    1 安装Xrdp Windows远程桌面使用的是RDP协议 xff0c 所以ubuntu上就要先安装Xrdp xff0c 在ubuntu软件中心搜索xrdp安装 安装xrdp的同时会自动安装vnc4server xbase clients组
  • NVIDIA Jetson TX2 查看系统参数状态

    1 xff0c 查看Jetson TX2 L4T版本 xff1a head n 1 etc nv tegra release 在刷 JetPack 3 0之前 和刷之后 版本参数发生细微的变化 xff1a REVISION xff1a 由
  • 解决 ImportError: No module named 'serial' 问题

    在pycharm里编写Python串口程序的时候 xff0c 编译时提示 ImportError No module named 39 serial 39 解决办法 xff1a 安装 serial module 这里区分python2和 p
  • 查看ubuntu下Qt的版本

    1 xff0c 查看ubuntu下Qt的版本 打开命令行输入 xff1a span style font size 14px qmake v span
  • 运算放大器基本运算

    转自 xff1a http www 21ic com jichuzhishi analog amplifier 2014 11 11 606654 html 运算放大器组成的电路五花八门 xff0c 令人眼花瞭乱 xff0c 是模拟电路中学
  • KEIL 注解和去注解 快捷键添加

    KEIL 注解和去注解 快捷键添加方法 xff1a 菜单栏Edit gt Configuration gt Shortcut Keys 1 例如设置 注解快捷键 xff1a Ctrl 43 2 例如设置 去注解快捷键 xff1a Ctrl
  • git、vscode免密登录

    1 git配置 git config global list 查看当前配置 git config global user name 34 xiaoyaozi 34 git config global user name 34 xiaoyao
  • 555 单稳态电路

    555 定时器成本低 xff0c 性能可靠 xff0c 只需要外接几个电阻 电容 xff0c 就可以实现多谐振荡器 单稳态 触发器及施密特触发器等脉冲产生与变换电路 它内部包括两个电压比较器 xff0c 三个5K欧姆的等值串联分压电阻 xf
  • Allegro 铺铜设置

    软件版本 xff1a Allegro16 6 敷铜 xff1a 放置禁止敷铜区域 xff1a Setup Areas Route Keepout 1 标题栏选Shap gt Global Dynamic Params Shape Polyg
  • OVP 过压保护电路

    过压保护电路 OVP 为下游电路提供保护 xff0c 使其免受过高电压的损坏 OVP电路监测外部电源 如 xff1a 离线电源或电池 的直流电压 xff0c 通过下述两种方式中的一种保护后续电路 xff1a 撬棍钳位电路或串联开关 撬棍电路
  • 超全蓝牙芯片原厂总结(含芯片型号)

    转自 xff1a https blog csdn net weixin 42583147 article details 80923946 作者 xff1a XCODER 蓝牙芯片原厂 1 CSR 高通 xff08 被高通收购 xff09
  • ST-Link的internal command error问题的解决方法

    问题 xff1a 显示 xff1a internal command error 这是由于stlink无法识别到芯片的情况 xff0c 通过解决这个问题我找到几个原因和解决方法 xff1a 1 xff0c 芯片睡眠 xff0c 停机 xff
  • 蓝牙 UUID 解释

    一 xff0c 什么是 UUID UUID 可以简单理解为编号 xff0c 唯一的编号 xff0c 用于区分不同的个体 服务和特性都有各自的UUID 比如经典的9527 UUID 就跟身份证一样 xff0c 不管是你是局长还是科长 xff0

随机推荐

  • 【人工智能】传教士和野人问题(M-C问题)

    摘要 本题需要解决的是一般情况下的传教士和野人问题 xff08 M C问题 xff09 通过对问题的一般化 xff0c 我们用一个三元组定义了问题的状态空间 xff0c 并根据约束条件制定了一系列的操作规则 xff0c 最后通过两个启发式函
  • 【算法设计与数据结构】为何程序员喜欢将INF设置为0x3f3f3f3f?

    在算法竞赛中 xff0c 我们常常需要用到一个 无穷大 的值 xff0c 对于我来说 xff0c 大多数时间我会根据具体问题取一个99999999之类的数 xff08 显得很不专业啊 xff01 xff09 在网上看别人代码的时候 xff0
  • 【slighttpd】基于lighttpd架构的Server项目实战(7)—http-parser

    对于http服务器 xff0c http request的解析是比较麻烦的 xff0c 由于我们的重点并不在这上面 xff0c 所以这一部分不打算自己编写 xff0c 而是使用开源的http parser库 xff0c 下面我们将使用该库来
  • select和epoll 原理概述&优缺点比较

    这个问题在面试跟网络编程相关的岗位的时候基本都会被问到 xff0c 刚刚看到一个很好的比喻 xff1a 就像收本子的班长 xff0c 以前得一个个学生地去问有没有本子 xff0c 如果没有 xff0c 它还得等待一段时间而后又继续问 xff
  • 笔记-关于神经网络黑盒模型可解释性,可视化

    原博地址 xff1a 深度学习黑盒可视化指南 xff0c 从隐藏层开始 摘 xff1a 一旦神经网络接收到相当大的所需数据集后 xff0c 该网络就会使用其精确的知识 权重 来证明或识别未知数据样本上的模式 即在经过大量数据集训练以后 xf
  • C++11 多线程 future/promise简介

    1 lt future gt 头文件简介 Classes std future std future error std packaged task std promise std shared futureFunctions std as
  • C++异步调用利器future/promise实现原理

    前言 在异步编程中 xff0c 各种回调将让人眼花缭乱 xff0c 代码分散 xff0c 维护起来十分困难 boost和C 43 43 11 的 future promise 提供了一个很好的解决方案 xff0c 使得代码更加漂亮 易维护
  • 【Heydrones】飞手百科第一篇:一定要看的无人机原理总结

    飞手百科 知识是最好的保险 本文目录 1 xff0c 无人机的飞行原理 2 xff0c 无人机的几大系统 3 xff0c 无人机的外观介绍 4 xff0c 无人机的专业术语 xff08 一 xff09 无人机的飞行原理 旋翼和轮子一样 xf
  • 【Tars】腾讯微服务框架Tars介绍

    目录 1 介绍2 设计思路3 整体架构4 平台特性 1 介绍 Tars是 基于名字服务 使用Tars协议 的高性能 RPC 开发框架 xff0c 同时配套一体化的 服务治理平台 xff0c 帮助个人或者企业快速的以微服务的方式构建自己稳定可
  • C++11常用新特性快速一览

    最近工作中 xff0c 遇到一些问题 xff0c 使用C 43 43 11实现起来会更加方便 xff0c 而线上的生产环境还不支持C 43 43 11 xff0c 于是决定新年开工后 xff0c 在组内把C 43 43 11推广开来 xff
  • 语法糖:萃取lambda表达式

    背景 现在手头主负责的服务代码 xff0c 基本上都用C 43 43 11来开发了 xff0c 异步编程使用的是TAF的future promise future的then函数 xff0c 接受的是一个Callback对象 xff0c 该对
  • hashmap C++实现分析

    一 简介 Map 是 Key Value 对映射的抽象接口 xff0c 该映射不包括重复的键 xff0c 即一个键对应一个值 在HashMap中 xff0c 其会根据hash算法来计算key value的存储位置并进行快速存取 本文介绍的C
  • SpringMVC(04) -- SpringMVC获取请求参数

    SpringMVC学习笔记 源码地址 4 1 xff09 通过ServletAPI获取 将HttpServletRequest作为控制器方法的形参 xff0c 此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的
  • docker删除所有容器/镜像

    1 想要删除容器 xff0c 则要先停止所有容器 xff08 当然 xff0c 也可以加 f强制删除 xff0c 但是不推荐 xff09 xff1a docker stop docker ps a q 2 删除所有容器 docker rm
  • php中常见的几种设计模式

    1 单例模式 单例模式可以说是面向对象语言里最常用 也是最简单的一种模式 单例就是单个实例 xff0c 单个对象的意思 xff0c 就是说我们去实例化一个类的时候 xff0c 不管调用多少次 xff0c 都永远只有一个实例 不会有多个 xf
  • Ubuntu查看文件大小或文件夹大小

    一 查看文件大小 查看文件大小的命令 xff1a ls l filename 会在终端输出 xff1a rw r r 1 root root 2147483648 Mar 5 09 39 filetemp0606 其中数字214748364
  • Git 遇到了 The remote end hung up unexpectedly -- early EOF -- index-pack failed 问题

    Git 遇到了 The remote end hung up unexpectedly early EOF index pack failed 问题 今天想 clone 在 gitlab 的 repo xff0c 结果在 clone 的过程
  • 【Docker】在Docker容器中运行VScode

    原文链接 xff1a 容器中的远程开发 Prerequisites VScodeDocker Desktop Steps 打开Docker xff1a 在Windows下出现鲸鱼图标且图标静止则打开成功 xff1b 检查Docker xff
  • github 常用命令汇总 更新代码和子模块的代码

    针对PX4代码 xff0c 在github库中建立了Firmware的分支 xff0c ADRC branch xff0c 用于存放修改的代码 xff0c 其中涉及了子模块ecl的修改 代码下载 xff1a git clone https
  • C、C++多线程编程

    本文的笔记来自于b站视频的爱编程的大丙 xff0c 博客链接 xff1a https subingwen cn xff0c 有做了相应的补充 xff01 一 线程概述 进程对应的虚拟地址空间的各个分区如图 xff1a 每个进程的虚拟地址空间