linux之线程

本文深入探讨了Linux环境下的线程和进程的区别,强调线程作为轻量级进程的特点,指出线程间的共享资源与非共享资源,并详细阐述了线程创建、线程退出、线程同步与通信的相关函数,如pthread_create、pthread_exit、pthread_join和pthread_cancel等。此外,还讨论了线程分离和属性设置的重要性,以及在多线程编程中需要注意的问题。
摘要由CSDN通过智能技术生成

关于线程和进程的区别:进程和线程的详解和区别_StudyWinter的博客-CSDN博客_任务进程线程的区别

1 什么是线程

(1)轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone;

(2)从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表是相同的(下图区别进程)

(3)进程可以蜕变成线程;

(4)线程可看做寄存器和栈的集合;

(5)在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位

参考:《Linux 内核源代码情景分析》
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但!线程不同!两个线程具有各自独立的 PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个 PCB 共享一个地址空间。实际上,无论是创建进程的 fork,还是创建线程的 pthread_create,底层实现都是调用同一个内核函数clone。


如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。因此:Linux 内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数pthread_* 是库函数,而非系统调用。

线程概念:

进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
ps -Lf 进程号 # 查看进程的线程

打开浏览器

2 线程共享资源

1. 文件描述符表
2. 每种信号的处理方式
3. 当前工作目录
4. 用户 ID 和组 ID
5. 内存地址空间 (.text/.data/.bss/heap/共享库)

3 线程间非共享资源

1. 线程 id
2. 处理器现场和栈指针(内核栈)
3. 独立的栈空间(用户空间栈)
4. errno 变量
5. 信号屏蔽字
6. 调度优先级

4 线程的优缺点

优点:
        1. 提高程序并发性
        2. 开销小
        3. 数据通信、共享数据方便
缺点:
        1. 线程不稳定(第三方库函数实现)
        2. 线程调试困难
        3. 等待使用共享资源时造成程序运行速度变慢,主要是一些独占性的资源
        4. 线程的死锁,较长时间的等待或者资源竞争造成死锁

5 线程控制原语

编译的时候记得后面 -l pthread 毕竟第三方库实现。

5.1 pthread_self 函数

作用:获取线程 ID。其作用对应进程中 getpid() 函数。

pthread_t pthread_self(void); 
// 成功返回本线程id

5.2 pthread_create 函数

创建一个新线程,其作用,对应进程中fork()函数。

int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);
// 成功返回0,失败返回errno
// 参数一:表示传出参数,表示创建的子线程id
// 参数二:线程属性,传NILL表使用默认属性
// 参数三:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
// 参数四:参数三函数的参数,空传NULL

测试

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
    printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
    if (res < 0)
    {
        perror("pthread_create error\n");
        exit(1);
    }
    
    return 0;
}

执行

gcc test.c  -l pthread

可以看到,子线程的打印信息并未出现。原因在于,主线程执行完之后,就销毁了整个进程的地址空间,于是子线程就无法打印。简单粗暴的方法就是让主线程睡1秒,等子线程执行。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
    printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
    if (res < 0)
    {
        perror("pthread_create error\n");
        exit(1);
    }
    sleep(1);               // 在这里添加休眠
    return 0;
}

执行

5.3 循环创建多个子线程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

// 子线程回调函数
void *tfn(void *arg)
{
    int i = (int)arg;
    sleep(i);
    printf("-----I'm %d th thread: pid = %d, tid = %lu\n", i + 1,
           getpid(), pthread_self());
    return NULL;
}

