1.Linux 2个线程库
1.NGPT
2.NPTL(一般使用这个)
2.线程模型
工具环境和调度者的身份,线程可以分为内核线程和用户线程。
1.内核线程:
有的系统也称为LWP(轻量级线程),运行在内核空间,有内核来调度。
2.用户线程:
用户线程运行在用户空间,又线程库来调度。
当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程。可见,内核线程相当于用户线程运行的"容器".
一个进程可以拥有 M 个内核线程和 N 个用户线程,其中 M <= N 。并且在一个系统的所有进程中,M 和 N 的比值都是固定的。
按照 M:N 的取值,线程的实现方式可以分为3种:
1.完全在用户空间实现 :
完全在用户空间实现的线程无需内核的支持,内核甚至根本不知道这些线程的存在。
线程库负责管理所有的线程,比如线程的优先级,时间片等。线程库利用 longjmp 来切换线程的执行,使它们看起来是"并发"执行的。
但实际上内核仍然是把整个进程作为最小的单位来调度的。换句话说,一个进程的所有线程共享该进程的时间片,它们对外表现出相同的
优先级。因此,对于这种实现方式而言, N = 1, 即 M 个用户空间线程对应 1 个内核线程,而该内核线程实际上就是进程本身。
完全在用户空间实现的线程的有点是:
创建和调度线程都无需内核的干预,因此速度相当快。并且由于它不占用额外的内核资源,所以即使一个进程创建了很多线程,也不会
对系统的性能造成明显的影响。
其缺点是:
对于多处理器系统,一个进程的多个线程无法同时运行在不同的 CPU 上,因为内核是按照其最小调度单位来分配 CPU 的。此外,线程的
优先级只对同一个进程的线程有效,比较不同进程中的线程的优先级没有意义。
2.完全由内核调度
完全由内核调度的模式将创建,调度线程的任务都交给了内核,运行在用户空间的线程库无需执行管理任务,这与完全在用户空间实现的线程恰恰相反。
二者的优缺点也正好互换。较早的 Linux 内核对内核线程的控制能力有限,线程库通常还要提供额外的控制能力,尤其是线程同步机制,不过现代 Linux
内核已经大大增强了对线程的支持。完全由内核调度的这种线程实现方式满足 M:N = 1:1,即 1 个用户线程被映射为 1 个内核线程。
3.双层调度
双层调度模式是前两种模式的混合体:内核调度 M 个内核线程,线程库调度 N 个用户线程。
这种线程实现方式结合了前两种方式的优点:
不但不会消耗过多的内核资源,而且线程切换速度也很快,同时它可以充分利用多核处理器的优势。
3.Linux 线程库
1.LinuxThreads
LinuxThreads 线程库的内核线程是用 clone 系统调用创建的进程模拟的。clone 系统调用和 fork 系统调用的作用类似 :
创建调用进程的子进程。不过我们可以为 clone 系统调用指定 CLONE_THREAD 标志,这种情况下它创建的子进程与调用进程共享相同的
虚拟地址空间,文件描述符和信号处理函数,这些都是线程的特点。不过,用进程模拟内核线程会导致很多语义问题,如:
1.每个线程拥有不同的 PID,因此不符合 POSIX 规范
2.Linux 信号处理本来是基于进程的,但现在一个进程内部的所有线程都能而且必须处理信号
3.用户ID,组ID 对一个进程中的不同线程来说可能是不一样的。
4.程序产生的核心转储文件不会包含所有线程的信息,而只包含产生该核心转储文件的线程的信息
5.由于每个线程都是一个进程,因此系统允许的最大进程数也是最大线程数
LinuxThreads 线程库一个有名的特性是所谓的管理线程。它是进程中专门用于管理其他工作线程的。其作用包括:
1.系统发送给进程的终止信号由管理线程接收,管理线程再给其他工作线程发送同样的信号以终止它们
2.当终止工作线程或者工作线程主动退出时,管理线程必须等待它们结束,以避免僵尸进程
3.如果主线程先于其他工作进程退出,则管理线程将阻塞它,直到所有的其他工作线程都结束之后才唤醒它
4.回收每个线程堆栈使用的内存
管理线程的引入,增加了额外的系统开销。并且由于它只运行在一个 CPU 上,所有 LinuxThreads 线程库也不能充分利用多处理器的优势。
2.NPTL
相比 LinuxThreads ,NPTL 的主要优势在于:
1.内核线程不再是一个进程,因此避免了很多用进程模拟内核线程导致的语义问题
2.摒弃了管理线程,终止线程、回收线程堆栈等工作都可以由内核来完成
3.由于不存在管理线程,所有一个进程的线程可以运行在不同 CPU 上,从而充分利用了多核处理系统的优势
4.线程的同步由内核来完成。隶属于不同进程的线程之间也能共享互斥锁,因此可以实现跨进程的线程同步。
4. 线程函数
pthread_create(); // 创建线程
pthread_exit(); // 结束线程
pthread_join(); // 回收其他线程
pthread_cancel(); // 取消线程
pthread_setcancelstate(); // 设置线程的取消状态
pthread_setcanceltype(); // 设置线程的取消类型
pthread_attr_init();
pthread_attr_destroy();
pthread_attr_getdetachstate();
pthread_attr_setdetachstate();
pthread_attr_getstackaddr();
pthread_attr_setstackaddr();
pthread_attr_getstacksize();
pthread_attr_setstacksize();
pthread_attr_getstack();
pthread_attr_setstack();
pthread_attr_getquardsize();
pthread_attr_setquardsize();
pthread_attr_getschedparam();
pthread_attr_setschedparam();
pthread_attr_getschedpolicy();
pthread_attr_setschedpolicy();
pthread_attr_getinheritsched();
pthread_attr_setinheritsched();
pthread_attr_getscope();
pthread_attr_setscope();
线程同步的机制: POSIX 信号量,互斥量和条件变量
5. POSIX 信号量
sem_init();
sem_destroy();
sem_wait();
sem_trywait();
sem_post();
6.互斥锁
pthread_mutex_init();
pthread_mutex_destroy();
pthread_mutex_lock();
pthread_mutex_trylock();
pthread_mutex_unlock();
互斥属性:
pthread_mutexattr_init();
pthread_mutexattr_destroy();
pthread_mutexattr_getpshared();
pthread_mutexattr_setpshared();
7.条件变量
pthread_cond_init();
pthread_cond_destroy();
pthread_cond_broadcase();
pthread_cond_signal();
pthread_cond_wait();
8.多线程环境
1.可重入函数
如果一个函数能被多个线程同时调用且不发生静态条件,则我们称为它是线程安全的,或者说是可重入函数.
2.线程和进程
如果一个多线程的某个线程调用了 fork 函数,那么新创建的子进程是否将自动创建和父进程相同数量的线程呢。 否
子进程只拥有一个执行线程,它就是调用 fork 的那个线程的完成复制。并且子进程将自动继承父进程中的互斥锁(条件变量与之类似)。
也就是说,父进程中被加锁的互斥锁在子进程中也是被锁住的。
3.线程和信号
每个线程都可以独立的设置信号掩码。
pthread_sigmask();
进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。
因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个
线程中设置了某个信号的信号处理函数之后,它将覆盖其他线程为同一个信号设置的信号处理函数。这2点都说明,我们应该定义一个专门的线程来处理
所有的信号。可以通过如下2个步骤来实现:
1.在主线程创建出其他子线程之前就调用 pthread_sigmask 来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际
上所有的线程都不会响应被屏蔽的信号了
2.在某个线程中调用如下函数来等待信号并处理之:
sigwai();
14.1 线程概述
14.2 创建线程和结束线程
14.3 线程属性
14.4 POSIX 信号量
14.5 互斥量
14.6 条件变量
14.7 线程同步机制包装类
14.8 多线程环境