线程的那些事

标签: 线程 | 发表时间:2015-08-04 17:17 | 作者:wwh578867817
出处:http://blog.csdn.net

线程的那些事

前言:这篇文章主要小结下linux下多线程的知识点,并且有一些多线程编程中的拓展概念。以及c语言编写线程池的思路


  • linux线程简介
  • 线程的私有数据和公有数据
  • 线程的上下文切换
  • 创建线程
  • 线程终止
  • 互斥量
  • 条件变量
  • 多线程概念简单拓展
    • 竟态条件
    • 并发与并行
    • 同步与互斥
    • volatile
    • CAS(compare and swap)
    • 指令重排和内存屏障
  • c语言实现线程池思想

linux线程简介

系统中的进程在某些情况下并不能满足我们的需求,比如一个操作系统创建进程的数量非常有限,进程的上下文切换相对来说比较慢,进程间通信(IPC)比较麻烦…等等
所以系统中引入线程这一概念,看看linux下的线程

linux实现线程的机制和其他操作系统有很大区别,从内核的角度,它并没有线程的概念,所有的线程都被当作进程来实现(轻量级进程) 。线程仅仅被视为和其他进程共享某些资源的进程。

linux下线程的创建和进程的创建类似,只不过在调用clone()系统调用的时候要传递一些参数标志来指明需要共享的资源。
这里写图片描述
线程调用的clone()
clone(CLON_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
clone函数中的参数就限定了线程,父子共享内存空间,文件系统资源,文件描述符和信号处理程序等等。
进程创建fork()
clone(SIGCHLD, 0);
进程创建vfork()
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

内核线程:
有时系统要执行一些后台操作,这些任务可以通过内核线程完成,独立运行在内核空间的标注进程,最重要的一点它们只运行在内核空间从不切换到用户空间去。曾经linux异步IO其中一种实现方式就是通过内核线程来操作,用户发起一个任务,立即返回,有内核线程帮助用户完成任务,完成后通知用户。


线程的私有数据和共有数据

每个线程都有自己的私有数据,其中包含执行环境的必须信息,线程ID,一组寄存器值,栈,调度优先级,信号屏蔽字,errno变量,以及线程的私有数据(TSD,一键多值技术,可以被多个函数访问,但是对其他线程是屏蔽的)。
一个进程信息对所有线程都是共享的,包括可执行程序的代码,程序的全局内存和堆内存,栈以及文件描述符。

例子(看一下进程内存映射数据分布):

  #include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <pthread.h>

void *thread_func(void *args)
{
    printf("tid: %u pid: %u thread id: %u\n", 
            getpid(), syscall(224), pthread_self());
    while(1)
    {
        sleep(10);
    }
}

int main(int argc, char *argv[])
{
    pthread_t thread;
    int count = 0;
    while(pthread_create(&thread, NULL, thread_func, NULL) == 0)
    {
        sleep(5);
        ++count;
    }

    return EXIT_SUCCESS;
}

创建了8个线程
这里写图片描述

此进程的内存映射
这里写图片描述

上图为/proc/(进程ID)/maps文件中的内容,我们可以看见进程为创建的线程分布的私有栈(保存线程栈变量以及cpu切换暂存线程数据等),还有最下面的stack,这个就是进程的栈了。理解了上图我们就能理解线程的私有数据和共有数据在进程中的大致分布。


上下文切换

线程的应用广泛离不开它的轻量,轻也体现在线程的上下文切换。
线程和进程的上下文切换最主要的区别是线程的切换虚拟内存空间是相同的,但进程是不同的,这两种切换都会将寄存器变量切换出去(线程私有寄存器变量),会有性能消耗。
另外进程中上下文切换会打乱处理器的缓存机制,简单的来讲,一旦切换出去上下文,处理器中所有已经缓存的内存地址和数据等都作废了,尤其是进程切换会将整个虚拟内存空间切换出去,刷新了许多缓存,导致效率降低。但是在线程切换中就不会出现这个问题。


创建线程

这里写图片描述

参数分别为线程id,线程属性设置,线程执行函数,线程执行函数参数。
pthread_attr_t是一个结构体,一般我们使用默认的属性。
编译时要连接线程库
注意:新线程要获取自己的pid只能使用pthread_self(),因为可能在pthread_create()函数未创建完成时,线程已经开始运行。我们使用线程自己提供的api获取线程id比较安全


线程终止

pthread_join和pthread_detach函数
pthread_join:
这里写图片描述
pthread_join参数为线程的id以及一个获得等待线程执行完成的返回值的一个二级指针。
pthread_join有两个功能:
1.主线程来等待子线程结束,如果线程未调用pthread_detach,则必须调用pthread_join来等待线程,如果没有等待,一方面主线程会不等待子线程从而提前结束,那么子线程可能会没机会运行,其次则会产生僵尸线程,与僵尸进程的概念类似,除了浪费系统资源外,如果存在大量僵尸线程,我们将可能无法创建新的线程。
2.线程之间是平等的,也就是说我们除了主线程等待子线程用pthread_join外,我们还可以连接任意两两线程,一个线程调用pthread_exit()并且返回一个数值,另外一个线程调用pthread_join()来接受返回值并且等待另外线程结束。

pthread_detach:
这里写图片描述
pthread_detach函数参数仅为线程id,意义为将指定id的线程变为后台运行,由系统来为它回收资源。


互斥量(互斥锁)

多个线程同时访问一个资源可能会出现不能预料到的结果,此时我们可以通过互斥量来保证线程同步访问资源。
当我们对一个临界资源(某时刻只能有一个线程或进程访问的资源)进行访问时,必须先拿到互斥锁,否则阻塞在锁上,当我们抢到互斥锁时就可以访问资源,访问完成时释放锁供其他进程或线程得到并继续交替访问临界资源。
初始化:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
上面使用的是静态(默认)初始化,我们也能通过设置互斥锁的属性(动态初始化)来改变互斥锁从而满足我们使用的场景。使用了动态初始化我们则必须销毁互斥锁

  //头文件:
#include <pthread.h>
//函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//头文件:
#include <pthread.h> 
//函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意以下几点:
1.

1.避免线程对多个共享资源的互斥量进行访问而导致的死锁,我们应该对互斥锁进行设定层级关系,也就是说加锁顺序必须确定

线程对多个共享资源的互斥量进行访问

这里写图片描述

如果我们强制设定加锁顺序为mutex1后mutex2,也就是说mutex1为一级锁,mutex2为二级锁,则不会出现上面的情况。

2.

递归互斥锁,也称为可重入锁,简单来讲就是我们可以重复对递归互斥锁加锁而不会出现普通互斥锁未释放锁就加锁的死锁情况。
递归互斥锁适用于如下场景

  func_one()
{
    lock_mutex;
    dosomething();
    unlock_mutex;
}
func_two()
{
    lock_mutex();
    dosomething();
    func_one();
    unlock_mutex;
}

3.

pthread_mutex_errorcheck
对此类互斥锁错误使用会进行报错。
这里写图片描述


条件变量

互斥量防止多个线程同时访问某一共享变量,条件变量允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待。
默认我们使用互斥锁时,如果锁此时被其他线程持有,那么后来的线程就默认阻塞,这样效率是非常低的,现在我们在想让线程获取互斥锁的地方调用条件变量的wait函数等待条件变量的条件变化从而通知我们,我们通过条件变量来避免线程阻塞,当持有锁的线程释放锁时,线程被通知。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
PTHREAD_COND_INITIALIZER是一个宏定义的默认结构体成员,上面代码是给条件变量设置默认的属性。我们也可以通过init来设置条件变量的属性。

  //初始化条件变量,attr是条件变量的属性,比如更改条件等。
pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//销毁条件变量,必须在没有使用时销毁
pthread_cond_destroy(pthread_cond_t *cond);
//阻塞某一线程,直至收到条件变量的cond的通知
pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t *mutex); 
//带休眠时间的条件变量 
pthread_cond_timewait()
//通知等待的任意一个线程
pthread_cond_signal(pthread_cond_t *cond);
//通知所有等待互斥量的线程
pthread_cond_broadcase(pthread_cond_t *cond);

