多线程

7 篇文章 0 订阅
  • 进程是包含程序指令和相关资源的集合,进程是CPU资源分配的最小单元
  • 线程是CPU调度的最小单元
  • 每次进程切换,都存在进程资源的保存和恢复动作,称为上下文切换
  •  对每个进程来说,系统资源看起来都是独占的
  • 同一个进程内部有多个线程,共享的是同一个进程的所有资源(线程共享了哪些资源????),如内存空间等。通过线程可以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,且并发任务间通信也更简单
同一进程间的线程究竟共享哪些资源呢,而又各自独享哪些资源呢?
共享的资源有
a. 堆  由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的)
b. 全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的
c. 静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的
d. 文件等公用资源  这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
CPU
独享的资源有
a. 栈 栈是独享的
b. 寄存器  这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC
c.线程ID
d.错误返回码
e.线程优先级
f.线程的信号屏蔽码

  • 线程的切换是轻量级的,所以可以保证足够快。每当有事件发生状态改变,都能有线程及时相应,且每次线程内部处理的计算强度和复杂度都不大。多线程的实现的模型也是高效的
一、多线程是什么
  • 一个程序的运行过程中,只有一个控制权存在。当函数被调用时,该函数获得控制权,称为激活函数,然后运行该函数中的指令
  • 多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。(即使是单核CPU的计算机,也能通过不停的在不同线程的指令间切换,造成多个线程同时运行的结果)
  • 多线程与栈密切相关
  • 在程序运行中,如果要中断一个线程/进程去执行另一个,必须保存现场的参数和变量等信息。这些都保存在栈中,执行完一个再去栈中取挂起的那个。栈只有最顶部的帧才能够被读取,只有该帧对应的那个函数会被激活,同一时间一个栈只能对应一个线程。所以多线程必定存在多个栈。每创建一个新的线程时,就需要为这个新线程创建一个新的栈
  • 多线程的进程在内存中有多个栈,多个栈之间以一定的空白区域隔开,已备栈的增长。每个线程可以调用自己栈最顶部的帧中的参数和变量,并且与其他线程共享内存中的Text、heap和global data区域。
二、多线程的创建与结束
1.线程的创建
  • 多线程函数需要包括的头文件:#include <pthread.h>
  • 多线程的相关函数都是创建成功返回0,创建不成功返回错误码
  • 线程创建函数:pthread_create
//参数
// thread:指向线程标识符的指针。函数运行成功时,thread指向的内存单元将被设置为新创建的线程的线程ID
//attr:用于设置不同的线程属性。
// start_routine:线程运行函数的入口地址。新创建的线程从start_routine开始运行,该函数只有一个万能指针参数arg
//arg:线程运行函数的参数。如果向start_routine传递的参数不止一个,那么需要把这些参数放置在一个结构体中,把这个结构体的指针作为start_routine的参数传入
//返回值:如果创建线程成功,那么返回0,创建失败则返回出错编号
int pthread_create( pthread_t *thread,const pthread_attr_t *attr, void *(*start_routine)(void *),void *arg);
//pthread_t:typedef unsigned long int pthread_t  即pthread_t是一个无符号长整形( 在unix系统中,pthread_t的类型是不确定的
  • 等待一个线程的结束函数:pthread_join
//该函数是一个线程阻塞函数,调用它的函数将会一直等待知道被等待的线程结束为止,当函数返回时,被等待的线程的资源被回收
//参数
// thread:被等待的线程标识符
//retval:用户定义的指针,存储被等待线程的返回值
int pthread_join(pthread_t thread,void **retval);
  • 一个线程的结束有两种途径:一是函数已经结束,调用它的线程也就结束了;二是通过函数pthread_exit来实现
//参数:
// retval:函数的返回代码
void pthread_exit(void *retval);
  • pthread_exit和pthread_join函数的区别:
1.pthread_join一般是主线程来进行调用,用来等待子线程退出。因为是等待,所以是阻塞的,一般主线程会依次添加所有它创建的子线程
2.pthread_exit一般是子线程调用的,用来结束当前线程
3. 子线程可以通过pthread_exit传递一个返回值,而主线程可以通过pthread_join来获得该返回值,从来判断该子线程的退出是正常还是异常
  • 在线程中调用pthread_exit以便主线程能够接收到返回值
  • 如果线程调用的函数是在一个类中的时候,应该把该函数写成静态成员函数         P303
