本文详细介绍了多线程的常见概念 生产者消费者模型将在多线程(下)继续讲解

欢迎大家指正 提起讨论进步啊

目录

多线程的理解

线程的优点

线程的缺点:

线程的用途

线程VS进程

用户级线程库

POSIX线程库

线程创建:

线程等待

线程终止

取消线程

分离线程

Linux线程互斥

互斥量mutex

初始化互斥量

销毁互斥量

互斥量加锁和解锁

可重入VS线程安全

概念

常见的线程不安全的情况

常见不可重入的情况

常见可重入的情况

可重入与线程安全联系

可重入与线程安全区别

常见锁概念

死锁

Linux线程同步

条件变量

同步和竞态条件

为什么 pthread_cond_wait 需要互斥量


多线程的理解

线程(thread)是一个执行分支,执行粒度比进程更细调度成本更低(不需要进行cache换),他是进程内部的一个执行流,同时是CPU调度的基本单位

进程是承担分配系统资源的基本实体

上面一堆话怎么理解呢?

首先明确一些知识:

寄存器分为两种:可见的和不可见的

CPU内部:运算器,寄存器,控制器,MMU,硬件cache,L1,L2,L3

cache是位于cpu和内存之间的存储器,读写速度高于内存而低于cpu内部的寄存器

由于数据的空间局部性原理和时间局部性原理,cache的引入提升了主存的读写效率

当cpu对某一内存地址发出读操作时,如果cache中已经缓存了此地址的数据,称为命中(hit),数据由cache直接发送给cpu。如果cache中并未缓存,则称为缺失(miss),数据将先由主存发送到cache,再由cache发送到cpu

硬件cache又称为高速缓存:缓存各种数据和代码,故增大了切换进程的成本

局部性原理/预加载:把一些热点数据提前加载到缓冲区

下面从linux操作系统角度来理解上面说的一堆线程概念

这是我们之前理解的进程,有自己的PCB,CPU中有维护这个PCB的寄存器,并且他对应着一个地址空间,进程有自己的PCB和代码数据

但是今天我们除了这一个PCB,还有更多的

每一个PCB我们可以称之为一个执行流,或者是所谓的线程,这些线程是指向同一块地址空间,所以他们可以看到一些共享数据,执行代码的角度来说,每一个PCB处理代码区中不同代码区域相当于一个进程有不同的执行流,那么线程就是进程内部的一个执行流,所以线程是一个执行分支,执行粒度比进程更细

之前在进程之间切换的时候要不停的切换cache中的数据,但是今天有了线程,我们可以看到同一份共享数据,cache不需要切换,所以调度成本变低

对CPU,他看到的还是PCB,只不过看到更多,每一次调度PCB的时候他是找线程而不是进程,所以线程是CPU调度的基本单位

而今天 进程的概念是这样的

不难理解,他是承担分配系统资源的基本实体

操作系统中一个线程对应着一个TCB(Thread Control Block),叫做线程控制模块,控制着线程的运行和调度,他属于进程PCB

Windows系统就是这样设计的,但是这个真的太复杂了,所以linux大佬想到了更好的方法:复用

复用你PCB的结构体 用进程PCB模拟线程的TCB

线程的优点

1.创建线程的代价要比创建进程的代价小得多

2.与进程之间的切换相比,线程的切换对于操作系统来说工作量小得多

3.线程占用的资源更少

4.充分利用多处理器的可并行数量

5.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

加密解密 文件压缩和解压等预算法有关的——比较消耗CPU资源

6.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

下载上传 IO主要消耗的是IO资源,磁盘的IO,网络带宽等等

误区:

线程可以比较多但是不是越多越好,具体多少是要量化的——保证进程/线程CPU的个数/核数一致

线程的缺点:

  • 1.性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器

如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变

  • 2.健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

多线程中一个线程崩溃 最后会导致整个进程崩溃 为什么?

系统角度:线程是进程的执行分支 线程做就是进程做

信号角度:页表转换时MMU识别写入权限,没有验证通过

MMU异常——>OS识别——>给进程发信号——>linux进程信号 信号是以进程为主的

  • 3.缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响

因为执行流看到的资源是通过地址空间看到的 多个LWP看到的是同一个地址空间 所以所有的线程可能会共享进程的大部分资源 修改共享资源所有线程都能看到

  • 4.编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

线程的用途

合理的使用多线程,能提高CPU密集型程序的执行效率
并且可以提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是
多线程运行的一种表现)

线程VS进程

