线程初探
[1] 线程
线程
- 线程是计算机独立运行(操作系统分配CPU时间的基本单位)的最小单位,运行时占用很少的系统资源
- 单cpu单核:多个线程是交替执行的 多cpu多核:多个线程可以同时运行
- 同一进程内的多个线程共享进程的地址空间
- 线程之间的切换速度比进程的切换快很多
- 进程通信要以专门的通信方式、一个线程的数据可以直接供同一进程的其他线程使用
线程节约资源、节约时间、可以提高应用程序的响应速度、可以提高多处理器效率、改善程序的结构
线程在进程内部共享的资源:
- 地址空间、打开的文件描述符等待
线程的私有数据:
- 线程号
- 寄存器(程序计数器、堆栈指针)
- 栈
- 信号掩码
- 优先级
- 私有的存储空间
- 自己的错误返回码
线程有自己的栈但是共享 堆(heap)
We talked about the two types of memory available to a process or a thread, the stack and the heap. It is important to distinguish between these two types of process memory because each thread will have its own stack, but all the threads in a process will share the heap.
——————————————— What’s the Diff: Programs, Processes, and Threads
- A thread in execution works with
- thread ID
- Registers (program counter and working register set)
- Stack (for procedure call parameters, local variables etc.)
- A thread shares with other threads a process’s (to which it belongs to)
- Code section
- Data section (static + heap)
- Permissions
- Other resources (e.g. files)
关于线程的具体信息可以看这篇博客 Linux 线程的实质
[2] 创建线程
在主线程里创建线程后,程序会在创建线程的地方产生分支,变成两个程序来执行,一段代码可以被多个线程执行,线程在地位上是同等的,不存在父线程和子线程的概念
创建线程的函数:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数意义:
- thread : 指向线程标识符的指针
- attr : 可指定线程的属性,如过是NULL则为默认属性
- start_routinez : 是一个函数指针,指向线程创建后要运行的函数,称为线程函数
- arg : 指向要传递给线程函数的参数
返回值:
- 线程创建成功返回0
- 失败则返回出错编号
其他的系统调用:
#include <pthread.h>
pthread_t pthread_self(void); // 获取本线程的线程ID
int pthread_equal(pthread_t t1, pthread_t t2); // 比较线程ID
// 如果两个线程为同一线程返回非0值,否则返回0
int pthread_once(pthread_once_t *once_control,
void (*init_routine)(void)); // 保证线程函数只执行一次
pthread_once_t once_control = PTHREAD_ONCE_INIT;
// 成功完成后,pthread_once()将返回零; 否则,返回错误编号以指示错误。
[实例1]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int * thread(void * arg)
{
pthread_t newthid;
newthid = pthread_self(); // 返回新创建线程的ID
printf("this is a new thread, thread ID = %u\n", newthid);
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t thid;
printf("main thread, ID is %u\n", pthread_self());
// 打印主线程ID
if (pthread_create(&thid, NULL, (void *)thread, NULL) != 0)
// 创建一个线程, 创建成功返回 0
{
printf("thread creation failed\n");
exit(1);
}
sleep(1);
return 0;
}
编译时需要链接 libpthread.a
, gcc createthread.c -lpthread
在某些情况,只需要执行一次函数,这时就需要用到 pthread_once()
函数原型中的控制变量once_control必须初始化为 PTHREAD_ONCE_INIT
(0), 否则线程函数不会执行
/* This is similar to a lock implementation, but we distinguish between three
states: not yet initialized (0), initialization in progress, and initialization finished; If in the first state, threads will try to run the initialization by moving to the second state;
the first thread to do so via a CAS on once_control runs init_routine, other threads block.*/
[实例2]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_once_t once = PTHREAD_ONCE_INIT;
// 使用初值为PTHREAD_ONCE_INIT的once_control变量保证
// 函数在本进程执行序列中仅执行一次。
void run(void)
{
printf("Function run is running in thread %u\n", pthread_self());
}
void * thread1(void * arg)
{
pthread_t thid = pthread_self();
printf("Current thread's ID is %u\n", thid);
pthread_once(&once, run);
printf("thread1 ends\n");
}
void * thread2(void * arg)
{
pthread_t thid = pthread_self();
printf("Current thread's ID is %u\n", thid);
pthread_once(&once, run);
printf("thread2 ends\n");
}
int main()
{
pthread_t thid1, thid2;
pthread_create(&thid1, NULL, thread1, NULL);
pthread_create(&thid2, NULL, thread2, NULL);
sleep(3);
printf("main thread exit!\n");
exit(0);
}
在多线程编程环境下,如果pthread_once()调用出现在多个线程中,init_routine()函数仅执行一次,但究竟在哪个线程中执行是由内核调度来决定的。
两种情况:
线程属性
线程创建函数的第二个参数的类型为: pthread_attr_t, 结构体定义为:
typedef struct
{
int detachstate; // 线程的分离状态
int schedpolicy; // 线程的调度策略
int schedparam; // 线程的调度参数
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
int guardsize; // 线程栈末尾的警戒缓冲区大小
int stackaddr_set; // 堆栈地址集
int stackaddr; // 堆栈的大小
int stacksize; // 堆栈的大小
}pthread_attr_t;
如果第二个参数为空,线程会采用默认的属性,绝大多数情况下不需要为线程特殊指定其属性。线程的属性只能在线程创建的时候指定,线程创建完成之后其属性不能被更改。
默认属性:
属性 | 值 | 结果 |
---|---|---|
scope | PTHREAD_SCOPE_PROCESS | 新线程与进程中的其他线程发生竞争。 |
detachstate | PTHREAD_CREATE_JOINABLE | 线程退出后,保留完成状态和线程 ID。 |
stackaddr | NULL | 新线程具有系统分配的栈地址。 |
stacksize | 0 | 新线程具有系统定义的栈大小。 |
priority | 0 | 新线程的优先级为 0。 |
inheritsched | PTHREAD_INHERIT_SCHED | 新线程继承父线程调度优先级。 |
schedpolicy | SCHED_OTHER | 新线程对同步对象争用使用 Solaris 定义的固定优先级。线程将一直运行,直到被抢占或者直到线程阻塞或停止为止。 |
由于线程的属性是不透明的,所以不能直接修改,而需要使用一系列的函数来初始化、配置、销毁。
参考资料:
- [1]Creating a (Default) Thread
- [2]多线程编程指南 > 第 3 章 线程属性 > 属性对象
初始化/销毁线程属性
#include <pthread.h>
pthread_attr_t tattr;
int ret;
/* initialize an attribute to the default value */
ret = pthread_attr_init(&tattr);
int pthread_attr_destroy(pthread_attr_t *attr);
// 如果成功会返回0, 失败返回非0的错误代码
初始化线程属性的时候系统会给属性对象分配内存,为了避免内存泄漏,当线程属性不再使用后应当调用pthread_attr_destroy()来释放分配的内存,销毁线程属性并不会影响创建时候使用了该线程属性的线程。当线程属性被销毁之后可以重新初始化它,但对已销毁的线程属性对象的任何使用的结果都是未定义的。
线程栈大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
默认情况下由系统设定栈的大小,可以用命令来查看系统的默认设置 ulimit -s
设置栈的大小不是可移植的
线程分离属性
// set/get detach state attribute in thread attributes object
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// On success, these functions return 0; on error, they return a nonzero error number.
pthread_attr_setdetachstate()中detachstate的值有两种:
- PTHREAD_CREATE_DETACHED
- PTHREAD_CREATE_JOINABLE(默认属性)
如果是PTHREAD_CREATE_JOINABLE,需要调用 pthread_join 或 pthread_detach来释放资源(线程的描述信息和stack)。调用pthread_detach的过程是不可逆的。
可连接和分离的线程 | Joinable and Detached Threads
线程栈溢出保护区大小
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);
当我们使用线程栈超过了设定大小之后,系统还会使用部分扩展内存(guardsize大小)来防止栈溢出。
如果没有guardsize,当一个线程的栈溢出到其他的区域,该区域又是可写的,会造成不可预料的后果,而且覆盖了那部分内存之后也不会产生任何错误或者是信号。
这种情况很难确定应该为栈分配多大的内存。
当设置了guardsize之后,如果栈溢出后尝试写入这部分内存,会给进程发送信号并且终止它。
线程竞争CPU的范围
int pthread_attr_getscope(const pthread_attr_t *attr,int *contentionscope);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
POSIX 定义了两个值:
- PTHREAD_SCOPE_SYSTEM: 与系统中的所有线程一起竞争
- PTHREAD_SCOPE_PROCESS:只与同进程的线程竞争
线程调度策略
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
进程的调度策略和优先级属于主线程,设置进程的调度策略和优先级只会影响主线程的调度策略和优先级。每一个对等线程能够拥有它自己的独立于主线程的调度策略和优先级。
在 Linux 系统中,进程有三种调度策略:SCHED_FIFO(实时、先入先出)、SCHED_RR(实时、轮转) 和 SCHED_OTHER(正常、非实时)(默认属性)。
线程继承的调度策略
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
在 pthread 库中,提供了一个函数,用来设置被创建的线程的调度属性:是从创建者线程继承调度属性(调度策略和优先级以及竞争范围),还是从属性对象设置调度属性。该函数就是:
int pthread_attr_setinheritsched (pthread_attr_t * attr, int inherit) 当中,inherit 的值为下列值中的其一:
enum
{
PTHREAD_INHERIT_SCHED, //线程调度属性从创建者线程继承(默认属性)
PTHREAD_EXPLICIT_SCHED //线程调度属性设置为 attr 设置的属性
};
假设在创建新的线程时,调用该函数将參数设置为 PTHREAD_INHERIT_SCHED 时,那么当改动进程的优先级时。该进程中继承这个优先级而且还没有改变其优先级的所有线程也将会跟着改变优先级。
线程调度參数
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
//sched_param结构体
struct sched_param {
int sched_priority; /* Scheduling priority */
};
sched_priority 仅当调度策略为(SCHED_RR 或 SCHED_FIFO)时才有效,默认为0
[3] 线程终止
有两种终止线程的方式:
- 通过 return 从线程返回
- 调用函数 pthread_exit() 使线程退出
pthread_exit()
// pthread_exit - terminate calling thread
#include <pthread.h>
void c(void *retval);
两种特殊情况:
- 如果从 main() 函数返回或调用了 exit 函数退出了主线程,则整个进程将会终止,进程终端所有线程也会终止,故主线程不能过早的从 main 函数返回。
- 如果主线程调用了 pthread_exit 函数,仅仅是主线程消亡,进程不会结束,进程内的其他线程也不会结束直至所有的线程结束
线程的取消
线程可以创建也可以取消,一个线程可以向另一个线程发送结束请求,实现这种机制用到了一个函数:
// pthread_cancel - send a cancellation request to a thread
#include <pthread.h>
int pthread_cancel(pthread_t thread);
函数执行成功返回0,失败返回非零的错误码。
函数执行成功并不意味着所要请求的线程会被取消,另一个线程接受到取消的请求后,具体行为依赖于线程的类型和状态(tyep and state)。
线程的state和type可以通过下面两个函数来设置:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
state
enabled(默认)[PTHREAD_CANCEL_ENABLE]
如果是这种状态,线程会取消,但何时取消依赖于type的值
disabled[PTHREAD_CANCEL_DISABLE]
线程会继续执行
type:
deferred(默认)[PTHREAD_CANCEL_DEFERRED]--同步
state如果为enabled,线程会继续执行,当遇到取消点的时候退出
POSIX 标准规定了一些函数作为取消点,当线程调用这些函数的时候就会结束,具体函数可以查看 man 7 pthreads
asynchronous[PTHREAD_CANCEL_ASYNCHRONOUS]-异步
state为enabled的情况下,type为这个值意味着线程可以在任何时候取消,当线程收到取消请求后,通常会立即退出
临界资源的释放
虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。此外,还有许多变量、数据等都可以被若干进程共享,也属于临界资源。 对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区。
临界资源在一段时间内只能被一个线程所持有,当线程要访问临界资源的时候需要提出请求,如果该资源没有被使用则申请成功,否则等待。临界资源使用完后需要释放以供其他线程使用。当一个线程终止时,如果不释放线程所占有的临界资源,则该资源还会被认为被使用中,如果另一个线程在等待使用这个临界资源,那它可能会无限等待下去,形成了死锁。
Linux 系统提供了一对函数:pthread_cleanup_push()、pthread_cleanup_pop()
来自动释放资源,两个函数以宏定义提供,所以两个程序必须成对出现且位于同一代码段才能通过编译。
pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理,void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。
等待线程的结束
函数pthread_join()用来等待线程的结束,其函数原型为:
// pthread_join - join with a terminated thread
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
pthread_join() 的调用者会被挂起睡眠,等待thread线程的结束,如果retval不为NULL,则这个值即为调用pthread_exit()的参数,如果线程是被取消的,retval的值是PTHREAD_CANCELED。测试了一下如果线程是return返回的话retval的值为0;
一个线程只允许一个线程使用pthread_join来等待他的结束,否则第一个接收到的线程成功返回,其他调用这个函数的线程返回错误代码 ESRCH.
thread 这个线程的状态必须是可join的。
test>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void assisthread(void * arg)
{
printf("I am helping to do some jobs\n");
sleep(3);
pthread_exit(0);
}
int main(void)
{
pthread_t assistthid;
int status;
pthread_create(&assistthid, NULL, (void*) assisthread, NULL);
pthread_join(assistthid, (void* )&status);
printf("assisthread's exit is caused %d\n", status);
return 0;
}