线程是操作系统调度器可以调度的最小执行单元。一个进程包含一个或多个线程。同一进程中的多个线程共享进程的内存地址空间。线程间切换的代价要比进程小的多,因为线程是在进程内切换的。
多线程同时执行可以实现并发,提高系统的吞吐量,也可以提高响应能力。比如,顺序执行的程序可能会因为等待输入一直阻塞下去,而多线程则可以分配一个线程进行等待,其他线程继续执行其他任务,从而提高效率。
创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
thread
保存新创建的线程ID,attr
用来设置新创建线程的属性,start_routine
和arg
分别为执行新线程将运行的函数及参数
start_routine
必须包含如下特征
void *start_thread(void *arg);
创建成功返回0,失败返回error number
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *work(void *arg) {
printf("work function\n");
printf("arg is %d", *(int *)arg);
return NULL;
}
int main() {
pthread_t tid;
int arg = 666;
int ret = pthread_create(&tid, NULL, work, (void *)&arg);
if (ret < 0) {
perror("pthread_create");
exit(1);
}
printf("new thread id is %ld\n", tid);
sleep(1);
return 0;
}
pthread_create
用来创建一个线程,属性为默认,线程转去执行work函数,传入一个参数,主线程通过sleep等待work执行完成最后结束。这里有一个技巧,如果work需要传入多个参数,可以封装一个结构体,传入整个结构体实现。
编译时需要添加-lpthread
退出线程
#include <pthread.h>
void pthread_exit(void *retval);
函数通过retval
传递信息给调用者。不会返回调用者,而且永远不会失败。
join线程
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
函数阻塞等待tid为thread
的线程结束,前提是目标线程是可回收的,retval
保存线程退出状态。如果被等待线程已经结束,函数立即返回。执行成功返回0,失败返回error number
detach线程
#include <pthread.h>
int pthread_detach(pthread_t thread);
默认情况下,线程是创建成可join的。但是,线程也可以detach(分离),使得线程不可join。当分离的线程终止时,它的资源会自动释放回收,而不需要另一个线程join等待。成功时,pthread_detach
会分离thread
指定的线程,并返回0。
终止线程
#include <pthread.h>
int pthread_cancel(pthread_t thread);
函数可以通过指定thread
来终止线程
不过,接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,分别由两个函数完成:
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
这两个函数的第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消),第二个参数分别记录线程原来的取消状态和取消类型。
以上是关于线程的一些基本函数,下面看几个示例
一,创建一个线程,执行work函数,传入一个地址,线程执行函数先睡眠两秒,再继续执行。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *work(void *arg) {
sleep(2); //睡眠两秒
printf("gaoyuelong is %d years old!\n", *(int *)arg);
return NULL;
}
int main() {
pthread_t tid;
int arg = 18;
int ret = pthread_create(&tid, NULL, work, (void *)&arg);
if (ret < 0) {
perror("pthread_create");
exit(1);
}
arg = 19;
pthread_join(tid, NULL);
return 0;
}
程序运行,预期得到gaoyuelong is 18 years old!
,而实际输出为19岁,因为在线程函数睡眠的时候,参数地址中的内容已被修改。因此,实际得到与预期不符。而对于此类问题可以设置一个缓存区来保存信息,传参时传入缓存区的地址,或者直接传入值而不是地址。
二、创建两个线程,执行同一个work函数。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *work(void *arg) {
printf("%s\n", (char *)arg);
return NULL;
}
int main() {
pthread_t tid1, tid2;
char *msg1= "msg1";
char *msg2 = "msg2";
pthread_create(&tid1, NULL, work, (void *)msg1);
pthread_create(&tid2, NULL, work, (void *)msg2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
程序输出
msg2
msg1
或
msg1
msg2
两个线程执行顺序是不确定的,导致输出结果会不同,但printf
并没有输出异常的信息,因为它是个线程安全的函数,也就是并没有发生竞争。
一个函数被称为线程安全的,当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的
三、既然线程是共享同一进程内存空间,那么给定一个num,我们希望开两个线程,对这一个变量累加到100。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int num = 0;
void *work() {
while (1) {
if (num >= 100) break;
num++;
printf("num = %d\n", num);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, work, NULL);
pthread_create(&tid2, NULL, work, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
两个线程执行顺序是不确定的,因此可能会出现同一个值被加了两次的情况,因而数据发生错误。这时就需要用到线程同步,确保数据在某一时刻只能被一个线程访问,其他线程来了只能先等待。关于线程同步,将在下一篇总结。
多线程的程序可以提高效率,但随之而来的是编程的复杂性,从而也就导致了程序难懂,较难调试。
参考资料
[1] 游双.Linux高性能服务器编程[M].北京:机械工业出版社,2013.
[2] 龚奕利,贺莲译.深入理解计算机系统[M].北京:机械工业出版社,2016.
[3] 祝洪凯,李妹芳,付途译.Linux系统编程[M].北京:人民邮电出版社,2014.