线程私有:线程id 一组寄存器(有自己独立的上下文) 栈 errno 信号屏蔽字 调度优先级

共享:文件描述符表 每种信号的处理方式 当前工作目录 用户id和组id

用户级线程库

linux下没有在真正意义上的线程 而是用进程模拟的线程LWP 所以linux不会直接提供创建线程的系统调用 他会给我们最多提供创建轻量级进程的接口

用户视角:只认线程

用户级线程库:对下将linux轻量级接口封装 对上给用户提供进行线程控制的接口

用户级线程库是任何系统都要自带的也叫原生线程库

POSIX线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项

线程创建:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);

thread:返回线程ID
attr:设置线程的属性,attr为nullptr表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事
前面讲的线程ID属于进程调度的范畴 因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

pthread_t pthread_self(void);

pthread_t 到底是什么类型呢?取决于实现 对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址

线程等待

为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
创建新的线程不会复用刚才退出线程的地址空间

int pthread_join(pthread_t thread, void **value_ptr);

thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传nullptr给value_ ptr参数。

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return   这种方法对主线程不适用,从main函数return相当于调用exit
2. 线程可以调用pthread_ exit终止自己
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程

void pthread_exit(void *value_ptr);

value_ptr : value_ptr不要指向一个局部变量
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

取消线程

int pthread_cancel(pthread_t thread);

返回值:成功返回0;失败返回错误码

分离线程

默认情况下,新创建的线程是joinable的(线程的属性),线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的——一个线程如果被分离 不能join

如果join会报错——join只是一个属性

Linux线程互斥

先明确一些概念:

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互  多个线程并发的操作共享变量,会带来一些问题

(全局变量是共享资源 但是加上__thread修饰全局变量 变成__thread int g_val=100 就是每个线程各有一份 在线程局部存储!是线程内部的局部变量)

对全局变量-- 没有保护的话会存在并发访问的问题 进而导致数据不一致

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else{break;}}
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

结果是

thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

为什么可能无法获得争取结果?
if 语句判断条件为真以后,代码可以并发的切换到其他线程
usleep之后,可能有很多个线程会进入该代码段
--ticket 操作本身就不是一个原子操作

load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址

要解决的问题:

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要做到这三点,本质上就是需要一把锁   Linux上提供的这把锁叫互斥量

初始化互斥量

静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);

mutex:要初始化的互斥量
attr:nullptr

销毁互斥量

销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化(静态分配)的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

此时可以把上面的抢票代码修改一下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
// sched_yield(); 放弃CPU
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}

可重入VS线程安全

概念

线程安全:保证多个线程并发同一段代码时,不会出现不同的结果

常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全的问题

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入

一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态  随被调用  而发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都由函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入与线程安全区别

可重入函数是线程安全函数的一种,线程安全不一定是可重入的,而可重入函数则一定是线程安全的
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的

常见锁概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态

死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

如何避免死锁:核心 破坏死锁的四个必要条件

不主动加锁(去掉互斥问题)

主动释放锁(破坏请求和等待)
按顺序申请锁
同一控制线程释放锁——剥夺锁

Linux线程同步

条件变量

条件变量是线程库提供的一个描述临界资源状态的变量 不用频繁的申请和释放锁也能检查到临界资源
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

可以用条件变量实现线程同步

同步和竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条 。在线程场景下,这种问题也不难理解

条件变量的初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);

cond:要初始化的条件变量
attr:nullptr

销毁:

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal——唤醒睡眠的线程,一次只能唤醒一个线程
pthread_cond_broadcast——唤醒睡眠的线程,一次唤醒所有睡眠的线程(所以叫广播)

为什么 pthread_cond_wait 需要互斥量

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护   没有互斥锁就无法安全的获取和修改共享数据、

所以条件变量和互斥锁总是一起使用

条件变量使用规范

等待条件代码

pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

给条件发送信号

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

