多线程和线程安全

程序结构

程序一旦抽象成一个流程,就能够用流程图来表示,只是复杂度不一样罢了,
常见的程序执行结构有:顺序结构和循环结构和选择结构。linux

peekmessage和dispatchmessage

window是一个事件循环的程序,WinMain方法就是一个死循环,不断读取和处理window中的消息。
首先系统有本身的系统消息队列主要存放来自按键和鼠标的消息,每一个应用程序中的线程都有本身的
消息队列,在多线程的程序中消息队列的数量与线程数量相等。ios

当一个信号发出后,主线程收到这个消息,而后将它放入事件队列,也就是说主线程会先peekmessage,
而后再dispatchmessage,相似于一个邮件员将邮件收集起来,而后在分发出去。c++

首先一个GUI程序都应该是一个多线程的程序,否则当前线程阻塞,而后全部其余的任务都被阻塞。windows

每一个线程在调度机制的影响下,都只有一个时间片的运行时间,因为是如今的进程是抢占式进程,每一个线程
都有机会执行。安全

点击窗口的一个按钮,按钮就会发送一个相似信号的东西,主进程捕获了它,将它放入消息队列中,主进程每隔
一个时间片就会去peekmessage(查看消息队列中的事件),而后dispatchmessage(运行这个选中的事件(可
能对应的是一个时间处理函数))。多线程

试想若是在该窗口增长一个"cancel"按钮,用于取消另外一个"run"按钮的发出的动做, 那应该怎么办? 如今假设系统每隔
5秒会peekmessage,run按钮执行MyFunc函数会消耗20秒的时间。那么在这20秒中程序什么事都干不了,只能等待执行完成。
解决办法是在MyFunc这一级别增长用户模拟的查询操做,让MyFunc每隔五秒发送一个peekmessage和dispatchmessage,
这样就能在合适的时间发现cancel这个事件,从而不须要等待20秒就能迅速取消这个MyFunc处理函数。并发

什么是进程?

1.所谓进程其实就是程序在运行时的实体,是一个程序的一次运行过程,一个具备独立运行能力的程序,其实就是一个进程。它是操做系统进行任务调度的基本单元
2.进程由代码段,数据段,堆栈段三部分组成,每一个进程都有本身的独立的内存空间,不一样进程间的数据不能共享,各进程能够进程通讯。
3.一个进程生成时,系统会为它分配一个惟一的进程标识符(PCB),操做系统靠它来感知进程的存在。
4.一个进程能够包含多个线程,也就是所谓的多线程socket

什么是线程?

1.线程,有时也称轻量级进程(LWP),它是程序执行流的最小单元。它由堆栈,寄存器集,线程ID,当前指令指针(PC)组成。
2.线程也能够理解为颗粒度更小的进程,对任务划分更为精细,线程能够像进程同样,进行并发,提升了程序的响应度,一个
进程包含了多个线程。
3.线程之间能够共享进程中一部分的内存空间(包括代码段,数据段,堆),还有一个进程中的资源(如打开的文件,socket,还有信号)
4.进程是能够做为一个程序独立运行的,可是线程并不能够,它必须依附于进程来工做,一个进程最少有一个线程。函数

线程相较于进程的优势

那有了进程,为何咱们还须要线程?
1.存在就是有价值的,为何这么说呢,理由之一:仍是要从进程的缺点提及,进程与线程相比是一个很重的东西,建立一个进程消耗的资源是建立一个线程消耗的资源的几倍,
不只如此,线程切换上下文比进程切换上下文的效率更高,速度更快,一个进程消耗的资源通常是线程的几十倍(视工做环境)。
2.理由之二:每一个进程都有一个独立的地址空间,要进行数据传递只能经过通讯的方式,这样不只费时,并且效率还不高,可是线程就不同了,进程是包含线程的,因此线程可
以共享进程大部分的数据,这样操做数据时更加快速,快捷,但这也引起了线程安全的问题,稍后会讨论。性能

除去进程,单论线程的优点

1.提升程序响应能力,就如咱们通常所使用的桌面环境,若是你点击一个按钮,接下的动做须要二十秒的时间完成,那么在这段时间中,咱们什么都作不了,若是有了多线程,那
咱们彻底能够从新生成一个线程处理这个线程,,原线程继续跟用户进行交互,大大提高了程序的响应程度。
2.提供CPU的利用率,随着计算机技术的发展,如今的CPU都是多核的,要想彻底利用好这些CPU资源,咱们能够在每一个核中生成一个线程,只要咱们的线程数小于CPU中的核心数
数,就能够实现正的并发(内核线程在线程模型中会介绍)

线程的访问权限

  • 栈:(尽管并非彻底不能被其余线程访问,可是通常状况下,仍是被认为为私有数据)
  • 线程局部存储(Thread Local Storge, TLS):是某些操做系统为线程单独提供的私有空间,一般具备颇有限的容量。
  • 寄存器(包括pc寄存器):寄存器是执行流的基本数据,为线程私有。

线程私有:

  1. 函数参数
  2. TLS

  3. 线程之间共享:
  4. 代码段
  5. 全局变量
  6. 函数中的静态变量
  7. 堆上的数据
  8. 一些进程中资源:打开的文件,socket,信号等

线程的调度和优先级

线程在进程中的三种状态

线程在运行时能够有三种状态:等待,就绪,运行。
切换顺序:

  1. 等待 -> 就绪 -> 运行 -> 等待
  2. 运行 -> 就绪 -> 运行(对于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); //正常退出

}
相关文章
相关标签/搜索