一 . 线程的概念
线程概念:
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
在linux下:
线程就是进程,轻量级的进程。
对于内核来说,线程就是进程。
主线程和子线程共享和不共享的区域
共享
.text
.bss
.data
堆
动态库加载区
环境变量
命令行参数
通信:全局变量,堆
不共享:
如果有五个线程
栈区被平分为5份
所以线程之间通信,不可以用局部变量(因为局部变量在栈里)
二. 线程相关操作与函数
1. 创建函数
pthread_create线程创建函数
int pthread_create(
pthread_t *tid,
const pthread_attr_t *attr,
void *(*start_rountn)(void *),
void *arg);
参数:
pthread_t *tid:传出参数,表新创建的子线程 id
const pthread_attr_t *attr:线程属性。传NULL表使用默认属性。
void *(*start_rountn)(void *):子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
void *arg:参3的参数。没有的话,传NULL
返回值:
成功:0
失败:errno
pthread_self函数获取线程id
pthread_t pthread_self(void);
获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
返回值:本线程id
示例1:创建线程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h> //这里是线程对应的头文件
void* myfunc(void* arg)
{
printf("child pthread id: %lu\n", pthread_self());
return NULL;
}
int main(int argc, const char* argv[])
{
// 创建子线程
pthread_t thid;
// 返回错误号
int ret = pthread_create(&thid, NULL, myfunc, NULL);
if(ret != 0)
{
printf("error number: %d\n", ret);
// 根据错误号打印错误信息
printf("error information: %s\n", strerror(ret));
}
printf("parent pthread id: %lu\n", pthread_self());
//这里sleep是因为,父线程和子线程也会竞争cpu。
//如果父线程先结束,那么则整个程序释放,子线程来不及执行,就结束了
sleep(1);
return 0;
}
运行结果:
示例2:循环创建多个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
void *tfn(void *arg){
int i = (int)arg;
sleep(i);
printf("--I'm %dth thread: pid = %d, tid = %lu\n",i+1, getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[]){
int i;
int ret;
pthread_t tid;
for(i=0;i<5;i++){
ret = pthread_create(&tid, NULL, tfn, (void *)i);
if (ret != 0) {
sys_err("pthread_create error");
}
}
sleep(i);
printf("I'm main, pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
运行结果:
如果将i取地址后再传入线程创建函数里,就是说
当前传的是:(void *)i
改成: (void *)&i
相应的,修改回调函数:int i = *((int *)arg)
运行代码,会出现如下结果:
如果多次运行都只有主线程的输出,将主线程等待时长从i改为大于6的数即可。因为子线程等待时间i是不定的,但都小于等于6秒,由于抢cpu时没抢过主线程,导致没有子线程的输出。错误原因在于,子线程如果用引用传递i,会去读取主线程里的i值,而主线程里的i是动态变化的,不固定。所以,应该采用值传递,不用引用传递。
2. 线程全局变量共享
子线程里更改全局变量后,主线程里也跟着发生变化。
运行结果:
3. 退出函数
pthread_exit退出线程
void pthread_exit(void *retval); 退出当前线程。
参数:retval:退出值。 无退出值时,NULL
比较:
exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
示例:pthresd_exit.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h> //这里是线程对应的头文件
void* myfunc(void* arg)
{
//打印子进程的id
printf("child pthread id: %lu\n", pthread_self());
for(int i=0;i<5;++i)
{
printf("child i = %d\n",i);
}
return NULL;
}
int main(int argc, const char* argv[])
{
// 创建子线程
//线程ID变量
int ret = pthread_create(thid, NULL, myfunc, NULL);
if(ret != 0)
{
printf("error number: %d\n",ret);
//打印错误信息
printf("%s\n",strerror(ret));
}
printf("parent pthread id: %lu\n", pthread_self());
//退出主线程
pthread_exit(NULL);
for(int i=0;i<5;++i)
{
printf("parent child i = %d\n",i);
}
return 0;
}
运行结果,由于pthread_exit()让主线程退出,所以主程序最后的循环部分,并不会执行
4. 回收线程
pthread_join函数:阻塞回收线程
int pthread_join(pthread_t thread, void **retval);
参数:
thread: 待回收的线程id
retval:传出参数。 回收的那个线程的退出值。
线程异常借助,值为 -1。
返回值:
成功:0
失败:errno
示例:pthread_join.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
struct thrd {
int var;
char str[256];
};
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
struct thrd *tval;
tval = malloc(sizeof(tval));
tval->var = 100;
strcpy(tval->str, "hello thread");
return (void *)tval;
}
int main(int argc, char *argv[])
{
pthread_t tid;
struct thrd *retval;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0)
sys_err("pthread_create error");
//int pthread_join(pthread_t thread, void **retval);
ret = pthread_join(tid, (void **)&retval);
if (ret != 0)
sys_err("pthread_join error");
printf("child thread exit with var= %d, str= %s\n", retval->var, retval->str);
pthread_exit(NULL);
}
编译运行,结果如下:最后返回了线程的返回值。
5. 杀死线程
pthread_cancel函数:杀死一个线程。
注意事项:在要杀死的子线程对应的处理函数内部
必须做一次系统调用。
int pthread_cancel(pthread_t thread);
参数:
thread: 待杀死的线程id
返回值:
成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg){
while (1) {
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
return NULL;
}
int main(int argc, char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(5);
ret = pthread_cancel(tid); // 终止线程
if (ret != 0) {
fprintf(stderr, "pthread_cancel error:%s\n", strerror(ret));
exit(1);
}
while (1);
pthread_exit((void *)0);
}
运行结果:
可以看到,主线程确实kill了子线程。这里要注意一点,pthread_cancel工作的必要条件是进入内核,如果tfn没有进入内核,则pthread_cancel不能杀死线程,此时需要手动设置取消点,就是pthread_testcancel()
6. 线程分离
pthread_detach函数设置线程分离,调用该函数之后,不需要在利用Pthread函数进行线程回收。子线程会自动回收自己的pcb。
int pthread_detach(pthread_t thread);
参数:
thread: 待分离的线程id
返回值:
成功:0
失败:errno
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
ret = pthread_detach(tid); // 设置线程分离` 线程终止,会自动清理pcb,无需回收
if (ret != 0) {
fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
exit(1);
}
sleep(1);
ret = pthread_join(tid, NULL);
printf("join ret = %d\n", ret);
if (ret != 0) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void *)0);
}
编译运行,结果如下:
线程分离后,系统会自动回收资源,用pthread_join去回收已经被系统回收的线程,那个线程号就是无效参数。