【Linux】多线程(上)相关推荐

  1. 【Linux】Linux多线程(上)

    前言 hi~ 大家好呀,欢迎来到我的Linux学习笔记.本篇笔记将会重点从内核结构引入Linux下的线程,理解Linux下线程和进程的相关性和区别,以及线程相关的操作方法,在到之后的线程互斥和线程同步 ...

  2. linux架构接口层教程,在LINUX平台上进行成功实现RIL层功能和框架层应用

    1.课题研究的背景和意义 1.1 Android RIL简介 Android RIL(Radio Interface Layer)提供了无线基带Modem与电话应用之间的抽象层.在Android RI ...

  3. linux 内核信号量与用户态信号量(system v,信号量在Linux多线程机制中的应用

    [摘 要]本文以信号量原理为基础,重点阐述信号量在Linux多线程同步机制中的实现特色. [关键词]信号量:Linux:多线程:同步 1 信号量 1965年E. W. Dijkstra首次提出信号量的 ...

  4. 新书预告:《Linux 多线程服务端编程——使用 muduo C++ 网络库》

    看完了 W. Richard Stevens 的传世经典<UNIX 网络编程>, 能照着例子用 Sockets API 编写 echo 服务, 却仍然对稍微复杂一点的网络编程任务感到无从下 ...

  5. linux多线程编写哲学家,Linux系统编程(三) ------ 多线程编程

    一.线程的创建和调度 1.线程是程序执行的某一条指令流的映像. 为了进一步减少处理机制的空转时间,支持多处理器及减少上下文切换开销,进程在演化中出现了另一个概念--线程.它是进程内独立的一条运行路线, ...

  6. Linux 之二 Linux 多线程

      最近在整理旧电脑时,发现了一些刚入行时的学习记录,以及最早使用新浪博客 http://blog.sina.com.cn/zcshou 写的一些文章.最近要重拾 Linux,所以把这些 Word 文 ...

  7. linux多线程求和_linux 多线程信号处理总结

    linux 多线程信号总结(一) 1. 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知 ...

  8. Linux多线程——使用互斥量同步线程

    前文再续,书接上一回,在上一篇文章:Linux多线程--使用信号量同步线程中,我们留下了一个如何使用互斥量来进行线程同步的问题,本文将会给出互斥量的详细解说,并用一个互斥量解决上一篇文章中,要使用两个 ...

  9. 《Linux多线程服务端编程:使用muduo C++网络库》书摘6.6.2节

    6.6.2 常见的并发网络服务程序设计方案 W. Richard Stevens 的<UNIX 网络编程(第2 版)>第27 章"Client-ServerDesign Alte ...

  10. ZT 为什么pthread_cond_t要和pthread_mutex_t同时使用 || pthread/Linux多线程编程

    为什么线程同步的时候pthread_cond_t要和pthread_mutex_t同时使用 (2009-10-27 11:07:23) 转载▼ 标签: 杂谈 分类: 计算机 举一个例子(http:// ...

最新文章

  1. 十分流行的自举法(Bootstrapping )为什么有效
  2. 【设计模式】四:开放-封闭原则
  3. Win32 API 打开另一个进程
  4. Winform中设置ZedGraph的X轴与Y轴的刻度不在对面显示
  5. c语言补全 subilme_Sublime Text3 C语言插件
  6. js粘贴板为什么获取不到图片信息_图床+typora,告别markdown中关于图片的困惑
  7. 腾讯面试编程题python_腾讯面试官出的 2 道经典数据分析面试题
  8. 查询链接服务器信息,如何通过 SQL Server 链接服务器和分布式查询使用 Excel
  9. 用户眼中的银行信息化-我的开博感言
  10. 分式混合运算20道题_FAG剖分式调心滚子轴承的性能
  11. leetcode刷题:求旋转有序数组的最小值
  12. Flutter进阶—质感设计之表单输入
  13. java switch finally_Java中的switch疑问
  14. 多目标优化算法(一)NSGA-Ⅱ(NSGA2)
  15. 目前电子计算机处于什么时代,目前,电子计算机处于哪大规模和超大规模集成电路时代。()...
  16. 忘记网站后台密码 PHP+mysql+md5 破解
  17. 软件设计模式Java版
  18. 我的世界租赁服自定义服务器,我的世界租借服务器(我的世界技巧教程 租赁服务器玩家间传送功能)...
  19. GrapeCity Documents for Imaging
  20. 微信网页程序开发,如何解决后退时重复登录的问题

热门文章

  1. 在 Heroku 部署 PHP,部署在 Heroku 云
  2. 单口RAM、伪双口RAM、真双口RAM、单口ROM、双口ROM的区别
  3. Ubuntu耳机没有声音
  4. IPC网表、坐标文件、装配PDF输出
  5. Java Web 简明教程
  6. one morning of Saturday
  7. 一张图让你分清楚企业微信的这些群!
  8. 构建外卖系统小程序,订单管理功能实现步骤详解
  9. eclipse 导入项目后,如何消除error pages的错误
  10. 自定义浏览器滚动条的样式