作者:倪震洋
1:进程标识符
#include <unistd.h>
pid_t getpid(void);
//返回值:调用进程的进程ID
pid_t getppid(void);
//返回值:调用进程的父进程ID
uid_t getuid(void);
//返回值:调用进程的实际用户ID
uid_t geteuid(void);
//返回值:调用进程的有效用户ID
gid_t getgid(void);
//返回值:调用进程的实际组ID
gid_t getegid(void);
//返回值:调用进程的有效组ID
【注意】这些函数都没有出错返回
2:fork函数
一个现有进程可以调用fork函数创建一个新进程
#include <unistd.h>
pid_t fork(void);
//返回值:子进程中返回0,父进程中返回子进程ID。出错返回-1
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。
注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段。
3:文件共享
fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。
父、子进程的每个相同的打开描述符共享一个文件表项。
在fork之后处理文件描述符有两种常见的情况:
(1):父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已执行了相应更新。
(2):父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。
fork有下面两种用法:
(1):一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的---父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2):一个进程要执行不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
1:线程标识
线程ID用pthread_t数据类型来表示
比较两个线程ID
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
//返回值:若相等则返回非0值,否则返回0
线程可以通过调用pthread_self函数获得自身的线程ID。
#include <pthread.h>
pthread_t pthread_self(void);
//返回值:调用线程的线程ID
2:线程的创建
调用pthread_create
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void) ,void *restrict arg);
//返回值:若成功则返回0,否则返回错误编号
当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。
attr参数用于定制各种不同的线程属性。设置NULL,创建默认属性的线程。
新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
3:线程终止
单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流。
(1):线程只是从启动例程中返回,返回值是线程的退出码。
(2):线程可以被同一进程中的其他线程取消。
(3):线程调用pthread_exit
#include <pthread.h>
void pthread_exit(void *rval_ptr);
rval_ptr 是一个无类型指针,与传给启动例程的单个参数类似。
进程中的其他进程可以通过调用pthread_join函数访问到这个指针。
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
//返回值:若成功则返回0,否则返回错误编号
--> 从控制流中得到退出状态
调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
如果线程只是从它的启动例程返回,rval_ptr将包含返回码。
如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。
当一个线程通过调用pthread_exit退出或者简单的从i动例程中返回时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。
pthread_create和pthread_exit函数的无类型指针参数能传递的数值可以不止一个,该指针可以传递包含更复杂信息的结构的地址,但是注意这个结构所使用的内存在调用者完成调用以后必须仍然是有效的,否则就会出现无效或非法内存访问。例如,在调用线程的栈上分配了该结构,那么其他的线程在使用这个结构时内存内容可能已经改变了。又如,线程在自己的栈上分配了一个结构然后把指向这个结构的指针传给pthread_exit,那么当调用pthread_join的线程试图使用该结构时,这个栈可能已经被销毁,这块内存也已另作他用。
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。
#include <pthread.h>
int pthread_cancel(pthread_t tid);
//返回值:若成功则返回0,否则返回错误编号
在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是,线程可以选择忽略取消方式或是控制取消方式。
线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。
这样的函数称为线程清理处理程序(thread cleanup handler)。
线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时的顺序相反。
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute)
当线程执行以下动作时调用清理函数,调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push函数来安排的。
(1)调用pthread_exit时。
(2)响应取消请求时。
(3)用非零execute参数调用pthread_cleanup_pop时。
如果execute参数置为0,清理函数将不被调用。无论哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。
在默认情况下,线程的终止状态会保存到对该线程调用pthread_join,如果线程已经处于分离状态,线程的底层存储资源可以在线程终止时立即被收回。当线程被分离时,并不能用pthread_join函数等待它的终止状态。对分离状态的线程进行pthread_join的调用会产生失败,返回EINVAL。pthread_detach调用可以用于使线程进入分离状态。
#include <pthread.h>
int pthread_detach(pthread_t tid);
//返回值:若成功则返回0,否则返回错误编号
线程同步需要锁
【互斥量】
可以通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。
互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。
如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,
第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,
只能回去再次等待它重新变为可用。
在这种方式下,每次只有一个线程可以向前执行。
线程执行的函数前会对互斥量加锁,之后,其它线程如果想对该函数加锁的话,就会被阻塞,直到互斥量的锁被释放
在设计时需要规定所有的线程必须遵守相同的数据访问规则,只有这样,互斥机制才能正常工作。
操作系统并不会做数据访问的串行化。
如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,
那么即使其他的线程在使用共享资源前都获得了锁,也还是会出现数据不一致的问题。
互斥量用pthread_mutex_t数据类型来表示,在使用互斥量以前,必须首先对它进行初始化,可以把他置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态的分配互斥量(例如通过调用malloc函数),
那么在释放内存前需要调用pthread_mutex_destroy。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//返回值:若成功则返回0,否则返回错误编号
要用默认的属性初始化互斥量,只需把attr设置为NULL。
对互斥量进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。
对互斥量解锁,需要调用pthread_mutex_unlock。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//返回值:若成功则返回0,否则返回错误编号
如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。
如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,
否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。
【避免死锁】
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。
当同时需要两个互斥量时,总是让它们以相同的顺序加锁,以避免死锁。
多线程的软件设计经常要考虑互斥锁的粒度问题
如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,源自并发性的改善微乎其微。
如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响,而且代码变得相当复杂。
【读写锁】
读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。
读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。
虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。
这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
读写锁非常适合于对数据结果读的次数远大于写的情况。当读写锁在写模式下时,它锁保护的数据结果就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读模式下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结果可以被多个获得读模式锁的线程读取。
读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。
与互斥量一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁。
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
要在读模式下锁定读写锁,需要调用pthread_rwlock_rdlock;要在写模式下锁定读写锁,需要调用pthread_rwlock_wrlock。
不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock进行解锁。
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//返回值:若成功则返回0,否则返回错误编号
有条件的读写锁原语的版本。
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
可以获得锁时,函数返回0;否则,返回错误EBUSY。
【条件变量】
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。
条件变量使用之前必须首先进行初始化,pthread_cond_t数据类型代表的条件变量可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_init函数进行初始化。
在释放底层的内存空间之前,可以使用pthread_mutex_destroy函数对条件变量进行去除初始化(deinitialize)。
#include <pthread.h>
int pthread_cond_int(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
//返回值:若成功则返回0,否则返回错误编号
使用pthread_cond_wait等待条件变为真,如果在给定的时间内条件不能满足,那么会生成一个代表出错码的返回变量。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
//返回值:若成功则返回0,否则返回错误编号
传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进行休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数相似,只是多了一个timeout。timeout值指定了等待的时间,它是通过timespec结构指定。时间值用秒数或者分秒数来表示,分秒数的单位是纳秒。
struct timespec{
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
使用这个结构时,需要指定愿意等待多长时间,时间值是一个绝对数而不是相对数。
例如,如果能等待3分钟,就需要把当前时间加上3分钟再转换到timespec结构,而不是把3分钟转换成timespec结构。
pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的所有线程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//返回值:若成功则返回0,否则返回错误编号
调用pthread_cond_signal或者pthread_cond_broadcast,也称为向线程或条件发送信号。必须注意一定要在改变条件状态以后再给线程发送信号。
网易云产品免费体验馆,无套路试用,零成本体验云计算价值。
本文来自网易实践者社区,经作者倪震洋授权发布