一. 线程函数
常用线程函数:
#include<pthread.h>
int pthread_equal(pthread_t tidl, pthread_t tid2); //线程比较
pthread_t pthread_self(void); //返回线程ID
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg); //新建线程,成功返回0
/* 新创建线程从start_rtn函数地址开始,该函数只有一个无类型指针参数arg,
新创建的线程ID被设置成tidp所指向的内存单元, attr参数用于定制线程属性*/
void pthread_exit(void *rval_ptr); //线程退出函数
int pthread_join(pthread_t thread, void **rval_ptr); //获取线程退出状态(代码)
int pthread_cancel(pthread_t tid); //请求取消同一进程中其他线程,仅仅代表提出申请,不一定得到响应
void pthread_cleanup_push(void (*rtn)(void *), void *arg);//清理处理函数
void pthread_cleanup_pop(int execute); //清理触发函数
/* 注:若线程从它的启动线程返回而终止的话,其清理程序不会调用。而且清理程序调用顺序是和启动顺序相反*/
pthread_cancel一直阻塞进程,直到进程返回或者终止后获得其返回码。
二. 线程传参问题
pthread_create和pthread_exit函数的无类型指针参数可以传递的值不止一个,即可传递包含复杂信息的结构的地址(例如结构体类型)。但是前提是分配给此线程的结构体所在的空间未被释放,否则将传递无效信息。示例如下:
#include "apue.h"
#include <pthread.h>
struct foo {
int a, b, c, d;
};
void
printfoo(const char *s, const struct foo *fp)
{
printf("%s", s);
printf(" structure at 0x%lx\n", (unsigned long)fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}
void *
thr_fn1(void *arg)
{
struct foo foo = {1, 2, 3, 4};
printfoo("thread 1:\n", &foo);
pthread_exit((void *)&foo);
}
void *
thr_fn2(void *arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void *)0);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_exit(err, "can't join with thread 1");
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
由上面程序可以看到,当第二次调用,printfoo()时,foo的内存地址已经释放,所以数据丢失了。结果如下:
**但这里存在一个问题,即如何将pthread 1中的数据经(void)数据类型传送给主函数?**解决方法在下面习题解释部分。
三. 线程和进程控制函数对比
进程原语 | 线程原语 | 描述 |
---|---|---|
fork | pthread_creat | 创建新的控制流 |
exit | pthread_exit | 从现有的控制流中退出 |
waitpid | pthread_join | 从控制流中获取退出状态 |
atexit | pthread_cancel_push | 退出流时执行的函数 |
getpid | pthread_self | 获取控制流ID |
abort | pthread_cancel | 请求控制流非正常退出 |
四. 互斥量
常用函数:
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
/* 互斥量初始化函数。mutex:互斥量 attr:初始化值*/
int pthread_mutex_destory(pthrea_mutex_t *mutex); //互斥量清理函数
int pthread_mutex_lock(pthread_mutex_t *mutex); //互斥量加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr); //设定加锁等待时间
五. 避免死锁
避免死锁的方法就是在程序运行现需要多个互斥量时,使程序必须按顺序申请互斥量,避免互相申请互斥量但得不到满足的情景。
下面是利用两个互斥量维护一个散列列表的程序:
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo *
foo_find(int id) /* find an existing object */
{
struct foo *fp;
pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
/* need to recheck the condition */
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* remove from list */
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}
这里简单介绍下这上面的函数
- foo_alloc
此函数在是每次调用时生成一个foo结构,并将其加入到NHASH散列表中。表中存放指向最新加入的foo的链表的头地址。 - foo_hold
此函数对f_count进行操作,每次进行自增操作是需要给当前foo加上锁。 - foo_find
此函数根据 id 计算出来的HASH值在NHASH散列表中寻找需要进行自增操作的foo - foo_rele
此函数是内存空间回收函数,通过判断foo中f_count的值是否大于0来决定是否释放其对应的foo内存。其中需要注意的是下面代码进行两次检查的原因是为了防止同步线程在执行过程中对f_count进行了自增操作。
解决方法就是将foo_find中的对单个foo加锁变成对整个散列表进行加锁。不过这样会导致程序执行效率下降。所以在程序设计时需要考虑效率和程序复杂度。
if (fp->f_count == 1) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
/* need to recheck the condition */
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
..........
..........
六. 读写锁
常用函数:
#include<pthread.h>
#include<time.h>
int pthread_rwlock_init(pthread_rewlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); //初始化读写锁
int pthread_rwlock_destory(pthread_rwlock_t *rwlock); //释放读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //加读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //加写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //加读写锁
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr); //带有超时的读锁
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struce timespe *restrict tsptr); //带超时的写锁
七. 条件变量
条件变量函数:
#include<pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
/*条件变量初始化*/
int pthread_con_destory(pthread_cond_t *cond);//条件变量释放
int pthread_cond_wait(pthred_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 tsptr);
/*与上面相同,此外还可以设置超时时间*/
pthread_cond_wait函数将传递给互斥量作为其条件得保护量,调用者把锁住得互斥量传给函数,函数自动将调用线程放到条件等待条件列表上,对互斥量进行解锁。
八. 自旋锁
自旋锁用于锁被持有时间短,且线程不希望在重新调度上花太多时间。
自旋锁函数:
#include<pthread.h>
int pthread_spin_init(pthread_spinlock *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
九. 屏障
协调多个线程并行工作得同步机制。
函数:
#include<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count);
/* 屏障初始化 count:运行屏障数目 attr:屏障对象属性*/
int pthread_barrier_destroy(pthread_barrier_t *barrier);
利用屏障实现线程同步,使多线程配合执行排序任务,代码如下:
#include "apue.h"
#include <pthread.h>
#include <limits.h>
#include <sys/time.h>
#define NTHR 8 /* number of threads */
#define NUMNUM 8000000L /* number of numbers to sort */
#define TNUM (NUMNUM/NTHR) /* number to sort per thread */
long nums[NUMNUM];
long snums[NUMNUM];
pthread_barrier_t b;
#ifdef SOLARIS
#define heapsort qsort
#else
extern int heapsort(void *, size_t, size_t,
int (*)(const void *, const void *));
#endif
/*
* Compare two long integers (helper function for heapsort)
*/
int
complong(const void *arg1, const void *arg2)
{
long l1 = *(long *)arg1;
long l2 = *(long *)arg2;
if (l1 == l2)
return 0;
else if (l1 < l2)
return -1;
else
return 1;
}
/*
* Worker thread to sort a portion of the set of numbers.
*/
void *
thr_fn(void *arg)
{
long idx = (long)arg;
heapsort(&nums[idx], TNUM, sizeof(long), complong);
pthread_barrier_wait(&b);
/*
* Go off and perform more work ...
*/
return((void *)0);
}
/*
* Merge the results of the individual sorted ranges.
*/
void
merge()
{
long idx[NTHR];
long i, minidx, sidx, num;
for (i = 0; i < NTHR; i++)
idx[i] = i * TNUM;
for (sidx = 0; sidx < NUMNUM; sidx++) {
num = LONG_MAX;
for (i = 0; i < NTHR; i++) {
if ((idx[i] < (i+1)*TNUM) && (nums[idx[i]] < num)) {
num = nums[idx[i]];
minidx = i;
}
}
snums[sidx] = nums[idx[minidx]];
idx[minidx]++;
}
}
int
main()
{
unsigned long i;
struct timeval start, end;
long long startusec, endusec;
double elapsed;
int err;
pthread_t tid;
/*
* Create the initial set of numbers to sort.
*/
srandom(1);
for (i = 0; i < NUMNUM; i++)
nums[i] = random();
/*
* Create 8 threads to sort the numbers.
*/
gettimeofday(&start, NULL);
pthread_barrier_init(&b, NULL, NTHR+1);
for (i = 0; i < NTHR; i++) {
err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM));
if (err != 0)
err_exit(err, "can't create thread");
}
pthread_barrier_wait(&b);
merge();
gettimeofday(&end, NULL);
/*
* Print the sorted list.
*/
startusec = start.tv_sec * 1000000 + start.tv_usec;
endusec = end.tv_sec * 1000000 + end.tv_usec;
elapsed = (double)(endusec - startusec) / 1000000.0;
printf("sort took %.4f seconds\n", elapsed);
for (i = 0; i < NUMNUM; i++)
printf("%ld\n", snums[i]);
exit(0);
}
-
单处理器下对比使用多线程和单线程的执行结果如下:
多线程情况下执行时间减少了3s左右,提升了20%的效率 -
我们将处理器数目改为4个,再重复上面的实验,执行结果如下:
在4处理器的情况下,多线程执行时间从14s多减少到4s左右。对比同情况下的单线程执行时间,多线程效率提升了80%。由此可知,在现在多处理器情况下多线程能大大提升程序执行的效率。
十. 部分习题解答
11.1 如何实现线程之间利用无指针类型传参?
前面我们介绍了线程之间传参出现的问题,即传完参数后因为空间已经释放,主线程得到的数据丢失。原因就是自动变量会随着子函数的返回而释放其地址,所以这里我们用动态分配内存来替代自动变量,代码如下:
#include "apue.h"
#include <pthread.h>
struct foo {
int a, b, c, d;
};
void
printfoo(const char *s, const struct foo *fp)
{
printf("%s", s);
printf(" structure at 0x%lx\n", (unsigned long)fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}
void *
thr_fn1(void *arg)
{
struct foo *fp;
if((fp = malloc(sizeof(struct foo))) == NULL)
err_sys("Can't allocate memory");
fp->a = 1;
fp->b = 2;
fp->c = 3;
fp->d = 4;
printfoo("thread 1:\n", fp);
pthread_exit((void *)fp);
}
void *
thr_fn2(void *arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void *)0);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_exit(err, "can't join with thread 1");
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
上面主要就是把线程1中的自动变量变成动态分配的内存,下面为执行结果:
11.3 把图11-15技术应用于工作线程实例,会出现什么问题?
这里首先需要了解为什么要使用条件变量而不是只使用互斥量就行了。这里可以参考这边博文:为什么有了互斥量还需要条件变量
简单来说,只使用互斥量一直需要CPU去查询条件是否满足。而用到条件变量后,我们可以将等待线程挂起,直到条件满足后,线程会由其他线程唤醒,从而减少资源消耗。
这里会出现的问题是惊群效应,即队列发出处理信号消息后,会有多个挂起的进程响应,产生线程间的竞争,浪费CPU资源。
11.5 实现屏障需要什么同步原语?给出pthread_barrier_wait的一个实现。
首先考虑此函数必须实现的是多个线程到达同一个点,然后在这个点阻塞,直到线程达到一定数量后,发送信号让线程开始运行。现在问题是如何实现线程信号发送给pthread_barrier_wait函数。