Linux 进程中的线程

进程中的线程

1.1 线程的创建

创建一条POSIX线程非常简单,只需指定线程的执行函数即可,但函数接口看起来比较复杂,细节如下:

#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);

参数说明:
thread:新线程的TID
attr:线程属性,若创建标准线程则该参数可设置为NULL
start_routine:线程函数
arg:线程函数的参数
start_routine是一个函数指针,指向线程的执行函数,其参数和返回值都是 void *,使用示例代码如下:
// simpleThread.c

#include <pthread.h>
void *doSomething(void *arg)
{
    // ...
}
int main()
{
    // 创建一条线程,并让其执行函数 doSomething()
    pthread_t tid;
    pthread_create(&tid, NULL, doSomething, NULL);
    // ...
}

线程的各种接口单独放在线程库中,因此在编译带线程的代码时,必须要指定链接线程库phread,如下:
gec@ubuntu:~$ gcc simpleThread.c -o simpleThread -lpthread

1.2并发性

线程最重要的特性是并发,线程函数 doSomething() 会与主线程 main() 同时运行,这是它与普通函数调用的根本区别。需要特别提醒的是,由于线程函数的并发性,在线程中访问共享资源需要特别小心,因为这些共享资源会被多个线程争抢,形成“竞态”。最典型的共享资源是全局变量,比如以下代码:
// concurrency.c

#include <pthread.h>
int global = 100;
void *isPrime(void *arg)
{
    while(1)
    {
        // 一段朴素的代码
        if(global%2 == 0)
            printf("%d是偶数\n", global);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, isPrime, NULL);
    // 一条人畜无害的赋值语句
    while(1)
        global = rand() % 5000;
}

运行结果如下:

gec@ubuntu:~$ ./concurrency
4383是偶数
2777是偶数
492是偶数
492是偶数
2362是偶数
3690是偶数
59是偶数
3926是偶数
540是偶数
3426是偶数
4172是偶数
211是偶数
368是偶数
2567是偶数
1530是偶数
1530是偶数
2862是偶数
4067是偶数


gec@ubuntu:~$
可以看到结果错漏百出,原因就是因为线程之间的并发的,global随时都会被争抢,像这种多线程或多进程同时访问共享资源的情形,必须使用互斥锁、读写锁、条件量等同步互斥机制加以约束方可正常运行。========

1.3 线程的退出

与进程类似,当一条线程执行完毕其任务时,可以使用如下接口来退出:

#include <pthread.h>
void pthread_exit(void *retval);

其中,参数retval是线程的返回值,对应线程执行函数的返回值。若线程没有数据可返回则可写成NULL。
注意此函数与exit的区别:

pthread_exit(): 退出当前线程
exit(): 退出当前进程(即退出进程中的所有线程)

一个进程中各个线程是平行并发运行的,运行主函数main()的线程被称为主线程,主线程是可以比其他线程先退出的,比如:

#include <pthread.h>
void *count(void *arg)
{
    // 循环数数
    for(int i=0; ;i++)
    {
        printf("%d\n", i);
        usleep(200*1000);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, count, NULL);
    // 主线程先退出
    pthread_exit(NULL);
}

主线程退出后,其余线程可以继续运行,但请注意,上述代码中如果主线程不调用 pthread_exit() 的话,那么相当于退出了整个进程,则子线程也会被迫退出。

1.5 线程的接合

与进程类似,线程退出之后不会立即释放其所占有的系统资源,而会成为一个僵尸线程。其他线程可使用 pthread_join() 来释放僵尸线程的资源,并可获得其退出时返回的退出值,该接口函数被称为线程的接合函数:

#include <pthread.h>
int pthread_join(pthread_t tid, void **val);

接口说明:
若指定tid的线程尚未退出,那么该函数将持续阻塞。
若只想阻塞等待指定线程tid退出,而不想要其退出值,那么val可置为NULL。
若指定tid的线程处于分离状态,或不存在,则该函数会出错返回。
需要注意的是,包括主线程在内,所有线程的地位是平等的,任何线程都可以先退出,任何线程也可以接合另外一条线程。以下是接合函数的简单应用示例:

#include <pthread.h>
void *routine(void *arg)
{
    pthread_exit("abcd");
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, routine, NULL);
    // 试图接合子线程,并获取其退出值
    void *val;
    pthread_join(tid, &val);
    printf("%d\n", (char *)val);
}

其他线程API

2.4.1 获取线程TID

如下接口可以获取线程的ID号:

#include <pthread.h>
pthread_t pthread_self(void);

以上接口类似进程管理中的 getpid(),需要注意的是,进程的PID是系统全局资源,而线程的TID仅限于进程内部的线程间有效。当我们要对某条线程执行诸如发送信号、取消、阻塞接合等操作时,需要用到线程的ID。

2.4.2 线程错误码

线程函数对系统错误码的处理跟标准C库函数的处理方式有很大不同,标准C库函数会对全局错误码 errno 进行设置,而线程函数发生错误时会直接返回错误码。
以线程接合为例,若要判定接合是否成功,成功的情况下输出僵尸线程的退出值,失败的情况下输出失败的原因,那么实现代码应这么写:

void *val;
errno = pthread_join(tid, &val);
if(errno == 0)
    printf("成功接合线程,其退出值为:%ld", (long)val);
else
    printf("接合线程失败:%s\n", strerror(errno)); // 注意需包含头文件 string.h
或:
void *val;
errno = pthread_join(tid, &val);
if(errno == 0)
    printf("成功接合线程,其退出值为:%d", (int)val);
else
    perror("接合线程失败");

所有以 pthread_xxx 开头的线程函数,成功一律返回0,失败一律返回错误码。

2.4.3 函数单例

考虑这么一种情形:
假设某程序内含多条线程,这些线程使用信号量(不管是system-V信号量组还是POSIX信号量)进行协同合作,由于信号量使用前必须进行初始化,为了使程序性能最优,我们希望线程们启动时谁跑得快谁就对信号量执行初始化的工作,且要确保初始化的工作被严格执行一遍。

#include <pthread.h>
// 函数单例控制变量
pthread_once_t once_control = PTHREAD_ONCE_INIT;
// 函数单例启动接口
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

接口说明:
once_control是一种特殊的变量,用来关联某个函数单例,被关联的函数单例只会被执行一遍。
init_routine函数指针指向的函数就是只执行一遍的函数单例。
以下是示例代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
// 函数单例控制变量
pthread_once_t once_control = PTHREAD_ONCE_INIT;
void init_routine(void)
{
    printf("我会被严格执行一遍。\n");
}
void *f(void *arg __attribute__((unused)))
{
    pthread_once(&once_control, init_routine);
    pthread_exit(NULL);
}
int main()
{
    pthread_t tid;
    for(int i=0; i<20; i++)
    	pthread_create(&tid, NULL, f, NULL);
    pthread_exit(NULL);
}


版权声明:本文为m0_45463480原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。