多线程概念简单拓展

竟态条件

从多线程的角度来说就是多个线程对同一块资源进行访问时,最终结果取决于线程执行的顺序,但是线程执行的顺序不固定,每一执行的结果也就是不确定的。


并发与并行

并发:同一时间,同时发生的事情
并行:同一时间,同时在做的事情
简单的来说就并发是指同一时间我们能接受多少,并行是同一时间我们能处理多少。
举个例子:老妈在做饭,让我帮忙干活,短短的一分钟她说了四件事,去超市买盐,去收衣服,去扫地,并嘱咐我喝完桌子的饭, 没办法我只能答应- -,这就是并发,我不会分身术,我只有两只手,我可以边扫地边喝桌子上的饭,这是并行。
再说个socket的例子吧,都知道epoll IO复用解决了C10K问题,它可以同时接受10000左右的连接数,这指的是并发, 但是一台计算机只有两个CPU,
同时只能有几个线程在上面跑,这是并行。


同步与互斥

互斥:指的是同一件事或者同一个东西,在同一时刻在只允许一个用户在使用或者在做。
同步:在互斥的基础上有序的进行访问。注意同步并没有说什么同一时刻之类的。


volatile

线程间同步的一种手段,但是并不保证能正确得到结果,volatile保证每次读取数据都是从内存中最新的而不是缓存中。
缺点:
volatile关键字能保证可见性,但不能保证原子性(不可在被分割),可见性只能保证每次读取的是最新的值,但是volatile无法保证对变量等操作的原子性。
例子:
线程1对变量进行递增操作,线程1读取了变量i的初始值为10,然后时间片到了,轮到线程2,线程2对变量进行自增操作,线程2也去读取i的原始值10,由于线程1只是对i进行读取并没有修改,线程2修改后写入主存11,此时轮到线程1执行,线程1修改的还是初始值10,然后写回内存中修改后的11,等于两次操作一次修改。