int main(int argc, char *argv[])
{
    int i;
    int res;
    pthread_t tid; // 线程id
    for (i = 0; i < 5; i++)
    {
        res = pthread_create(&tid, NULL, tfn, (void *)i); // 创建线程
        if (res != 0)
        {
            perror("pthread_create error\n");
            exit(1);
        }
    }
    printf("-------main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    sleep(i);
    return 0;
}

执行

编译时会出现类型强转的警告。

5.4 线程间全局变量

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int var = 10;
// 子线程回调函数
void *tfn(void *arg)
{
    var = 100;
    printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    return NULL;
}
int main(int argc, char *argv[])
{
    printf("At first var = %d\n", var);
    pthread_t tid; // 子线程id
    int res = pthread_create(&tid, NULL, tfn, NULL);
    if (res != 0)
    {
        perror("pthread error\n");

        exit(1);
    }
    sleep(1);
    printf("After pthread_create, var = %d\n", var);
    return 0;
}

执行

可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。

5.5 pthread_exit退出

作用:将单个线程退出。

void pthread_exit(void *retval);
// 参数:retval 表示线程退出状态,通常传 NULL

比较

exit();         // 退出当前进程。
return:         // 返回到调用者那里去。
pthread_exit(): // 退出当前线程。

重点

在多线程的回调函数中加代码

1 exit函数

// 如果在回调函数里加一段代码:
if(i == 2)
{
    exit(0);
}

 执行

看起来好像是退出了第三个子线程,然而运行时,发现后续的4,5也没了。这是因为,exit是退出进程。

2 return

if(i == 2)
{
    return NULL;
}

执行

这样运行一下,发现后续线程不会凉凉,说明return是可以达到退出线程的目的。

然而真正意义上,return是返回到函数调用者那里去,线程并没有退出。

再修改一下,再定义一个函数func,直接返回那种

void *func(void){
    return NULL;
} 
if(i == 2)
{
    func();
}

执行

运行,发现1,2,3,4,5线程都还在,说明没有达到退出目的。

再次修改

void *func(void) {
    pthread_exit(NULL);
    return NULL;
} 

if(i == 2)
{
    func();
}

执行

编译运行,发现3没了,看起来很科学的样子。
pthread_exit表示将当前线程退出。放在函数里,还是直接调用,都可以。

5.6 pthread_join 函数(重)

作用:阻塞等待线程退出,获取线程退出状态其作用,对应进程中 waitpid() 函数;

补充:任意线程得到其他线程的pid都可以回收,没有父线程回收子线程的说法。而进程需要父进程回收子进程。

int pthread_join(pthread_t thread, void **retval);
// 阻塞 回收线程。
// thread: 待回收的线程id
// retval:传出参数。 回收的那个线程的退出值。
// 线程异常借助,值为 -1。
// 返回值:成功:0
// 失败:errno

下面这个是回收线程并获取子线程返回值的小例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>


struct thrd
{
    int var;
    char str[256];
};
// 回调函数
void *tfn(void *arg)
{
    struct thrd *tval;
    tval = malloc(sizeof(struct thrd)); // 申请空间
    tval->var = 100;
    strcpy(tval->str, "hello thread"); // 拷贝数据
    return (void *)tval;
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    struct thrd *retval;
    int res = pthread_create(&tid, NULL, tfn, NULL);
    if (res != 0)
    {
        perror("pthread_create error\n");
        exit(1);
    }
    res = pthread_join(tid, (void **)(&retval));
    if (res != 0)
    {
        perror("pthread_join error\n");
        exit(1);
    }
    printf("Child thread exit with var = %d, str = %s\n", retval->var, retval->str);
    pthread_exit(NULL);
}

执行

使用pthread_join函数将循环创建的多个子线程回收

这里tid要使用数组来存

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int var = 100;
void *tfn(void *arg)
{
    int i;
    i = (int)arg;
    if (i == 1)
    {
        var = 111;
        printf("I'm %dth pthread tid = %lu, var = %d\n", i,
               pthread_self(), var);
        return (void *)var;
    }
    else if (i == 3)
    {
        var = 333;
        printf("I'm %dth pthread tid = %lu, var = %d\n", i,
               pthread_self(), var);
        return (void *)var;
    }
    else
    {
        printf("I'm %dth pthread tid = %lu, var = %d\n", i,
               pthread_self(), var);
        return (void *)var;
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid[5];
    int i;
    int *res[5];
    // 循环创建多个子进程
    for (i = 0; i < 5; i++)
    {
        pthread_create(&tid[i], NULL, tfn, (void *)i);
    }
    // 循环回收多个子进程 
    for (i = 0; i < 5; i++)
    {
        pthread_join(tid[i], (void **)(&res[i]));
        printf("--------------%d 's res = %d\n", i, (int)(res[i]));
    }
    // 输出主线程 
    printf("I'm main pthread tid = %lu, var = %d\n", pthread_self(), var);
    return 0;
}

执行

5.7 pthread_cancel函数

作用:杀死(取消)线程,其作用,对应进程中 kill() 函数。

线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。


取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用
creat,open,pause, close,read,write… 执行命令 man 7 pthreads
可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。可粗略认为一个系统调用(进入内核)即为一个取消点。

如线程中没有取消点,可以通过调pthread_testcancel函数自行设置一个取消点。

int pthread_cancel(pthread_t thread);
// 杀死一个线程。 需要到达取消点(保存点)
// thread: 待杀死的线程id
// 返回值:成功:0
// 失败:errno

如果,子线程没有到达取消点, 那么 pthread_cancel 无效。我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。

小例子,主线程调用pthread_cancel杀死子线程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

// 回调函数
void *tfn(void *arg)
{
    while (1)
    {
        printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
        sleep(1);
    }
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    int res = pthread_create(&tid, NULL, tfn, NULL); // 创建线程
    if (res != 0)
    {
        fprintf(stderr, "pthread_create error:%s\n", strerror(res));
        exit(1);
    }
    printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    sleep(5);                  // 父线程睡5s
    res = pthread_cancel(tid); // 终止线程
    if (res != 0)
    {
        fprintf(stderr, "pthread_cancel error:%s\n", strerror(res));
        exit(1);
    }
    while(1);                     // 不退出
    pthread_exit((void *)0);
}

执行

可以看到,主线程确实kill了子线程。
这里要注意一点,pthread_cancel工作的必要条件是进入内核,如果tfn真的奇葩到没有进入内核,则pthread_cancel不能杀死线程,此时需要手动设置取消点,就是pthread_testcancel()

5.8 pthread_detach 函数

作用:实现线程分离,线程结束后,自动释放资源。无需pthread_join() 回收资源。

int pthread_detach(pthread_t thread);
// 设置线程分离
// thread: 待分离的线程id
// 返回值:成功:0
// 失败:errno

线程分离状态:指定该状态,线程主动与主控线程断开关系。

线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。进程若有该机制,将不会产生僵尸进程。

僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。也可使用 pthread_create 函数参 2(线程属性)来设置线程分离。

下面这个例子,使用detach分离线程,照理来说,分离后的线程会自动回收:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

// 子线程回调函数
void *tfn(void *arg)
{
    printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
    if (res != 0)
    {
        // printf("pthread_create error:%s\n", strerror(res));
        fprintf(stderr, "pthread_create error:%s\n", strerror(res));
        exit(1);
    }

    res = pthread_detach(tid); // 设置线程分离,分离完的程序可以自动回收
    if (res != 0)
    {
        // printf("pthread_detach error:%s\n", strerror(res));
        fprintf(stderr, "pthread_detach error:%s\n", strerror(res));
        exit(1);
    }
    sleep(1);

    res = pthread_join(tid, NULL); // 回收子线程
    printf("join res = %d\n", res);
    if (res != 0)
    {
        // printf("pthread_join error:%s\n", strerror(res));
        fprintf(stderr, "pthread_join error:%s\n", strerror(res));
        exit(1);
    }
    printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    pthread_exit((void *)0);
}

这里是最终版,使用fprintf函数和strerror函数。

执行

6 线程进程控制原语比对

进程线程
forkpthread_create
exitpthread_exit
waitpthread_join
killpthread_cancel
getpidpthread_self
 pthread_detach()

7 线程分离属性设置

线程属性:
设置分离属性。

pthread_attr_t attr; 
// 创建一个线程属性结构体变量

pthread_attr_init(&attr); 
// 初始化线程属性

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置线程属性为分离态

pthread_create(&tid, &attr, tfn, NULL); 
// 借助修改后的 设置线程属性 创建为分离态的新线程

pthread_attr_destroy(&attr); 
// 销毁线程属性

调整线程状态,使线程创建出来就是分离态,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

// 子线程回调函数
void *tfn(void *arg)
{
    printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    pthread_attr_t attr;                // 结构体变量
    int res = pthread_attr_init(&attr); // 创建分离
    if (res != 0)
    {
        fprintf(stderr, "pthread_attr_init error :%s\n", strerror(res));
        exit(1);
    }
    if(res != 0)
    {
        fprintf(stderr, "pthread_attr_init error :%s\n",
                strerror(res));
        exit(1);
    }

    res = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 设置线程属性为分离属性
    if (res != 0)
    {
        fprintf(stderr, "pthread_attr_setdetachstate error :%s\n",
                strerror(res));
        exit(1);
    }
    res = pthread_create(&tid, &attr, tfn, NULL); // 创建一个线程
    if (res != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n",
                strerror(res));
        exit(1);
    }
    res = pthread_attr_destroy(&attr); // 回收分离
    if (res != 0)
    {
        fprintf(stderr, "pthread_attr_destroy error :%s\n",
                strerror(res));
        exit(1);
    }
    sleep(1);                   // 保证子进程结束
    res = pthread_join(tid, NULL); // 阻塞回收,分离成功,这里应该回收失败
    if (res != 0)
    {
        fprintf(stderr, "pthread_join error :%s\n", strerror(res));
        exit(1);
    }
    
    printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    pthread_exit((void *)0);
}

 执行

如图,pthread_join报错,说明线程已经自动回收,设置分离成功。

8 线程属性注意事项(重)

1 主线程退出其他线程不退出,主线程应调用 pthread_exit

2 避免僵尸线程
pthread_join
pthread_detach
pthread_create 指定分离属性
被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

3 malloc 和 mmap 申请的内存可以被其他线程释放

4 应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit

5 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值