多线程编程

线程创建

线程创建

多线程内存

多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果。
多线程的进程在内存中有多个栈。多个栈之间以一定的空白区域隔开,以备栈的增长。每个线程可调用自己栈最下方的帧中的参数和变量,并与其它线程共享内存中的Text,heap和global data区域。

多线程同步

互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。

1) 互斥锁

互斥锁是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。互斥锁一般被设置成全局变量。

while (1) {                /*infinite loop*/
  mutex_lock(mu);           /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/
  if (i != 0) i = i - 1;
  else {
    printf("no more tickets");
    exit();
  }
  mutex_unlock(mu);         /*release mutex, make it unblocked*/
}

主要有 pthread_mutex_init 、 pthread_mutex_destroy。可 、 pthread_
mutex_lock 和 pthread _ mutex _unlock 这几个函数,分别完成锁的初始化 、 锁的销毁 、 上锁和
释放锁操作 。 锁的创建有两种方式,静态和动态 。 例 9.10 中是以静态方式创建了锁 , 代码
如下:

//静态动态创建锁
pthread_mutex_t mutex_x= PTHREAD_MUTEX_ INITIALIZER ;
int pthread_ mutex_init (pthread_ mutex_ t
attr) ;

如果存在某个线程依然使用原先的程序,即不尝试获得 mutex_x,
而直接修改 total_ticket_num ,互斥锁不能阻止该程序修改 total_ticket_num ,互斥锁就失去
了保护资源的 意义 。 所以,互斥锁机制需要程序员自己来写出完善的程序来实现 互斥锁的
功能 。

对锁的操作主要包括加锁 pthread _ mutex _lock() 、解锁 pthread _ mutex _unlock()和测 试加
锁 pthread _ mutex _ trylock() 3 个,代码如下:

int pthread_ mutex_ lock(pthread _mutex t *mutex)//锁被占用时等待
int pthread_mut ex_ unlock(pthr ead_ mutex_ t *mutex)
int pthread_ mutex_ trylock(pth read_ mutex_t *mutex)//锁占用时返回ebusy

pthread _ mutex一trylock()语义与 pthread _ mutex _Jock() 类 似, 不 同的 是在锁已 经被占 据 时
返回 EBUSY ,而不是挂起等待 。

2) 条件变量

条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。

/*mu: global mutex, cond: global codition variable, num: global int*/
mutex_lock(mu)

num = num + 1;                      /*worker build the room*/

if (num <= 10) {                     /*worker is within the first 10 to finish*/
    cond_wait(mu, cond);            /*wait*/
    printf("drink beer");
}
else if (num = 11) {                /*workder is the 11th to finish*/
  cond_broadcast(mu, cond);         /*inform the other 9 to wake up*/
}

mutex_unlock(mu);

上面使用了条件变量。条件变量除了要和互斥锁配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。
**条件变量特别适用于多个线程等待某个条件的发生。**如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

pthread_cond_t cond=PTHREAD_COND_INITIALIZER//静态创建
int pthread_cond_init(p thread_co nd_t *cond , pthread_c ondattr_t *cond_att r)//动态创建
int pthread_cond_destroy(pthrea d_cond_t *cond)
int pthread_cond_wait(pthread_c ond_t *cond , pthread_ mutex_t *mutex)//等待条件变量
int pthread_cond_timedwait(pthr ead_cond_ t *cond, pthread_mutex_ t *mutex,
const struct timespec *abstime)//等待,超时返回
pthread _ cond _signal()//)激活一个等待该条件的线程,存在多个等待线程时按人队顺序激活其中一个
pthread _ cond _broadcast()//则激活所有等待线程

虚假唤醒

在多处理器系统上,pthread_cond_signal是很有可能唤醒多个pthread_cond_wait()的线程。也就意味着当一个线程中,pthread_cond_wait()返回的时候,不一定代表条件已经满足了,需要在程序中做额外的判断来检测是否真的已经满足条件了:
关于多处理器系统出现虚假唤醒(sprious wakeup)的原因,我的理解是因为多处理器上,多线程共享的数据需要在多核处理器上cache进行更新和拷贝的原因。

消息遗漏 ,通过在多线程下,因为单线程是原子操作

在pthread_cond_wait的实现内部,首先会解锁,然后进入block状态,解锁和进入block必须合并成一个原子操作,这样就保证了在pthread_cond_wait之后调用的pthread_cond_signal不会被以后掉。
如果在一个线程调用pthread_cond_wait的过程中但未进入block状态,此时有线程调用了pthread_cond_signal或者pthread_cond_broadcast,那么此次消息将被遗漏掉,因为没有任何线程在pthread_cond_wait的block状态。
对于多线程来说,pthread_cond_wait不能保证一定在pthread_cond_signal之后执行,也就意味着,当pthread_cond_wait进入block之后,已经错过了pthread_cond_signal。因为已经错过了pthread_cond_signal,很有可能会导致该线程永远block下去。
通常这类问题的解决办法是设置一个pthread_cond_signal或者pthread_cond_broadcast的计数器count,在调用pthread_cond_wait之前先对这个count进行判断,如果count != 0 则说明已经错过了消息,可以不用等待,直接往下执行即可:

pthread_cond_signal()和pthread_cond_wait()