CAS

CAS(compare and swap)是一种策略,当A线程访问一个变量时,先取出旧值并保存,然后保存变化后的新值,此时再次比较旧值和内存中的值是否一致(一致说明没有被其他线程修改),如果一致则更新值。


指令重排和内存屏障

指令重排:一般处理器不会按照我们代码的顺序来执行而是会重新排序代码形成的指令,按照最顺畅最快的方式来执行。
比如代码有三件事情要做,但是第一和第三件可以连着做,那么cpu就会重排指令变为一三二,为了达到更快的执行速度。

内存屏障:指令重排在多线程情况下可能会产生不确定的结果
也被成为“内存栅栏”, 简单的讲就是严格的限定指令的执行顺序,也就是禁止特定类型的处理器重排。也是为了避免多线程并发受到影响。


c语言实现线程池的思想

没写过线程池的童鞋都会有一些误解,所以借着这篇博客说一下c语言实现线程池的思想。
一般对线程池纠结的地方会有如下。
线程是如何保存在“池”中的?
线程是如何被调度的?
线程如何被借用执行任务然后在归还?归还过程是怎么样的?
线程池要执行的任务又是如何保存的?

线程池一般是一个队列,元素为创建的线程的id,我们通过线程id来控制线程,线程池要执行的任务也是保存在一个队列或链表中,里面存储的是函数指针,也称为callback。

那么线程池一般维持一个线程池结构体,内部成员包含:
互斥锁(保证任务存取队列每时每刻只有一个线程操作,避免竟态条件)
条件变量(当任务队列从空变为非空时,要通过条件变量来唤醒)
线程id链表(也就是存放线程id的队列或链表,管理所有线程)
任务队列(存放callback函数指针)
线程池大小(capacity容量)
任务队列大小(要判断当前任务队列是否为空)
毁坏标记flag(如果想要销毁线程池需要有毁坏标记)
任务结构体成员就包括:
callback函数指针(调用执行任务)
callback函数参数arg(传递给callback的参数)
关键点:
其实所有线程一开始创建完成后执行的线程函数内都是一个while(1)死循环,所有的线程都会去抢任务队列的锁,抢到了则执行任务释放锁,每次抢任务时会检查任务队列的大小,如果为0则线程阻塞,当向任务队列中添加任务时,我们通过条件变量的同步来唤醒阻塞的线程。抢不到的则阻塞在锁上,并没有所想的那种线程执行完成后归还到“池”里需要则调用再次继续取出线程的过程。线程执行完继续抢任务(实际上是锁),抢不到就阻塞。抢到了就执行,没有所谓的归还过程。while(1)循环里会存在检查flag标记,如果标记被修改,则销毁线程。


