1 线程
本节对应APUE第十一、十二章内容
1.1 线程概念
线程本质:一个正在运行的函数。
进程本质:加载到内存的程序。
进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源。
典型的UNIX进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计成在某一时刻能够做不止一件事,每个线程处理各自独立的任务。
每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。
1.1.1 POSIX线程接口
POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。
Pthreads定义了一套C语言的类型、函数与常量,它以pthread.h头文件和一个线程库实现。Pthreads API中大致共有100个函数调用,全都以pthread_开头,并可以分为四类:
- 线程管理,例如创建线程,等待(join)线程,查询线程状态等。
- 互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作
- 条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作
- 使用了互斥锁的线程间的同步管理
因此在编译时需要makefile的编译和链接选项:
CFLAGS+=-pthread # 编译选项
LDFLAGS+=-pthread # 链接选项
1.1.2 进程和线程
1.2 线程标识
就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
进程ID是用pid_t数据类型来表示的,是一个非负整数。线程ID是用pthread_t 数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。
因此需要一个函数来对两个线程ID进行比较:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
返回值:
相等返回非0,否则返回0
获取自身的线程id:
#include <pthread.h>
pthread_t pthread_self(void);
返回值:
调用线程的线程ID
8.3 线程创建
在传统 UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程(pthread)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。
新增的线程可以通过调用pthread_create函数创建。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
返回值:
成功返回0,失败返回errno
- thread:事先创建好的pthread_t类型的参数。成功时thread指向的内存单元被设置为新创建线程的线程ID。
- attr:用于定制各种不同的线程属性。APUE的12.3节讨论了线程属性。通常直接设为NULL。
- start_routine:新创建线程从此函数开始运行,无参数时arg设为NULL即可。形参是函数指针(该函数返回值和形参均为void*),因此需要传入函数地址。
- arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。
代码示例
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
//线程执行的函数
static void *pthread_fn(void *s)
{
printf("pthread_fn is working!\n");
return NULL;
}
int main()
{
pthread_t tid = 0;
int err = 0;
puts("Begin!");
err = pthread_create(&tid, NULL, pthread_fn, NULL);
if(err)
{
printf("pthread_create is err!\n");
return -1;
}
puts("end!");
return 0;
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ ./a.out
Begin!
end!
分析:线程的调度取决于调度器策略。创建线程后,新的线程还没来得及被调度,main线程就执行了return或exit(0)使得进程退出,所以新的线程并没有执行就退出了。
1.4 线程终止
1.4.1 终止方式
如果进程中的任意线程调用了 exit、_Exit 或者 _exit,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么,发送到线程的信号就会终止整个进程。
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流:
- 线程可以简单地从启动例程中返回,返回值是线程的退出码
- 线程可以被同一进程中的其他线程取消
- 线程调用pthread_exit
#include <pthread.h>
void pthread_exit(void *rval_ptr);
参数:
rval_ptr: 是一个无类型指针,
进程中其他线程可以通过调用pthread_join函数访问到这个指针。
eg:
//线程执行的函数
static void *pthread_fn(void *s)
{
printf("pthread_fn is working!\n");
pthread_exit(NULL);
//return NULL;
}
函数pthread_join用来等待一个线程的结束。相当于进程控制中的wait。(调用线程将会一直阻塞,直至指定的线程调用上述三种退出方式)
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
返回值:
成功返回0
失败返回错误编号
- thread:为被等待的线程标识符
- rval_ptr:为用户定义的指针,它可以用来存储被等待线程的返回值,即pthread_exit的参数。这是一个二级指针,因此传入的参数为一级指针的地址,如果不关心返回值则用NULL
代码示例1
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
//线程执行的函数
static void *pthread_fn(void *s)
{
printf("pthread_fn is working!\n");
pthread_exit(NULL);
//return NULL;
}
int main()
{
pthread_t tid = 0;
int err = 0;
puts("Begin!");
err = pthread_create(&tid, NULL, pthread_fn, NULL);
if(err)
{
printf("pthread_create is err!\n");
return -1;
}
pthread_join(tid, NULL);
puts("end!");
return 0;
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ ./a.out
Begin!
pthread_fn is working!
end!
代码示例2
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
//线程执行的函数
static void *pthread_fn1(void *s)
{
printf("pthread_fn1 is working!\n");
pthread_exit((void *)1);
//return NULL;
}
static void *pthread_fn2(void *s)
{
printf("pthread_fn2 is working!\n");
pthread_exit((void *)2);
}
int main()
{
pthread_t tid1 = 0;
pthread_t tid2 = 0;
int err = 0;
void *ptret = NULL;
puts("Begin!");
err = pthread_create(&tid1, NULL, pthread_fn1, NULL);
if(err)
{
printf("pthread1_create is err!\n");
return -1;
}
err = pthread_create(&tid2, NULL, pthread_fn2, NULL);
if(err)
{
printf("pthread2_create is err!\n");
return -1;
}
err = pthread_join(tid1, &ptret);
if(err)
{
printf("pthread1_join is err!\n");
return -1;
}
printf("thread1 exit code is %ld\n", (long)ptret);
err = pthread_join(tid2, &ptret);
if(err)
{
printf("pthread1_join is err!\n");
return -1;
}
printf("thread2 exit code is %ld\n", (long)ptret);
puts("end!");
return 0;
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ ./a.out
Begin!
pthread_fn1 is working!
thread1 exit code is 1
pthread_fn2 is working!
thread2 exit code is 2
end!
lei@ubuntu:~/Desktop/pthread$ ./a.out
Begin!
pthread_fn1 is working!
pthread_fn2 is working!
thread1 exit code is 1
thread2 exit code is 2
end!
1.4.2 栈的清理
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数(钩子函数)安排退出是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时,清理函数routine是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:
- 调用pthread_exit时;
- 响应取消请求时;
- 用非零execute参数调用pthread_cleanup_pop 时。
如果 execute 参数设置为0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次 pthread_cleanup_push调用建立的清理处理程序。
注意:这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用。pthread_cleanup_push 的宏定义可以包含字符{,这种情况下,在 pthread cleanup_pop 的定义中要有对应的匹配字符}。
代码示例
include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void *cleanup_fun(void *s)
{
puts(s);
}
static void *pthread_fun(void *s)
{
puts("thread is working!");
pthread_cleanup_push(cleanup_fun, "cleanup 1");
pthread_cleanup_push(cleanup_fun, "cleanup 2");
pthread_cleanup_push(cleanup_fun, "cleanup 3");
puts("push over!");
//成对出现
pthread_cleanup_pop(1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);
pthread_exit((void *)1);
}
int main()
{
pthread_t tid = 0;
int err = 0;
void *pret;
puts("begin!");
err = pthread_create(&tid, NULL, pthread_fun, NULL);
if (err)
{
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
pthread_join(tid, &pret);
printf("pthread exit code is %ld\n", (long)pret);
puts("end!");
return 0;
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ ./a.out
begin!
thread is working!
push over!
cleanup 3
cleanup 1
pthread exit code is 1
end!
1.4.3 线程的取消
多线程程序中,一个线程可以借助 pthread_cancel() 函数向另一个线程发送“终止执行”的信号,从而令目标线程结束执行。
pthread_cancel调用并不等待线程终止,它只提出请求。线程在取消请求发出后会继续运行,直到到达某个取消点(CancellationPoint)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。
与线程取消相关的函数有:
int pthread_cancel(pthread_t thread);
功能:
发送终止信号给thread线程
返回值:
如果成功则返回0,否则为非0值。
发送成功并不意味着thread会终止。
int pthread_setcanceltype(int type, int *oldtype)
功能:
设置本线程取消动作的执行时机
返回值:
成功返回0
失败返回错误编号
void pthread_testcancel(void)
功能:
在不包含取消点,但是又需要取消点的地方创建一个取消点,
以便在一个没有包含取消点的执行代码线程中响应取消请求
1.5 线程同步
1.5.1 概念和例子
五个基本的同步机制:
互斥量,读写锁,条件变量,自旋锁,屏障
互斥量要避免死锁,带有超时的互斥量
带有超时的读写锁
线程竞争的实例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM (RIGHT - LEFT + 1)
static void *thread_fun(void *s)
{
int i, j, mark;
i = *(int *)s;
mark = 1;
for(j = 2; j < i/2; j++) {
if(i % j == 0) {
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer.\n", i);
pthread_exit(NULL);
}
int main()
{
int i, err;
pthread_t tid[THREADNUM];
puts("begin!");
for(i = LEFT; i <= RIGHT; i++)
{
err = pthread_create(tid+(i-LEFT), NULL, thread_fun, &i);
if (err)
{
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
}
for(i = LEFT; i <= RIGHT; i++)
{
pthread_join(tid[i-LEFT], NULL);
}
puts("end!");
return 0;
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ ./a.out
begin!
3000017 is a primer.
3000077 is a primer.
3000047 is a primer.
3000181 is a primer.
3000181 is a primer.
end!
lei@ubuntu:~/Desktop/pthread$ ./a.out
begin!
3000017 is a primer.
3000017 is a primer.
3000199 is a primer.
3000181 is a primer.
end!
结果每次都不一样。
原因:线程发生了竞争。
创建线程时,main线程传递给函数thread_fun的参数&i是同一个地址,但是地址保存的值不相同。
解决竞争
定义一个结构体,成员为要计算判断的数,然后每次动态分配内存,将地址作为线程函数的参数即可。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM (RIGHT - LEFT + 1)
struct thr_arg_st{
int n;
};
static void *thread_fun(void *s)
{
int i, j, mark;
//先将void*强转为struct thr_arg_st *
i = ((struct thr_arg_st *)s) -> n;
mark = 1;
for(j = 2; j < i/2; j++) {
if(i % j == 0) {
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer.\n", i);
pthread_exit(s);//将s作为返回值
}
int main()
{
int i, err;
pthread_t tid[THREADNUM];
struct thr_arg_st *p;
void *ptr;
puts("begin!");
for(i = LEFT; i <= RIGHT; i++)
{
p = malloc(sizeof(*p));
if (p == NULL)
{
perror("malloc()");
return -1;
}
p->n = i;
err = pthread_create(tid+(i-LEFT), NULL, thread_fun, p);
if (err)
{
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
}
for(i = LEFT; i <= RIGHT; i++)
{
pthread_join(tid[i-LEFT], &ptr);
free(ptr);//释放动态分配的内存
}
puts("end!");
return 0;
}
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。同样,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。
当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。当然,这种行为是与处理器体系结构相关的,但是可移植的程序并不能对使用何种处理器体系结构做出任何假设。
图11-7描述了两个线程读写相同变量的假设例子。在这个例子中,线程A读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。当线程B在这两个存储器写周期中间读取这个变量时,它就会得到不一致的值。
为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。图11-8描述了这种同步。如果线程B希望读取变量,它首先要获取锁。同样,当线程A更新变量时,也需要获取同样的这把锁。这样,线程B在线程A释放锁以前就不能读取变量。
1.5.2 互斥量
可以使用 pthread 的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。
对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
只有将所有线程都设计成遵守相同数据访问规则的,互斥机制才能正常工作。操作系统并不会为我们做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。
互斥变量是用 pthread_mutex_t 数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用pthread_mutex_destroy。
相关函数:
- 初始化和销毁:
#include <pthread.h>
// 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 静态分配互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
返回值:
成功返回0
失败返回错误编号
- 加锁和解锁
#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锁住互斥量,返回0.
不能锁住互斥量,返回EBUSY
代码示例——20个线程读写一个文件
先向/tmp/out下入1,然后创建20个线程来读这个文件内容并加1,然后写入。期望内容为21
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define FNAME "/tmp/out"
#define THRNUM 20
#define LINESIZE 1024
static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINESIZE];
fp = fopen(FNAME, "r+");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
fgets(linebuf, LINESIZE, fp);
// 读完后将文件指针指向文件起始处
fseek(fp, 0, SEEK_SET);
sleep(1);
// 向文件写入内容
fprintf(fp, "%d", atoi(linebuf) + 1);
fclose(fp);
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid[THRNUM];
int i, err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(tid + i, NULL, thr_add, NULL);
if(err) {
fprintf(stderr, "pthread_create(): %s\n", strerror(err));
exit(1);
}
}
for(i = 0; i < THRNUM; i++)
pthread_join(tid[i], NULL);
//读取文件中数据
FILE *fp;
fp = fopen(FNAME, "r");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
char buf[1024];
fseek(fp, 0, SEEK_SET);
fgets(buf, LINESIZE, fp);
printf("buf = %s\n", buf);
fclose(fp);
exit(0);
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ echo 1 > /tmp/out
lei@ubuntu:~/Desktop/pthread$ ./a.out
buf = 2
lei@ubuntu:~/Desktop/pthread$ ./a.out
buf = 3
lei@ubuntu:~/Desktop/pthread$ ./a.out
buf = 4
分析:由于调度和竞争,线程读到文件内容1后,休眠1s,然后一起写入2,所以结果就为2。
补充
#include <stdlib.h>
int atoi(const char *str)
作用:
C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。
参数:
str -- 要转换为整数的字符串。
返回值:
该函数返回转换后的长整数,
如果没有执行有效的转换,则返回零。
代码示例——互斥量的使用
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define FNAME "/tmp/out"
#define THRNUM 20
#define LINESIZE 1024
//初始化互斥量,设置为常量PTHREAD_MUTEX_INITIALIZER,也可以使用init初始化
//PTHREAD_MUTEX_INITIALIZER只适用于静态分配的互斥量
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINESIZE];
fp = fopen(FNAME, "r+");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
//进入临界区加上互斥锁
pthread_mutex_lock(&mut);
fgets(linebuf, LINESIZE, fp);
// 读完后将文件指针指向文件起始处
fseek(fp, 0, SEEK_SET);
sleep(1);
// 向文件写入内容
fprintf(fp, "%d", atoi(linebuf) + 1);
fclose(fp);
//退出临界区后解锁
pthread_mutex_unlock(&mut);
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid[THRNUM];
int i, err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(tid + i, NULL, thr_add, NULL);
if(err) {
fprintf(stderr, "pthread_create(): %s\n", strerror(err));
exit(1);
}
}
for(i = 0; i < THRNUM; i++)
pthread_join(tid[i], NULL);
//读取文件中数据
FILE *fp;
fp = fopen(FNAME, "r");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
char buf[1024];
fseek(fp, 0, SEEK_SET);
fgets(buf, LINESIZE, fp);
printf("buf = %s\n", buf);
fclose(fp);
//销毁互斥锁
pthread_mutex_destroy(&mut);
exit(0);
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ echo 1 > /tmp/out
lei@ubuntu:~/Desktop/pthread$ cat /tmp/out
1
lei@ubuntu:~/Desktop/pthread$ ./a.out
buf = 21
代码示例3——使用互斥锁实现线程同步
需求:四个线程依次打印abcd
锁链式同步
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define PTHNUM 4
pthread_mutex_t mut[PTHNUM];
// 返回下一个线程的编号0~3,3的下一个为0
static int next(int n)
{
return (n + 1) == PTHNUM ? 0 : n + 1;
}
static void *pth_fun(void *p)
{
int c = 'a' + (int)p;//线程打印的字符
int n = (int)p;//线程编号
while(1)
{
//加锁
pthread_mutex_lock(mut + n);
write(1, &c, 1);
//释放下一个线程的锁
pthread_mutex_unlock(mut + next(n));
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[PTHNUM];
int i = 0, err;
for (i = 0; i < PTHNUM; i++)
{
//初始化互斥锁
pthread_mutex_init(mut + i, NULL);
//加锁
pthread_mutex_lock(mut + i);
err = pthread_create(tid + i, NULL, pth_fun, (void *)i);
if (err)
{
fprintf(stderr, "pthread_create:%s\n", strerror(err));
exit(1);
}
}
//释放打印a线程的锁
pthread_mutex_unlock(mut + 0);
alarm(5);
for (i = 0; i < PTHNUM; i++)
{
pthread_join(tid[i], NULL);
}
return 0;
}
1.5.3 线程池
线程数是有一定限制的,1.5.1节用201个线程来检测质数,本节利用线程池来解决。
使用互斥锁让四个线程争抢方式处理数据。
假设线程池提供4个线程来检测201个质数。设置临界区资源num,当:
- num = 0:当前没有任务
- num = -1:当前任务已经全部完成
- num = 300000~3000200:当前有一个任务,需要一个线程来接受任务
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM 4 //线程池中有四个线程
static int num; // 临界区资源
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER; // 静态互斥量定义
static void *thread_fun(void *s)
{
int i, j, mark;
//领取任务直至结束
while(1)
{
pthread_mutex_lock(&mut_num);
//循环检测是否有任务
while(num == 0)//没有任务
{
pthread_mutex_unlock(&mut_num);
sched_yield(); // 让出CPU
pthread_mutex_lock(&mut_num);
}
//拿到任务,判断是否为-1,是则结束,不是则处理任务后继续等待
if(num == -1)
{
//释放锁后再退出,防止死锁
pthread_mutex_unlock(&mut_num);
break;
}
//拿到任务
i = num;
//将任务置为0
num = 0;
pthread_mutex_unlock(&mut_num);
//线程做任务
mark = 1;
for(j = 2; j < i/2; j++) {
if(i % j == 0) {
mark = 0;
break;
}
}
if(mark)
printf("[%d]%d is a primer.\n", (int)s, i);
}
pthread_exit(NULL);
}
int main()
{
int i, err;
pthread_t tid[THREADNUM];
puts("begin!");
//创建四个线程
for(i = 0; i < THREADNUM; i++)
{
err = pthread_create(tid+i, NULL, thread_fun, (void *)i);
if (err)
{
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
}
//主线程下发任务
for(i = LEFT; i <= RIGHT; i++)
{
pthread_mutex_lock(&mut_num);
//循环检测任务是否被领走
while(num != 0)
{
pthread_mutex_unlock(&mut_num);
sched_yield(); // 让出CPU
pthread_mutex_lock(&mut_num);
}
//设置num,即下发任务
num = i;
pthread_mutex_unlock(&mut_num);
}//任务下发完毕
//设置num == -1 ,代表任务全部结束
pthread_mutex_lock(&mut_num);
//循环检测最后一个任务是否被领走
while(num != 0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
num = -1;
pthread_mutex_unlock(&mut_num);
//收尸
for(i = 0; i < THREADNUM; i++)
{
pthread_join(tid[i], NULL);
}
//销毁互斥锁
pthread_mutex_destroy(&mut_num);
puts("end!");
return 0;
}
执行结果:
lei@ubuntu:~/Desktop/pthread$ ./a.out
begin!
[0]3000029 is a primer.
[1]3000017 is a primer.
[0]3000073 is a primer.
[3]3000047 is a primer.
[2]3000061 is a primer.
[0]3000089 is a primer.
[3]3000103 is a primer.
[0]3000133 is a primer.
[2]3000131 is a primer.
[1]3000077 is a primer.
[0]3000161 is a primer.
[2]3000199 is a primer.
[3]3000181 is a primer.
end!
不足:该程序存在盲等,即查询法的不足,上游main线程一直在循环查看任务是否被领走,而下游一直在循环查看是否有任务。
通知法:上游将设置任务后,唤醒下游来处理任务。如果没有领走任务,则阻塞自己,等待下游来唤醒。
下游发现有任务,则领走任务,并唤醒上游;没有任务,则阻塞,等待上游来唤醒。
1.5.4 线程令牌桶
1.5.5 条件变量
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
在使用条件变量之前,必须先对它进行初始化。由pthread_cond_t数据类型表示的条件变量可以用两种方式进行初始化。可以把常量PTHREAD_COND_INITTALIZER赋给静态分配的条件变量但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对它进行初始化。在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化。
相关函数和作用:
初始化条件变量
#include <pthread.h>
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
// 动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
成功返回0
失败返回错误编号
阻塞当前线程,等待条件的成立
#include <pthread.h>
// 有时间的条件等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
// 条件等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
返回值:
成功返回0
失败返回错误编号
- cond:已初始化好的条件变量
- mutex:与条件变量配合使用的互斥锁
- abstime:阻塞线程的时间
调用两个函数之前,我们必须先创建好一个互斥锁并完成加锁操作,然后才能作为实参传递给 mutex 参数。两个函数会完成以下两项工作:
- 阻塞线程,直至接收到条件成立的信号
- 当线程被添加到等待队列上时,将互斥锁解锁,即释放mutex
也就是说,函数尚未接收到“条件成立”的信号之前,它将一直阻塞线程执行。注意,当函数接收到“条件成立”的信号后,它并不会立即结束对线程的阻塞,而是先完成对互斥锁的“加锁”操作,然后才解除阻塞。
两个函数都以“原子操作”的方式完成“阻塞线程+解锁”或者“重新加锁+解除阻塞”这两个过程。
解除线程的“阻塞”状态(唤醒)
#include <pthread.h>
// 唤醒所有的阻塞线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒所有正在的至少一个线程
int pthread_cond_signal(pthread_cond_t *cond);
返回值:
成功返回0
失败返回错误编号
对于被上面两个函数阻塞的线程,我们可以借助如上两个函数向它们发送“条件成立”的信号,解除它们的“阻塞”状态。
由于互斥锁的存在,解除阻塞后的线程也不一定能立即执行。当互斥锁处于“加锁”状态时,解除阻塞状态的所有线程会组成等待互斥锁资源的队列,等待互斥锁“解锁”。
代码示例1——查询法转通知法
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM 4 //线程池中有四个线程
static int num; // 临界区资源
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER; // 静态互斥量定义
static pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER; //静态条件变量的定义
static void *thread_fun(void *s)
{
int i, j, mark;
//领取任务直至结束
while(1)
{
pthread_mutex_lock(&mut_num);
//循环检测是否有任务
while(num == 0)//没有任务
{
//阻塞自己,并释放互斥锁
pthread_cond_wait(&cond_num, &mut_num);
}
//拿到任务,判断是否为-1,是则结束,不是则处理任务后继续等待
if(num == -1)
{
//释放锁后再退出,防止死锁
pthread_mutex_unlock(&mut_num);
break;
}
//拿到任务
i = num;
//将任务置为0
num = 0;
//唤醒等待该条件的所有线程
pthread_cond_broadcast(&cond_num);
pthread_mutex_unlock(&mut_num);
//线程做任务
mark = 1;
for(j = 2; j < i/2; j++) {
if(i % j == 0) {
mark = 0;
break;
}
}
if(mark)
printf("[%d]%d is a primer.\n", (int)s, i);
}
pthread_exit(NULL);
}
int main()
{
int i, err;
pthread_t tid[THREADNUM];
puts("begin!");
//创建四个线程
for(i = 0; i < THREADNUM; i++)
{
err = pthread_create(tid+i, NULL, thread_fun, (void *)i);
if (err)
{
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
}
//主线程下发任务
for(i = LEFT; i <= RIGHT; i++)
{
pthread_mutex_lock(&mut_num);
//循环检测任务是否被领走
while(num != 0)
{
//阻塞自己,并释放互斥锁
pthread_cond_wait(&cond_num, &mut_num);
}
//设置num,即下发任务
num = i;
//唤醒等待该条件的所有线程
pthread_cond_broadcast(&cond_num);
pthread_mutex_unlock(&mut_num);
}//任务下发完毕
//设置num == -1 ,代表任务全部结束
pthread_mutex_lock(&mut_num);
//循环检测最后一个任务是否被领走
while(num != 0)
{
//阻塞自己,并释放互斥锁
pthread_cond_wait(&cond_num, &mut_num);
}
num = -1;
//唤醒等待该条件的所有线程
pthread_cond_broadcast(&cond_num);
pthread_mutex_unlock(&mut_num);
//收尸
for(i = 0; i < THREADNUM; i++)
{
pthread_join(tid[i], NULL);
}
//销毁互斥量,条件变量
pthread_mutex_destroy(&mut_num);
pthread_cond_destroy(&cond_num);
puts("end!");
return 0;
}
代码示例2——打印abcd
条件变量+互斥锁 = 通知法同步
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define PTHNUM 4
static int num = 0;
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 返回下一个线程的编号0~3,3的下一个为0
static int next(int n)
{
return (n + 1) == PTHNUM ? 0 : n + 1;
}
static void *pth_fun(void *p)
{
int c = 'a' + (int)p;//线程打印的字符
int n = (int)p;//线程编号
while(1)
{
//加锁
pthread_mutex_lock(&mut);
while(num != n)
{
//判断是不是自己编号,是继续,不是则阻塞并释放锁
pthread_cond_wait(&cond, &mut);
}
write(1, &c, 1);
num = next(num);
//唤醒等待该条件的所有线程
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[PTHNUM];
int i = 0, err;
for (i = 0; i < PTHNUM; i++)
{
err = pthread_create(tid + i, NULL, pth_fun, (void *)i);
if (err)
{
fprintf(stderr, "pthread_create:%s\n", strerror(err));
exit(1);
}
}
alarm(5);
for (i = 0; i < PTHNUM; i++)
{
pthread_join(tid[i], NULL);
}
pthread_mutex_destroy(&mut);
pthread_cond_destroy(&cond);
return 0;
}
1.5.6 信号量
使用互斥量和条件变量可以实现信号量的功能。
1.6 线程属性
pthread 接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。通常,管理这些属性的函数都遵循相同的模式。
在所有调用pthread_create函数的实例中,传入的参数都是空指针,而不是指向pthread_attr_t结构的指针。可以使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。
可以使用pthread_attr_init函数初始化 pthread_attr_t 结构。在调用 pthread attr_init 以后,pthread_attr_t 结构所包含的就是操作系统实现支持的所有线程属性的默认值。
初始化和销毁
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
返回值:
成功返回0
失败返回错误编号
下图总结了 POSIX.1 定义的线程属性。POSIX.1 还为线程执行调度(Thread Execution Scheduling)选项定义了额外的属性,用以支持实时应用。下图同时给出了各个操作系统平台对每个线程属性的支持情况。
线程分离状态属性
线程分离:在我们使用默认属性创建一个线程的时候,线程是 joinable 的。 joinable 状态的线程,必须在另一个线程中使用 pthread_join() 等待其结束, 如果一个 joinable 的线程在结束后,没有使用 pthread_join() 进行操作, 这个线程就会变成”僵尸线程”。可以使用pthread_detach函数让线程分离。
当线程被设置为分离状态后,线程结束时,它的资源会被操作系统自动的回收, 而不再需要在其它线程中对其进行 pthread_join() 操作。
#include <pthread.h>
// 设置状态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 获取状态
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
返回值:
成功返回0
失败返回错误编号
detachstate:可以设置为以下属性
- PTHREAD_CREATE_DETACHED:线程分离状态
- PTHREAD_CREATE_JONINABLE:线程可joinable状态
线程的栈和栈大小
可以使用下列函数设置线程的栈属性。
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
返回值:
成功返回0
失败返回错误编号
对于进程来说,虚地址空间的大小是固定的。因为进程中只有一个栈,所以它的大小通常不是问题。但对于线程来说,同样大小的虚地址空间必须被所有的线程栈共享。如果应用程序使用了许多线程,以致这些线程栈的累计大小超过了可用的虚地址空间,就需要减少默认的线程栈大小。另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(stack frame),那么需要的栈大小可能要比默认的大。
如果线程栈的虚地址空间都用完了,那可以使用malloc或者mmap来为可替代的栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。由stackaddr 参数指定的地址可以用作线程视的内容范围中的最低可寻找地址,该地址与处理器结构相应的边界应对齐。当然,这要假设malloc和mmap所用的虚地址范围与线程栈当前使用的虚地址范围不同。
stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置。对于一个给定的处理器结构来说,如果栈是从高地址向低地址方向增长的,那么 stackaddr线程属性将是栈的结尾位置,而不是开始位置。
应用程序也可以通过下列函数读取或设置线程属性stacksize。
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
返回值:
成功返回0
失败返回错误编号
如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性时,选择的stacksize不能小于PTHREAD_STACK_MIN。
代码示例——测试线程数量的上限:
1.7 同步属性
线程的同步对象也具有属性。
1.7.1 互斥量属性
互斥量属性是用 pthread_mutexattr_t 结构表示的。在1.5.2节每次对互斥量进行初始化时,都是通过使用PTHREAD_MUTEX_INITTALIZER 常量或者用指向互斥量属性结构的空指针作为参数调用 pthread_mutex_init 函数,得到互斥量的默认属性。
对于非默认属性,可以使用下列函数进行初始化和销毁:
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
返回值:
成功返回0
失败返回错误编号
互斥量的三个主要属性:
- 进程共享属性
- 健壮属性(略)
- 类型属性
进程共享
在进程中,多个线程可以访问同一个同步对象。正如在1.5.2节看到的,这是默认的行为。在这种情况下,进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE。
但也存在这样的机制:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。
相关函数调用:
#include <pthread.h>
//获得进程共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);
//修改进程共享属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
返回值:
成功返回0
失败返回错误编号
pshared:这里的p指的就是进程process,进程共享
- PTHREAD_PROCESS_PRIVATE:进程独占互斥量(只有初始化的那个进程内的多个线程可用)
- PTHREAD_PROCESS_SHARED:进程共享互斥量(多进程中的多个线程可用)
类型属性
类型互斥量属性控制着互斥量的锁定特性。POSIX.1定义了4种类型。
- PTHREAD_MUTEX_NORMAL:一种标准互斥量类型,不做任何特殊的错误检查或死锁检测。
- PTHREAD_MUTEX_ERRORCHECK:此互斥量类型提供错误检查。
- PTHREAD_MUTEX_RECURSIVE :此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量将依然处于加锁状态,对它再次解锁以前不能释放该锁。
- PTHREAD_MUTEX_DEFAULT:此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。
上图不占用解锁是指解锁不是自己加的锁(解锁别人加的锁),例如打印abcd的程序(互斥量+条件变量)。
相关函数:
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
返回值:
成功返回0
失败返回错误编号
1.7.2 读写锁属性
唯一个属性称为进程共享属性
1.7.3 条件变量属性
Single UNIX Specification目前定义了条件变量的两个属性:进程共享属性和时钟属性。
相关函数:
#include <pthread.h>
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);
返回值:
成功返回0
失败返回错误编号
1.7.4 自旋锁属性
一个属性称为进程共享属性
1.7.5 屏障属性
目前定义的属性只有进程共享属性