2.向线程传递参数
  • P304
  • 如果要传递一个以上的参数,应该把参数放置到一个结构体内部,然后传递这个结构体
  • 注意:可以给void *指针的函数参数传递一个其他类型的指针,而在提取void *指针中的数据时,需要先把void *转换称为需要提取数据的数据类型
  • 甚至可以直接给void *传递一个int数,只不过需要进行强制转换:(void *)int。也可以直接把void *作为int使用,同样使用强制转换,其他类型long等也一样
3.获得线程id
  • 有两种方法可以打印线程的ID:
1.在线程调用函数中可以使用pthread_self函数来获得线程id
2.在创建函数时生成的id, 即创建函数的第一个参数
  • 在线程调用函数中使用pthread_self函数来获得线程id
void *funtion(void *arg){
    unsigned int tmp = pthread_self();            //调用pthread_self函数获得线程id
}
  • 使用pthread_self和pthread_create获得的线程id是一样的
三、线程的属性
  • 线程的属性在创建线程时被指定,该属性被封装在一个对象中,该对象可以用来设置一个或者一组线程的属性。
  • 线程属性对象的类型为pthread_attr_t,该类型被包含在pthread.h头文件中
  • 线程属性对象不能直接设置,必须通过相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在函数pthread_create之前调用,之后必须调用pthread_attr_destroy来释放资源
  • 线程的主要属性:作用域、栈尺寸、栈地址、优先级、分离的状态、调度策略和参数等。默认的属性为非绑定、非分离、1MB大小的堆栈、与父进程一样的优先级别
  • 使用函数pthread_attr_init函数进行初始化:
typedef struct{
    int etachstate;                    //线程的分离状态
    int schedpolicy;                   //线程调度策略
    structsched_param schedparam;      //线程的调度参数
    int inheritsched;                  //线程的继承性
    int scope;                         //线程的作用域
    size_t guardsize;                  //线程栈末尾的警戒缓冲区大小
    int stackaddr_set;                 //线程的栈设置
    void *stackaddr;                   //线程栈的位置
    size_t stacksize;                  //线程栈的大小
}pthread_attr_t;

pthread_attr_t attr;
pthread_attr_init(&attr);              //注意pthread_attr_init的参数是一个pthread_attr_t的指针
//初始化之后再调用其他的函数来设置或者查看每一个属性
  • POSIX.1指定了一系列方法获取和设置pthread_attr_t结构里面的各个属性:
1.分离状态detached state:如果线程终止的时候,线程处于分离状态,那么系统将不会保留线程的终止状态    P310例子,如果线程处于分离状态,那么得不到线程结束状态信息,pthread_join函数会出错
情况一:不需要线程的终止状态,使用函数pthread_detach来分离线程
情况二:在线程创建的时候,指定线程处于分离状态,那么线程一开始就处于分离状态
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *state);
int pthread_attr_setdetachstate(pthread_attr_t *attr,int state);
2.栈地址stack address:
int pthread_attr_setstackaddr(pthread_attr_t *attr,void *addr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr,void **addr);
3.栈大小stack size:当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用;而当线程调用的函数需要分配很大的局部变量或者函数调用层次很深时,可能需要增大线程的默认大小
int pthread_attr_getstacksize(const pthread_attr_t *attr,size_t *size);
int  pthread_attr_setstacksize(pthread_attr_t *attr,size_t size);
还有两个函数getstack和setstack可以同时操作栈地址和栈大小两个属性
4.栈保护区大小stack guard size:在线程栈顶预留出一段空间,防止栈溢出。该属性默认是PAGESIZE大小,被设置后系统会自动将其补齐为页大小的整数倍
int pthread_attr_getguardsize(const pthread_attr_t *attr,size_t *guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);
5.线程优先级priority:schedparam新线程的优先级为0
6.继承父进程优先级inheritsched:新线程不会继承父线程的调度优先级
7.调度策略schedpolicy:新线程使用SCHED_OTHER调度策略。线程一旦开始运行,知道被抢占或者知道线程阻塞或停止为止
8.争用范围scope:
分两种PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)
PTHREAD_SCOPE_SYSTEM:此线程将与系统中的所有线程进行竞争
PTHREAD_SCOPE_PROCESS:此线程将与进程中的其他线程进行竞争
9.线程并行级别concurrency:????????????
四、多线程同步
  • 多线程相当于一个并发系统,可以同时执行多个任务
  • 但是多个任务共享资源,就要考虑到对某个变量同时写入的问题,即多线程的同步
