程序结构
程序一旦抽象成一个流程,就可以用流程图来表示,只是复杂度不同罢了,
常见的程序执行结构有:顺序结构和循环结构和选择结构。
peekmessage和dispatchmessage
window是一个事件循环的程序,WinMain方法就是一个死循环,不断读取和处理window中的消息。
首先系统有自己的系统消息队列主要存放来自按键和鼠标的消息,每个应用程序中的线程都有自己的
消息队列,在多线程的程序中消息队列的数量与线程数量相等。
当一个信号发出后,主线程收到这个消息,然后将它放入事件队列,也就是说主线程会先peekmessage,
然后再dispatchmessage,类似于一个邮件员将邮件收集起来,然后在分发出去。
首先一个GUI程序都应该是一个多线程的程序,不然当前线程阻塞,然后所有其他的任务都被阻塞。
每个线程在调度机制的影响下,都只有一个时间片的运行时间,由于是现在的进程是抢占式进程,每个线程
都有机会执行。
点击窗口的一个按钮,按钮就会发送一个类似信号的东西,主进程捕获了它,将它放入消息队列中,主进程每隔
一个时间片就会去peekmessage(查看消息队列中的事件),然后dispatchmessage(运行这个选中的事件(可
能对应的是一个时间处理函数))。
试想如果在该窗口增加一个"cancel"按钮,用于取消另一个"run"按钮的发出的动作, 那应该怎么办? 现在假设系统每隔
5秒会peekmessage,run按钮执行MyFunc函数会消耗20秒的时间。那么在这20秒中程序什么事都干不了,只能等待执行完成。
解决办法是在MyFunc这一级别增加用户模拟的查询操作,让MyFunc每隔五秒发送一个peekmessage和dispatchmessage,
这样就能在合适的时间发现cancel这个事件,从而不需要等待20秒就能迅速取消这个MyFunc处理函数。
什么是进程?
1.所谓进程其实就是程序在运行时的实体,是一个程序的一次运行过程,一个具有独立运行能力的程序,其实就是一个进程。它是操作系统进行任务调度的基本单元
2.进程由代码段,数据段,堆栈段三部分组成,每个进程都有自己的独立的内存空间,不同进程间的数据不能共享,各进程可以进程通信。
3.一个进程生成时,系统会为它分配一个唯一的进程标识符(PCB),操作系统靠它来感知进程的存在。
4.一个进程可以包含多个线程,也就是所谓的多线程
什么是线程?
1.线程,有时也称轻量级进程(LWP),它是程序执行流的最小单元。它由堆栈,寄存器集,线程ID,当前指令指针(PC)组成。
2.线程也可以理解为颗粒度更小的进程,对任务划分更为精细,线程可以像进程一样,进行并发,提高了程序的响应度,一个
进程包含了多个线程。
3.线程之间可以共享进程中一部分的内存空间(包括代码段,数据段,堆),还有一个进程中的资源(如打开的文件,socket,还有信号)
4.进程是可以作为一个程序独立运行的,但是线程并不可以,它必须依附于进程来工作,一个进程最少有一个线程。
线程相较于进程的优点
那有了进程,为什么我们还需要线程?
1.存在就是有价值的,为什么这么说呢,理由之一:还是要从进程的缺点说起,进程与线程相比是一个很重的东西,创建一个进程消耗的资源是创建一个线程消耗的资源的几倍,
不仅如此,线程切换上下文比进程切换上下文的效率更高,速度更快,一个进程消耗的资源一般是线程的几十倍(视工作环境)。
2.理由之二:每个进程都有一个独立的地址空间,要进行数据传递只能通过通信的方式,这样不仅费时,而且效率还不高,但是线程就不一样了,进程是包含线程的,所以线程可
以共享进程大部分的数据,这样操作数据时更加快速,快捷,但这也引发了线程安全的问题,稍后会讨论。
除去进程,单论线程的优势
1.提高程序响应能力,就如我们一般所使用的桌面环境,如果你点击一个按钮,接下的动作需要二十秒的时间完成,那么在这段时间中,我们什么都做不了,如果有了多线程,那
我们完全可以重新生成一个线程处理这个线程,,原线程继续跟用户进行交互,大大提升了程序的响应程度。
2.提供CPU的利用率,随着计算机技术的发展,现在的CPU都是多核的,要想完全利用好这些CPU资源,我们可以在每个核中生成一个线程,只要我们的线程数小于CPU中的核心数
数,就可以实现正的并发(内核线程在线程模型中会介绍)
线程的访问权限
- 栈:(尽管并不是完全不能被其他线程访问,但是一般情况下,还是被认为为私有数据)
- 线程局部存储(Thread Local Storge, TLS):是某些操作系统为线程单独提供的私有空间,通常具有很有限的容量。
- 寄存器(包括pc寄存器):寄存器是执行流的基本数据,为线程私有。
线程私有:
- 函数参数
- TLS
- 栈
线程之间共享: - 代码段
- 全局变量
- 函数中的静态变量
- 堆上的数据
- 一些进程中资源:打开的文件,socket,信号等
线程的调度和优先级
线程在进程中的三种状态
线程在运行时可以有三种状态:等待,就绪,运行。
切换顺序:
- 等待 -> 就绪 -> 运行 -> 等待
- 运行 -> 就绪 -> 运行(对于CPU密集型程序常出现时间片用完还没完成任务,于是在运行)
调度和优先级的基本概念
1.多线程的出现,导致了一个处理器上经常要运行多个线程,各个线程怎么运行?谁先谁后?这些问题都交给了线程调度,
有了线程调度,一个线程在规定的时间片(处于运行中的程序拥有的一段可以执行的时间)内运行,超出了时间片的时间,调度机制将会安排下一个线程运行。
2.操作系统从开始的多道程序到后来的分时操作再到现在的多任务操作系统,线程调度经历很多的演变,现在线程调度虽然各不相同,但是都有优先级调度和
轮转法的痕迹。轮转法(让每一个程序都运行一段时间,时间一到就切换到下一个程序),优先级调度(根据程序的优先级来运行,优先级高的先运行)
优先级的设置
一般在linux和window中用户可以手动设置线程的优先级,但系统也可以自己根据运行状态设置优先级,比如一个频繁进行等待的线程比每次都要把时间片
用光的线程更收欢迎,并且也更容易提升线程的优先级。
线程饿死
CPU密集型和IO密集型
简单来说,我们一般把那些频繁进行等待的线程称为IO密集型,而把那些很少进行等待的线程成为CPU密集型线程(具体可查阅资料)
想象一个场景,如果一个高优先级的CPU密集型任务,在每次时间片用尽后进入就绪状态,然后又进入运行状态,那么很低
优先级的程序就会永远等不到运行,这就是线程饿死。
线程提升优先级的方法
为了避免线程饿死现象,操作系统会把那些总是等不到运行的程序,随着时间的累计,逐渐增加他们的优先级,直到他们能够
被运行为止。
提供线程优先级的方法:
1.手动设置
2.根据等待的频繁程度,增加或减少优先级。
3.随着时间的累计,逐渐增减优先级。
抢占式线程和不可抢占式线程
在之前我们讨论的线程调度,每当一个线程在执行时间到达时间片后,都会被系统收回控制权,之后执行其他线程,这就是抢占式
线程,当在不可抢占式线程中时,线程是不可抢占的,除非线程自己发出一个停止执行的命令,或者进入等待。在该线程模型下,
线程必须自己主动进入就绪状态,而不是靠时间片强行进入就绪状态。如果一个线程没有等待,也没有主动进入就绪,那么它将
一直运行,其他线程被阻塞。
在非抢占式进程中,线程主动放弃有两种情况:
- 线程自己放弃
- 线程进入等待
但在现在非抢占式线程基本已经看不到了,基本上都是抢占式进程。
线程模型
内核线程
线程的并发执行是由多处理器和操作系统实现的,但实际情况更为复杂一点,windows和linux等操作系统,大多都在内核里提供了
线程的支持,内核线程有多处理器或者调度来实现并发,然而用户使用的线程其实是存在于用户态的线程,并不是内核线程,用户态
线程的数量并不等同于内核线程的数量,很可能是一个内核线程对应多个用户线程。
线程模型
1.一对一模型:
一个内核线程对应于一个用户线程,这样用户线程就有了和内核线程一致的线程,这时候线程之间的并发是真正的并发,
一个线程阻塞并不影响其他线程的运行。但是也有很大的缺点,很多操作系统限制操作系统的内核数量,因此用户线程的
数量就会收到影响,其次许多系统的内核线程切换上下文时开销比较大,导致效率低下。
2.一对多模型:
一个内核线程映射了许多个用户线程,这种情况下,线程之间的切换效率非常高,但是如果其中有一个线程阻塞的话,那么
处于该内核线程的其他线程将得不到运行。除此之外,多处理器系统上,如果处理器的增多,对线程的性能提升并不大,但
是多对一模型得到的好处是线程切换的高效和几乎不限制的线程数量。
3.多对多模型:
多对多模型结合了一对一和一对多模型的特点,一个线程的阻塞并不会使其他线程阻塞。多对多模型线程对用户线程的数量
也没有什么限制,在多处理器上的性能也还行,但是不如一对多模型。
线程安全
假想一个场景,如果你和你女朋友各有一张相同卡号的银行卡,余额为100万,然后你们同时在ATM上取钱,你直接100万,然后你
女朋友也取了40万用来买车,也成功取出来了。但是100万怎么取出140万了,这样银行岂不是要倒闭了,原因在于男的在取钱的时
候假如正在访问余额这个变量,但是女的也同时在访问这个变量,这就造成数据错误了, 如果男的在访问这个余额这个变量的时候,
女的不能访问,那么数据就不会造成错误了,这就是简单的线程安全。
几种线程锁
多线程处于一个多变的环境中,全局变量,堆数据,静态局部变量随时可能被多个程序更改,造成毁灭性的打击,所以在并发时数据的
安全性和一致性很重要。
同步与锁
1.为了避免一个变量被多个线程同时使用和修改,我们需要多个线程对该数据进行数据进行访问同步。同步就是一个线程在访问一个数据
的时候,其他线程不能再对其进行访问。
2.同步最常见的方法是使用锁,锁是一种非强制机制,线程在访问数据时Acquire,在访问结束时Release。当锁还没release时,其他
的线程不能访问该数据,处于阻塞状态,直到锁release。
常见的锁有:
二元信号量,多元信号量(简称信号量),互斥量,临界区,读写锁,条件变量
二元信号量
这是最简单的一种锁,只有两种状态,即锁住和未锁住,它适合只能唯一被一个线程占用的资源, 他可以先被一个线程获得,但是可以被其他
线程释放。
信号量
信号量可以称为多元信号量,它允许多个线程并发访问一个资源,一个初值为N的信号量,可以允许N个线程并发访问。线程访问资源时,首先
获取信号量,具体步骤如下:
1.h将信号量的值减1
2.如果信号量相减的值大于0,则继续运行,否则进入等待状态。
3.访问完资源后,进行下面操作
4.将信号量加1,如果大于1,唤醒一个等待中的进程
互斥量
互斥量和二元信号量很相似,同时只允许一个线程访问,只是二元信号量它可以被一个线程获取,但可以被任意进程释放。互斥量与之不同,一个
进程获取了互斥量,释放时只能由本线程释放,不能由其他线程释放。
临界区
临界区是比互斥量更为严格的一种锁,我们把临界区的锁的获取称为进入临界区,锁的释放离开临界区。不管是互斥量还是信号量,他们都是在整个
系统中可见的,也就是说一个进程创建了一个互斥量和信号量,其他进程可以获取该把锁。然而临界区的作用范围仅限于本进程可见,其他进程是无
法获取该锁的,除此之外,临界区和互斥量相同。
读写锁
假想一个场景,如果一个进程中,对一个数据要进行大量的读写,更具体的来说是大量地读,少量地写,如果每次在读写之前都上锁,读写完成后都
释放锁,那么加入我读写一共进行100次,那就一共有100次获得锁和释放锁的过程,如果使用读写锁,事情变得想对简单。
首先读写锁有两种获取方式,一种是独占式,一种是共享式。
**当锁处于自由状态时,以任何一种状态获得锁都能成功,如果锁处于共享状态,其他线程以共享方式获得也能成功(独占式不行)。如果一个锁处于独
占式的状态,那么以任何一种方法都不能获得锁**
条件变量
以上介绍的锁处于系统自动控制的状态,不能准确地控制各自线程的顺序,所以再次基础上,我们又加上了条件变量这个机制,在下面有一段关于条件变
量和互斥量相互使用的实例,可以参阅。
官方点来说条件变量是一种同步手段,对于条件变量来说,线程有两种状态,一种是线程可以等待条件变量(类比接收一个信号),其次是线程可以唤醒条件
变量(类比发送一个信号)。一个条件变量可以被多个线程等待,通俗点来说当一个线程唤醒了一个条件变量(发送信号)后,多个线程等到了条件变量(接收
到了信号),那么多个线程就可以一起执行。
可重入
一个函数被重入,如果一个函数没有执行完成,但是由于外部或者内部调用,又一次进入函数内部,一个函数要被重入,要有两个条件:
1.多个线程执行此函数
2.函数自身调用自身
一个建议:在锁中间最好避免出现函数调用的现象,以防出现重入现象。
// 这是一个函数调用自身的例子,当打印出hello world之后就一直卡死,造成死锁
#include <iostream>
#include <pthread.h>
pthread_mutex_t task_mutex;
void hello(){
pthread_mutex_lock(&task_mutex);
std::cout << "hello world" << std::endl;
hello();
pthread_mutex_unlock(&task_mutex);
}
int main(int argc, char const* argv[])
{
hello();
return 0;
}
代码演示
主要使用到的头文件:
pthread.h
semaphore.h
在命令行编译时要加上 -lpthread 表示链接libpthread.so这个动态库。
// c++中创建多个线程
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#define NUM_THREADS 5
void* say_hello(void *args){
cout << "hello world" << endl;
}
int main(){
for (int i = 0; i < NUM_THREADS; ++i) {
int ret = pthread_create(&tids[i], NULL, say_hello, NULL);
if(ret != 0){
cout << "pthread_create error: error_code= " << ret << endl;
}
}
//pthread_join(tids[0], NULL);
//pthread_join(tids[1], NULL);
//pthread_join(tids[2], NULL);
//pthread_join(tids[3], NULL);
//pthread_join(tids[4], NULL);
pthread_exit(NULL);
return 0;
}
运行结果:
hello world
hello world
hello world
hello world
hello world
// 这个版本是pthread_create中带参数
#include <iostream>
#include <pthread.h>
#include <cstdlib>
#include <unistd.h>
using namespace std;
#define NUM_THREADS 5
void* say_hello(void *args){
// 一定要进行强制类型转换
int* tid = (int *)args;
cout << "in say_hello, the args is " << *tid << endl;
}
int main(){
pthread_t tids[NUM_THREADS];
int arr[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; ++i) {
cout << "in main, the i is" << i << endl;
arr[i] = i;
// pthread_create中参数是void*类型,所以要强制类型转换
int ret = pthread_create(&tids[i], NULL, say_hello, (void*)&arr[i]);
if(ret != 0){
cout << "pthread_create error: error_code= " << ret << endl;
exit(-1);
}
}
pthread_exit(NULL);
return 0;
}
运行结果:
in main, the i is0
in main, the i is1
in say_hello, the args is 0
in main, the i is2
in say_hello, the args is 1
in main, the i is3
in say_hello, the args is 2
in main, the i is4
in say_hello, the args is 3
in say_hello, the args is 4
// 使用条件信号量的例子, 其中也包含了互斥锁的使用
#include <iostream>
#include <pthread.h>
#include <stdio.h>
using namespace std;
#define BOUNDARY 5
int tasks = 10;
pthread_mutex_t tasks_mutex; //定义互斥锁
pthread_cond_t tasks_cond; // 条件信号变量,处理处理两个线程之间的条件关系,当tasks>5,hello2处理,反之hello1,直到tasks8减到0
void* say_hello2(void *args){
pthread_t pid = pthread_self(); //获取当前线程id
cout << "[" << pid << "] hello in thread" << *((int *)args) << endl;
bool is_signed = false;
while(1){
pthread_mutex_lock(&tasks_mutex);
if(tasks > BOUNDARY){
cout << "[" << pid << "] tasks task: " << tasks << "in thread " << *((int *)args) << endl;
--tasks;
}else if(!is_signed){
cout << "[" << pid << "] pthread_cond_signal in thread" << *((int *)args) << endl;
pthread_cond_signal(&tasks_cond);
is_signed = true;
}
pthread_mutex_unlock(&tasks_mutex);
if(tasks == 0)
break;
}
}
void *say_hello1(void *args){
pthread_t pid = pthread_self();
cout << "[" << pid << "] hello in thread " << *((int *)args) << endl;
while(1){
pthread_mutex_lock(&tasks_mutex);
if(tasks > BOUNDARY){
cout << "[" << pid << "] pthread_cond_signal in thread " << *((int *)args) << endl;
pthread_cond_wait(&tasks_cond, &tasks_mutex);
} else {
cout << "[" << pid << "] task task: " << tasks << "in thread " << *((int *)args) << endl;
--tasks;
}
pthread_mutex_unlock(&tasks_mutex);
if (tasks == 0)
break;
}
}
int main(){
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_cond_init(&tasks_cond, NULL); // 初始化条件信号量
pthread_mutex_init(&tasks_mutex, NULL); // 初始化互斥量
pthread_t tid1, tid2; // 保存两个线程
int index = 1;
int ret1 = pthread_create(&tid1, &attr, say_hello1, (void *)&index);
if (ret1 != 0){
cout << "pthread_create error:error_code= " << ret1 << endl;
}
int index2 = 2;
int ret2 = pthread_create(&tid2, &attr, say_hello2, (void *)&index);
if (ret2 != 0){
cout << "pthread_create error:error_code= " << ret2 << endl;
}
pthread_join(tid1, NULL); //链接两个线程
pthread_join(tid2, NULL);
pthread_attr_destroy(&attr); //释放内存
pthread_mutex_destroy(&tasks_mutex);
pthread_cond_destroy(&tasks_cond); //正常退出
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。