作者:wwh578867817 发表于2015/8/4 9:17:54 原文链接
阅读:24 评论:0 查看评论

相关 [线程] 推荐:

MySQL Replication 线程

- - CSDN博客推荐文章
Replication 线程. Mysql 的Replication 是一个异步的复制过程,从一个Mysql instace(我们称之为Master)复制到另一个Mysql instance(我们称之Slave). 在Master 与Slave 之间的实现整个复制过程主. 要由三个线程来完成,其中两个线程(Sql 线程和IO 线程)在Slave 端,另外一个线程(IO 线程)在Master 端.

Java线程池

- - 企业架构 - ITeye博客
线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的. 在jdk1.5之后这一情况有了很大的改观. Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用. 为我们在开发中处理线程的问题提供了非常大的帮助.

Java 线程池

- - 编程语言 - ITeye博客
在项目中,系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互. 在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存周期很短的线程时,更应该考虑使用线程池. 使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数.

Java线程(四):线程中断、线程让步、线程睡眠、线程合并

- - 编程语言 - ITeye博客
 最近在Review线程专栏,修改了诸多之前描述不够严谨的地方,凡是带有Review标记的文章都是修改过了. 本篇文章是插进来的,因为原来没有写,现在来看传统线程描述的不太完整,所以就补上了. 理解了线程同步和线程通信之后,再来看本文的知识点就会简单的多了,本文是做为传统线程知识点的一个补充. 有人会问:JDK5之后有了更完善的处理多线程问题的类(并发包),我们还需要去了解传统线程吗.

Java Thread多线程

- - CSDN博客推荐文章
Java Thread多线程. Java 多线程例子1 小例子. super("zhuyong");//设置线程的名字,默认为“TestThread”. Java 多线程例子2 前台线程(用户线程) 后台线程(守护线程 ). 1,setDaemon(true)后就是后台线程(守护线程 ),反之就是前台线程(用户线程).

Android线程大坑

- - 移动开发 - ITeye博客
     android界面的更新实在主线程进行的,通常把主线程也叫UI线程,UI线程里进行事件的分发和交互. 在UI线程中进行耗时操作,比如网络请求,IO操作等会阻塞UI线程,界面会卡住,并且超过大概5秒钟程序会ANR(Application Not Responding),也就是死掉. 其实这种GUI单线程的思想在我上一篇博客(http://zyqwst.iteye.com/blog/2262011)都有阐述,道理一模一样,只是android实现的方式上略有不同,所以我建议把上一篇Swing线程的博客能够阅读一遍,Android线程的问题豁然开朗,始终晋级GUI开发的原则:在UI线程中进行界面的更新操作,在单独线程中进行耗时操作.

浅谈 iOS 线程

- - SegmentFault 最新的文章
通常主线程和其他线程的使用场景. Tips: 解压、打开 Zip 包,读写较大文件的操作也不宜放在主线程里. 一般异步网络请求中会有一个 completionBlock ,这个 completionBlock 是在主线程中被调用的. 所以,可能消耗大量时间的代码(例如上面提到的处理 Zip 包的方法)也不宜放在这些 block 中.

Java线程之FutureTask

- - zzm
FutureTask是Future和Callable的结合体. 然后通过Future来取得计算结果. 但是,若开启了多个任务,我们无从知晓哪个任务最先结束,因此,若要实现“当某任务结束时,立刻做一些事情,例如记录日志”这一功能,就需要写一些额外的代码. FutureTask正是为此而存在,他有一个回调函数protected void done(),当任务结束时,该回调函数会被触发.

Java多线程之synchronized

- - CSDN博客推荐文章
这里通过三个测试类阐述了synchronized应用的不同场景. 首先是最基本的synchronized Method的使用.  * @see 概述:Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor) .  * @see 说明:当synchronized关键字修饰一个方法时,则该方法为同步方法 .

[转]GDB调试多线程

- - 小彰
GDB 多线程调试基本命令 实现简介 以及一个问题的解决. 一直对GDB多线程调试接触不多,最近因为工作有了一些接触,简单作点记录吧. 先介绍一下GDB多线程调试的基本命令. 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID. 切换当前调试的线程为指定ID的线程.