1.多线程同步问题            P312
  • 同一线程内部,指令按照先后顺序执行。但是不同的线程之间的指令,却很难说清楚哪一条会先执行(即可能执行了线程一的第一条语句,又跳转去执行了线程二的指令)。如果程序的运行结果依赖于不同线程的执行先后的话,那么就会造成竞争条件。应该尽量避免竞争条件的形成。
  • 最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分割的一个原子操作,而其他任务不能插入到原子操作中来。
  • 多线程的同步是指在一定的时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源。可以通过互斥锁(mutex)、条件变量(condition variable)、读写锁(reader-writer lock)和信号量(semphore)来同步资源。
2.互斥锁              P314
  • 互斥量是线程程序必须的工具
  • 互斥锁(mutex)是一个特殊的变量,它有锁上(lock)和打开(unlock)两种状态。
  • 互斥锁一般设置称为全局变量
  • 打开的互斥锁可以由某个线程获得,一旦获得,这个互斥锁就会锁上,此后只有该线程有权打开,其他想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。
  • 在锁上和打开操作之间的代码构成了一个原子操作
//声明一个互斥锁,注意互斥锁一般声明为全局变量
//锁的创建有两种方式:静态和动态
pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER    //静态的方式
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
//互斥锁的常用函数
pthread_mutex_init()            //锁的初始化
pthread_mutex_destory()         //锁的销毁
int pthread_mutex_lock(pthread_mutex_t *mutex)            //上锁,失败会挂起等待
int pthread_mutex_unlock(pthread_mutex_t *mutex)          //开锁
int pthread_mutex_trylock(pthread_mutex_t *mutex)         //测试加锁,失败不会挂起等待,而是返回EBUSY,例子P316
3.条件变量              P317
  • 线程正在等待共享数据内某个条件出现,它会不停的上锁/解锁直到检查到该共享数据出现。这种频繁查询的效率非常低
  • 条件变量的功能:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就唤醒因等待满足特定条件而失眠的线程。这样就不会占用宝贵的互斥对象锁。
  • 条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用
  • 条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将会重新锁定互斥锁并重新测试条件是否满足
  • 条件变量的创建:有静态和动态两种方式
//静态方式
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
//动态方式使用pthread_cond_init函数
//参数
//cond:条件变量
//cond_attr:条件变量初始化的属性d
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
  • 注销:只有在没有线程在该条件变量上等待时才能注销这个条件变量
//使用函数
int pthread_cond_destroy(pthread_cond_t *cond);
  • 等待:有两种等待方式,条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。
  • abstime以与time()系统调用相同意义的绝对时间形式出现,1970年1月1日
  • 无论哪种等待方式,都需要和一个互斥锁相互配合,以防多个线程同时请求pthread_cond_wait()/pthread_cond_timedwait()的竞争条件
  • mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),而且在调用等待函数前必须由本线程加锁,而在更新条件等待队列以前,mutex必须保持锁定状态,并且在线程挂起之前进行解锁在条件满足从而离开等待之前,mutex将被重新加锁,与进入等待之前的加锁动作相对应。
//等待的函数
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);    //条件等待
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime)        //计时等待
  • 激发:激发也有两种形式:pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按照入队顺序激活其中一个;pthread_cond_broadcast()则激活所有等待线程
  • pthread_cond_signal函数的作用是发送一个信号给另一个正处在阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处于阻塞状态,pthread_cond_signal也会成功返回
  • 一个pthread_cond_signal调用最多发一次信号。使用pthread_cond_signal不会出现“惊群现象”(每当有资源可用,所有进程/线程都来竞争资源),因为它最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么根据各等待线程优先级的高低确定哪个线程会接收到信号并开始继续执行;如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。
  • P319 条件变量的例子
  • 如果先运行了激发的线程,而等待阻塞的线程后运行,会造成错误。可以增加一个计数器记录等待线程的个数,在决定出发条件变量前检查该变量即可。
4.读写锁              P326
  • 读写锁的类型为pthread_rwlock_t
  • 读写锁的属性类型为pthread_rwlockattr_t
  • 在程序中可能存在读者写者的问题,其中写操作具有排他性,是独占的;而读操作是共享的,也就是可以有多个线程同时去访问某个资源
  • 读写锁比起互斥锁具有更高的适用性与并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁
1.当读写锁是写加锁状态,在这个锁被解锁之前,所有视图对这个锁加锁的线程都会被阻塞
2.当读写锁是读加锁状态,所有试图以读模式对它进行加锁的线程都可以得到访问权限,但是 以写模式对它进行加锁的线程将会被阻塞
3. 当读写锁是读加锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求会长期阻塞
  • 读写锁也可以叫做共享-独占锁
  • 读写锁最适用于对数据结构的读操作次数多于写操作次数的场合
  • 处理读者-写者问题的两种常见策略:
