操作系统中的进程和线程,C/C++,Java,Python中多线程的备忘
操作系统中的进程和线程(From CareySon)
区别
- 操作系统中进程拥有独立的内存地址空间和一个用于控制的线程,多个线程共享进程的内存地址空间。
- 进程是组织资源的最小单位,线程是安排CPU执行的最小单位。
- 线程共享地址空间,而进程共享物理内存,打印机,键盘等资源。
进程占有的资源 线程占有的资源 地址空间,全局变量,打开的文件,子进程,信号量,账户信息 栈,寄存器,状态 ,程序计数器 - 线程的优势
- 在需要多线程互相同步或互斥的并行工作时,分解为不同的线程可以简化编程模型。
- 线程比进程轻量,创建和销毁的代价更小。
- 线程在宏观上并行,但微观上串行。当某些线程等待资源时(例如IO操作),其它线程得以继续执行,避免整个进程阻塞,提高了CPU利用率。
- 当多CPU或CPU多核心时,微观上线程也是并行的。
操作系统实现线程模式
线程实现在用户空间下
- 每一个进程中都维护着一个线程表来追踪本进程中的线程,表中包含每个线程独占的资源,如栈,寄存器,状态等。当一个线程完成了其工作或等待需要被阻塞时,其调用系统过程阻塞自身,然后将CPU交由其它线程。
- 进程表由系统维护,操作系统只能看到进程,而不能看到线程。过去的操作系统大部分是这种实现方式,优势之一在于即使操作系统不支持线程,也可以通过库函数来支持线程。
- 优势:
- 在用户空间下进行进程切换的速度要远快于在操作系统内核中实现
- 在用户空间下实现线程使得程序员可以实现自己的线程调度算法。如进程可以实现垃圾回收器来回收线程。
- 当线程数量过多时,由于在用户空间维护线程表,不会占用大量的操作系统空间。
- 劣势:
- 当一个进程中的某一个线程进行系统调用时,比如缺页中断而导致线程阻塞,此时操作系统会阻塞整个进程,即使这个进程中其它线程还在工作。
- 假如进程中一个线程长时间不释放CPU,因为用户空间并没有时钟中断机制,会导致此进程中的其它线程得不到CPU而持续等待。
线程实现在操作系统中
- 操作系统知道线程的存在,线程表存在操作系统内核中。
- 所有可能阻塞线程的调用都以系统调用(System Call)的方式实现,相比在用户空间下实现线程造成阻塞的运行时调用(System runtime call)成本会高出很多。当一个线程阻塞时,操作系统可以选择将CPU交给同一进程中的其它线程,或是其它进程中的线程,而在用户空间下实现线程时,调度只能在本进程中执行,直到操作系统剥夺了当前进程的CPU。
- 线程回收利用:当一个线程需要被销毁时,仅修改标记位,而不是直接销毁其内容,当一个新的线程需要被创建时,也同样修改被“销毁”的线程标记位即可。
混合模式
- 将上述两种方式混合,用户空间中由进程管理自己的线程,操作系统内核中有一部分内核级别的线程。
多线程代码备忘
C/C++
Windows下的多线程
创建线程
需要库文件
windows.h // for HANDLE
process.h // for _beginthreadex()CreateThread(Windows提供的api接口)
1 | HANDLE CreateThread( |
参数解释:
- lpsa是安全属性结构,主要控制该线程句柄是否可为进程的子进程继承使用,默认使用NULL时表示不能继承。若想继承线程句柄,则需要设置该结构体,将结构体的bInheritHandle成员初始化为TRUE。
- cbStack表示的线程初始栈的大小,若使用0则表示采用默认大小(1M)。
- lpStartAddr表示线程执行的函数地址,多个线程可以使用同一个函数地址。
- lpvThreadParam是传给线程函数的参数。
- fdwCreate是控制线程的标志,CREATE_SUSPENDED表示线程创建后挂起暂不执行,直到调用ResumeThread(),0表示线程创建之后立即执行。
- lpIDThread返回了线程的ID,传入NULL表示不需要返回。
- CreateThread的返回值:成功返回新线程的句柄,失败返回NULL。
示例:
1 | DWORD WINAPI ThreadTest(LPVOID pM){ |
- _beginthreadex
为什么要用_beginthreadex而不是CreateHandle?
简单来说:
_beginthreadex在内部调用了CreateThread,在调用CreateHandle之前_beginthreadex做了很多的工作,从而使得它比CreateThread更安全。——1999年7月MSJ的《Win32Q&A》
这里有详细的原因
调用方法和CreateThread相同,需要强制类型转换,线程函数修改
1 | unsigned int __stdcall ThreadTest(PVOID pM){ |
线程同步
多线程具有异步性。线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。同步包括互斥,互斥是一种特殊的同步。
事件Event
- CreateEvent
1 | HANDLE CreateEvent( |
参数解释:
- 设定安全结构,默认NULL不能继承句柄。
- 确定事件为手动置位还是自动置位,TRUE表示手动置位,FALSE表示自动置位。自动置位时,对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。
- 表示事件的初始状态,TRUR表示已触发。
- 事件名称,NULL表示匿名事件。
- OpenEvent
1 | HANDLE OpenEvent( |
参数解释:
- 访问权限,一般使用EVENT_ALL_ACCESS。
- 事件句柄继承性,一般为TRUE。
- 事件名称,不同进程中的线程使用名称访问同一事件。
- 触发事件:
BOOL SetEvent(HANDLE hEvent);
重置事件:BOOL ResetEvent(HANDLE hEvent);
销毁事件:CloseHandle(HANDLE hEvent);
- 事件脉冲:
BOOL PulseEvent(HANDLE hEvent);
此函数相当于先调用SetEvent()再立刻调用ResetEvent(),对于手动置位事件,所有正处于等待状态下线程都变成可调度状态,对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。函数不稳定,因为在调用时无法确定哪些线程正处于等待状态下。
- 触发事件:
- Event同步的实例
1 | #include <stdio.h> |
信号量Semaphore
- CreateSemaphore
1 | HANDLE CreateSemaphore( |
参数解释:
- 安全控制,NULL不能继承句柄。
- 初始资源数量。
- 最大并发数量。
- 信号量名称,NULL为匿名信号量。
- OpenSemaphore
1 | HANDLE OpenSemaphore( |
参数解释:
- 访问权限,一般使用SEMAPHORE_ALL_ACCESS。
- 信号量句柄继承性,一般使用TRUE。
- 名称。
- ReleaseSemaphore
1 | BOOL ReleaseSemaphore( |
参数解释:
- 信号量句柄。
- 资源增加个数,大于0,且不超过最大资源数。
- 用于传出之前的资源计数,NULL为不传出。当前资源数量大于0,表示信号量触发,等于0表示资源耗尽,信号量处于末触发。对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,若大于0,减1后调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
线程互斥
多个线程必然共享某种资源,线程A在使用某资源时,其它需要使用该资源的线程都要等待。在一段时间内只允许一个线程访问的资源称为临界资源或独占资源,计算机中大多数物理设备,进程中的共享变量等待都是临界资源,它们要求被互斥的访问。进程中访问临界资源的代码称为临界区。(介绍来自MoreWindows)
原子操作Interlocked
参数自增:
LONG__cdeclInterlockedIncrement(LONG volatile* Addend);
参数自减:
LONG__cdeclInterlockedDecrement(LONG volatile* Addend);
LONG__cdec InterlockedExchangeAdd(LONG volatile* Addend, LONG Value);
反回运算后的值,第二个参数正负代表加减LONG__cdeclInterlockedExchange(LONG volatile* Target, LONG Value);
返回原来的值,value为新值原子操作部分来自《MoreWindows:原子操作 Interlocked系列函数》,其中对例子中出错的原理解释有问题,原博中对自增语句的三条汇编代码进行了分析:
第一条汇编将变量的值从内存中读取到寄存器中,第二条汇编将寄存器中的值与1相加,计算结果仍存入寄存器中,第三条汇编将寄存器中的值写回内存中。由于线程执行的并发性,很可能线程A执行到第二局时,线程B开始执行,线程B将原来的值又写入寄存器中,这样线程A所主要计算的值就被线程B修改了。
实际上,线程有自己独立的寄存器集合,切换线程时会保护现场。真正的原因可能是:1.假如A执行到第二句切换到B,B执行结束后继续执行A,寄存器会恢复到A的值,将B覆盖。 2.A和B读取了同一个值,自增操作后存入内存,相当于只执行了一次。
关键段CRITICAL_SECTION
- Init:
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
销毁:void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
进入:void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
离开:void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
- CS关键段仅可用于互斥,不可用于同步。其在WinNT.h中声明,结构体定义为:
1 | typedef struct _RTL_CRITICAL_SECTION { |
其中,第四个参数HANDLE OwningThread记录允许进入关键段的线程句柄,第三个参数RecursionCount表示拥有这个关键段的访问权限的线程对此关键段的获得次数,如果OwingThread记录的线程再次进入,EnterCriticalSection()函数会更新RecursionCount记录该线程进入的次数,并且立即让该线程进入。此时其它线程调用EnterCriticalSection()会被切换到等待状态,当LeaveCriticalSection()至当前拥有访问权限的线程进入次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。
- 配合使用的旋转锁
线程在访问和等待间切换需要较大开销,在EnterCriticalSection()时如果需要等待,线程会先使用spinlock循环一段时间,在此期间如果仍未获得进入权限才会被切换到等待状态。- 另一种初始化关键段方法:
1 | BOOL InitializeCriticalSectionAndSpinCount( |
- 修改关键段的旋转锁次数:
1 | DWORD SetCriticalSectionSpinCount( |
- 一个关键段互斥举例
1 | #include <stdio.h> |
互斥量Mutex
CreateMutex
1
2
3
4
5HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);参数解释:
- 第一个参数是安全结构,默认NULL不能继承句柄。
- 第二个参数为FALSE时创建Mutex时不指定所有权,若为TRUE则指定为当前的创建线程ID为所有者,其他线程访问需要先ReleaseMutex。
- 第三个参数用于设置Mutex名,为NULL时表示是匿名互斥量。
OpenMutex
1 | HANDLE OpenMutex( |
参数解释:
- 访问权限,一般使用MUTEX_ALL_ACCESS。
- 互斥量句柄继承性,多为TRUE。
- 名称。一个进程中的线程创建互斥量后,其它进程中的线程可以通过该函数来使用该互斥量。
- 请求一个互斥量的访问权:WaitForSingleObject()
释放一个互斥量的访问权:ReleaseMutex()
销毁互斥量:CloseHandle() - 互斥量是内核对象,和关键段一样,mutex会记录线程访问权限,因此mutex不能用于线程的同步。但互斥量可以处理多进程间各个线程的互斥,可以处理遗弃情况:当当前某个占有互斥量的线程在触发互斥量之前意外中止(遗弃),系统会自动将互斥量内部储存占有该互斥量的线程ID重置为0,并且递归计数器重置为0,选择一个等待状态的线程进行调度,此时这个被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0。
Linux下的多线程pthread_t
库函数和编译脚本
pthread.h
gcc/g++ filename.c/cpp -o thread -lpthread
遵循POSIX线程接口
线程创建
- pthread_create
1 | int pthread_create( |
参数解释:
- func为指向新线程运行函数的指针。
- arg为传给func的参数。
- 返回值为0表示成功,错误返回errcode。
线程等待和结束
- pthread_join 用来等待一个线程结束
int pthread_join(pthread_t thread,void ** retval);
其中thread为等待的进程,retval指向一个存储返回值的变量。一个线程不能被多个线程等待,否则第一个接收到信号的线程会成功返回,其它线程返回错误代码ESRCH。 - pthread_exit
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
调用此函数线程自发结束 - 被动结束pthread_cancel
int pthread_cancel(pthread_t thread);
此函数调用成功返回0
线程同步
互斥量pthread_mutex_t
- 初始化
- 赋值为常量PTHREAD_MUTEX_INITIALIZER
- 动态分配,pthread_mutex_init函数
int pthread_mutex_init (pthread_mutex_t *__mutex,__const pthread_mutexattr_t *__mutexattr);
- 销毁
int pthread_mutex_destroy (pthread_mutex_t *__mutex);
- 上锁和解锁
1 | int pthread_mutex_lock (pthread_mutex_t *__mutex); |
读写锁
- 适用:允许多个线程同时读取,但只能有一个线程写入。适用于读的次数远大于写的情况。
- 初始化和销毁
1 | int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,__const pthread_rwlockattr_t *__restrict __attr); |
成功返回值为0,否则为错误代码。
- 加锁和解锁
- 读加锁:
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock)
- 写加锁:
int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock)
- 解锁:
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock)
,读写均用此函数
- 读加锁:
条件变量pthread_cond_t
一篇简洁的介绍《Linux线程同步-条件变量》
和一段具体的使用了mutex和cond的代码《linux 多线程(条件变量) 》
Java
创建线程
继承Thread类
1
2
3
4
5
6 class classname extends Thread{
...
public void run(){
...
}
}
实现Runnable接口
1
2
3
4
5
6 class classname implements Runnable{
...
public void run(){
...
}
}
线程的生命周期
- 构造方法
Thread();
Thread(Runnable target);
Thread(Runnable target,String name);
Thread(String name);
具体方法见 JDK API 1.6(百度网盘)
- 启动:
void start();
- 休眠:
static void sleep();
- 礼让:
static void yield();
- 详细方法见JDK API。
线程同步
同步方法
- 对方法使用synchronized关键字修饰。java的每个对象都含有一个内置锁,使用此关键字修饰时内置锁会保护整个方法。
public synchronized void methodname(){}
调用方法前需要获得内置锁,否则处于阻塞状态。 - 可以修饰静态方法,此时调用静态方法会锁住整个类。
- synchronized不同地方的使用方法
同步代码块
- 使用synchronized关键字修饰代码块。
synchronized(object){}
同步操作开销较高,要尽量减少同步内容,通常同步关键代码块。
特殊域变量volatile
- volatile关键字为域变量提供了一种免锁机制。使用该关键字时等于告诉虚拟机此域可能被其他线程更新,每次使用该域需要重新计算,不能使用寄存器中的值。
- 在需要同步的变量前加上volatile,不能修饰final变量。
重入锁
- JavaSE5.0中加入java.util.concurrent。
常用方法:ReentrantLock()
lock()
unlock()
- 示例:
1 | class Test { |
ThreadLocal类
- 使用空间换时间的方式,为每个线程创建一个变量副本。
- 常用方法:
ThreadLocal()
,get()
,initialValue()
,set(T value)`,分别用于:创建一个线程本地变量,返回此线程当前副本中的值,返回副本初始值和设置当前副本值为value。 - 示例:
1 | class Test { |
Python
1 | import threading |
原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(https://forec.github.io/2015/08/18/多线程编程备忘/) 、作者信息(Forec)和本声明。