虚假唤醒
典型用法,包括互斥量和条件变量顺序问题

3) 读写锁

读写锁与互斥锁非常相似。r、RW lock有三种状态: 共享读取锁(shared-read), 互斥写入锁(exclusive-write lock), 打开(unlock)。后两种状态与之前的互斥锁两种状态完全相同。
相当于读锁不独自享有资源,写锁享有
多个线程就可以同时读取共享资源

4)信号量

线程还可 以通过信号量来实 现通信。信号量 和互斥 锁的区别: 互斥锁只允许一个线程
进入临界区,而信号量允许多个线程同时进入临界区 。 要使用信号量同步,需要包含头文
件 semaphore.h 。 信号量函数的名字都以 ” sem ”打头 。 线程使用的基本信号 量函数有以下
4 个

int sem_init(sem_t *sem , int pshared , unsigned int value) ;

该函数用于初始化由 sem 指向的信号对象,设置它的共享选项,并给它一个初始的整数
值。 pshared 控制信号量的类型,如果其值为 0 ,就表示这个信号量是当前进程的局部信号量,
否则信号量就可以在多个进程之间共享。
value 为 sem 的初始值 。 调用成功时返回 0 , 失败返
回- 1 。

int sem_ wait(sem_t * sem);

该函数用于以原子操作的方式将信号量的值减 1 。 原子操作就是 ,如果两个线程企 图 同
时给一个信号量加 1 或减 l ,它们之间不会互相干扰 , 函数的原型如下:

int sem__post (sem_ t * sem );

该函数用于以原子操作的方式将信号量 的值加 1

C++ 线程优雅退出

优雅退出链接
多线程程序中, 经常会定时执行任务. 通常的做法是, 在 while 循环中执行一个 task, 然后 sleep 一段时间。
这段程序用 sleep 或 nanosleep 作为时间间隔, 并监听 SIGINT(ctrl + c) 和 SIGTERM(kill ) 两个信号. 但有以下几个问题:

sleep 和 nanosleep 无法被唤醒, 所以程序再接收到 SIGINT 或 SIGTERM 之后必须等待sleep 正常返回才能执行后面的 cleanup 代码.
如果不监听 SIGINT 或 SIGTERM, 那么系统会执行默认的 handler, 并终止程序, 这会 interrupt sleep 函数, 但也会导致 cleanup 代码被跳过.
sleep 是linux 系统调用(包含在 unistd.h 中), 其实现与平台相关的, 所以可移植性不好. 所以有些实现是用可移植的 select 函数代替 sleep. 但这同样会面临无法被唤醒的问题.

更好的实现方式是利用C++11 中的 mutex 和 condition_variable. 利用condition_variable::wait_for 实现可 interruptible 的 sleep 功能. 正常情况下 wait_for 超时, 接收到退出信号之后, 程序会立即被唤醒, 退出 while 循环, 并执行 cleanup 代码.

进程和线程关系及区别

很好的链接
1.定义

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

2.关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

3.区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  1. 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

  2. 线程的划分尺度小于进程,使得多线程程序的并发性高。

  3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

  4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

4.优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

多线程几种锁,互斥锁,条件锁,自旋锁,读写锁

参考链接
假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。

自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁

互斥量(mutex)是阻塞锁,当某线程无法获取锁时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放锁后,操作系统会激活那个被挂起的线程,让其投入运行

对于自旋锁来说,自旋锁使线程处于用户态,而互斥锁需要重新分配,进入到内核态。
自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
为什么要使用自旋锁
互斥锁有一个缺点,他的执行流程是这样的 托管代码 - 用户态代码 - 内核态代码、上下文切换开销与损耗,假如获取到资源锁的线程A立马处理完逻辑释放掉资源锁,如果是采取互斥的方式,那么线程B从没有获取锁到获取锁这个过程中,就要用户态和内核态调度、上下文切换的开销和损耗。所以就有了自旋锁的模式,让线程B就在用户态循环等着,减少消耗
自旋锁可能潜在的问题
过多占用CPU的资源
,如果锁持有者线程A一直长时间的持有锁处理自己的逻辑,那么这个线程B就会一直循环等待过度占用cpu资源
递归使用可能会造成死锁,不过这种场景一般写不出来

首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。

而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止

从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。

当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道**“自旋锁”是比较耗费CPU的**

可重入锁(递归锁)

可重入锁和不可重入锁
Mutex可以分为递归锁(recursive mutex)和非递归锁(non-recursive mutex)。

可递归锁也可称为可重入锁(reentrant mutex),
非递归锁又叫不可重入锁(non-reentrant mutex)。
二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁
Windows下的Mutex和Critical Section是可递归的**。Linux下的pthread_mutex_t锁默认是非递归的**。可以显示的设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁。

上下文

其中处理器总处于以下状态中的一种:
内核态,运行于进程上下文,内核代表进程运行于内核空间;

内核态,运行于中断上下文,内核代表硬件运行于内核空间;

用户态,运行于用户空间。

通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的 地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户 空间继续执行,

进程上下文

所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,当内核需要切换到另一个进程时,它 需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

用户级上下文: 正文、数据、用户堆栈以及共享存储区;

寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);

系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

当发生进程调度时,进行进程切换就是上下文切换.

操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的是模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换

中断上下文

所以,“中断上下文”就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值