1.强读者同步:总是给读者更高的优先权,只有写者当前没有进行写操作,那么读者就可以获得访问权限
2.强写者同步:优先权交付给写者,读者往往只能等到所有正在等待或者正在执行的写者结束之后才能执行
//航班订票系统往往采用强写者同步
//图书馆查阅系统采用强读者同步
  • 读写锁机制是由POSIX提供的,如果写者没有持有读写锁,那么所有读者都可以持有该锁,如果某个写者阻塞在上锁上,那么由POSIX系统来决定读者是否可以获得该锁
  • 初始化读写锁:静态和动态两种方法
1.静态方法:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
2.动态方法:使用pthread_rwlock_init()函数
int pthread_rwlock_init(pthread_rwlock_t *rwptr,const pthread_rwlockattr_t *attr);    //attr:属性指针
int pthread_rwlock_init(pthread_rwlock_t *rwptr);
  • 销毁读写锁
pthread_rwlock_destroy();
  • 初始化中,如果属性指针attr是一个空指针的话,表示使用默认属性。如果想使用默认属性需要使用以下两个函数
1.初始化属性指针
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
2.销毁属性指针
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
  • 在初始化读写锁完毕之后,该锁就处于一种非锁定状态。数据类型为pthread_rwlockattr_t的某个属性对象一旦初始化了,就可以通过不同的函数来启用或者禁用某个属性
  • 获取和释放读写锁
1.读加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);    //如果相应的读写锁已经被写加锁,那么就阻塞调用线程
2.写加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);    //如果相应的读写锁已经被读加锁或者写加锁,那么就阻塞该调用线程
3.读写解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);    //
//其中两个上锁操作都是阻塞操作,也就是说获取不到锁的话,那么调用线程不是立即返回,而是阻塞执行
  • 非阻塞方式获取读写锁
//非阻塞方式下获取读写锁,如果不能马上获取,就会立即返回一个EBUSY错误提示,而不是把调用线程投入到睡眠等待
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
5.信号量              P329
  • 信号量的类型为sem_t
  • 函数调用成功返回0,调用失败返回-1
  • 一般定义为全局变量
  • 线程可以通过信号量来实现通信
  • 信号量和互斥锁的区别:
互斥锁只允许一个线程进入临界区
而信号量允许多个线程同时进入临界区
  • 使用头文件#include <semaphore.h>,信号量函数的名字都以sem_开头
  • 信号量的创建
//使用 sem_init函数
//参数
//sem:信号量
// pshared:信号量的类型,如果为0,表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享
//value:sem的初始值,信号量在这个初始值的基础上进行加1和减1。一般用来表示可以同时获取信号量的线程数
int sem_init(sem_t *sem,int pshared,unsigned int value);
  • 信号量的等待函数:
//该函数以 原子操作的方式将信号量的值减1, 原子操作是指如果两个线程企图同时给一个信号量加1或者减1,它们之间不会互相干扰
int sem_wait(sem_t *sem);
  • 信号量加一函数
//该函数用于以原子操作的方式将信号量加1
int sem_post(sem_t *sem);
  • 销毁信号量函数
int sem_destroy(sem_t *sem);
五、多线程重入
  • 可重入函数:可以由多于一个任务并发使用,而不必担心数据错误的函数
  • 不可重入函数:只能由一个任务所占用,除非能确保函数的互斥(使用信号量或者在代码的关键部分禁用中断)
  • 可重入函数可以在任意时刻被中断,稍后再继续运行,且不会丢失数据。所以在使用本地变量和全局变量时保护自己的数据
  • 可重入函数的特点
1.部位连续的调用持有静态数据
2.不返回指向静态数据的指针
3.所有数据都由函数的调用者提供
4.使用本地数据,或者通过制作全局数据的本地副本来保护全局数据
5.如果必须访问全局变量,要利用互斥锁、信号量等来保护全局变量
6.绝不调用任何不可重用函数
  • 不可重入函数的特点:
1.函数中使用了静态变量,无论是全局静态变量还是局部静态变量
2.函数返回静态变量
3.函数中调用了不可重用函数
4.函数体内使用了静态的数据结构
5.函数体内调用了malloc或者free函数
6.函数体内调用了其他标准的I/O函数
  • 使用宏_REENTRANT





























  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值