Linux系统编程 多线程

什么是线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。 ———来源于百度百科。

并发
并发是指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果

并行
并行是指在同一时刻。有多条指令在处理器上同时执行

同步
彼此有依赖关系的调用不应该“同时发生”,而同步就是要阻止那些“同时发生”的事情

异步
异步的概念和同步对立,任何两个彼此独立的操作是异步的,他表明事情独立发生。

多线程的优势:
1.在多处理器中开发程序的并行性。
2.在等待慢速IO操作时,程序可以执行其他操作,提高并发性
3.模块化的编程,能更清晰的表达程序中独立事件的关系,结构清晰
4.占用较少的系统资源。

线程ID

线程 进程
标识符类型 pthread_t pid_t
获取Id ptherad_seif() getpid()
创建 pthread_create() fork()

获取进程,线程Id

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int main(){
    
    pid_t pid;

    pthread_t tid;

    pid = getpid();

    tid = pthread_self();

    printf("pid is %u, tid is %lu\n",pid,tid);

    return 0;
}

运行结果:
Linux系统编程 多线程

创建信线程

创建新的线程
int pthread_create(pthread_t *restrict tidp,
					const thread_attr_t *restrict attr,
					void *(&start_routine)(void*),
					void *restrict arg)
第一个参数:新线程id,如果成功则新线程的ID回调填充到tidp指向的内存中
第二个参数:线程属性(调度策略,继承性,分离性)
第三个参数:回调函数(新线程执行的函数)
第四个参数:回调函数的参数

返回值:成功返回0,失败返回-1;
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>

void *fun(void *args){
    (void*)args;
    int i;
    for(i=0;i<5;i++){
        printf("this is child thread i=%d\n",i);
    }
}

int main(){
    pthread_t tid;
    tid = pthread_self();
    int err;
    printf("newtid is 0X%lx\n",tid);

    err = pthread_create(&tid,NULL,fun,"new thread");

    if(err != 0){
        printf("create new threaad failed\n");
        return -1;
    }

    usleep(200);	//为了确保新线程执行,主线程睡眠。
    return 0;
}

注意:编译需要使用 -pthread这个库。
gcc -phread thread.c
运行结果:
Linux系统编程 多线程

主线程

  • 当C程序运行时,首先允许main函数,在线程代码中,这个特殊的执行流程被称为初始线程主线程
  • 主线程的特殊性在于,他在main函数返回的时候,会导致进程结束,进程内所有线程会结束。你可以在主线程中调用pthread_exit函数,这样进程就会等到所有线程结束时才会终止。
  • 住先吃接收参数的方式是通过args,argv。而普通的线程只有一个void*
  • 在绝大多数情况下,主线程在默认堆栈下运行,这个堆栈可以增长到足够的长度,而普通的线程的堆栈是收到限制的,一旦溢出就发发生错误。

案例——主线程传递结构体,子线程打印

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

struct student{
    int age;
    char name[20];
};
/**
 * pthread_create函数的第四个参数为需要传递的参数值。
 * 但是会被转换为void类型。在子线成函数里需要将void类型的参数强制转换为结构体。
*/

void *fun(void *arg){
    printf("student arg is %d, name is %s\n",((struct student*)arg)->age,((struct student*)arg)->name);

    return (void*)0;
}


int main(int args,char *argv[]){
    pthread_t tid;
    int ret;

    struct student stu;

    stu.age=20;
    memcpy(stu.name,"李白",20);		//结构体赋值用拷贝。

    ret = pthread_create(&tid,NULL,fun,(void*)(&stu));

    if(ret < 0){
        printf("create child thread failed\n");
    }

    int i;
    printf("main thread have %d args\n",args);
    for(i=0;i<args;i++){
        printf("main thread args is %s\n",argv[i]);
    }

    sleep(2);

    return 0;

}

运行结果:
Linux系统编程 多线程

线程的基本状态

状态 含义
就绪 线程能够运行,但是在等待可用的处理器
运行 线程正在运行,在多核系统中,可能同时有多个线程运行
阻塞 线程在等待处理器以外的操作
终止 线程从启动函数中返回,或者调用pthread_exit函数,获取被取消

线程状态关系图。
Linux系统编程 多线程

就绪
当线程刚被创建时就处于就绪状态,或者当线程被接触阻塞以后也会处于就绪状态。就绪的线程在等待一个可用处理器,当一个运行的线程被抢占时,它立刻又回到就绪状态。

运行
当一个处理器选中一个就绪的线程执行时,他立刻变成运行状态

阻塞
线程会在以下情况发生阻塞,试图加锁一个已被锁住的互斥量,等待某个条件变量,调用singwait等待尚未发生的信号,执行无法完成的I/O信号,由于内存页错误。

终止
线程通常启动函数中返回来终止自己,或者调用pthread_exit退出,或者取消线程。

线程的回收

线程的分离属性:
分离一个正在运行的线程并不影响他,仅仅是通知当前系统该线程结束时,其所属的资源可以回收,一个没有被分离的线程在终止运行时会保留他的虚拟内存,包括他们的堆栈和其它关系资源,有时这种线程被称为僵尸线程
创建线程时默认是非分离的

如果线程具有分离属性,线程终止时会立刻回收,回收将释放掉所有在线程终止时为释放的关系资源和进程资源包括保存线程返回值的内存空间,堆栈,保存寄存器的内存空间等

终止被分离的线程会释放所有资源,但是必须释放该线程所占用的资源由malloc或者mmap分配的内存可以在任何运行时候由任何线程释放,条件变量,互斥量,信号灯可以有任何线程销毁,只要他们被解锁了或者没有线程等待。但是只有互斥量的主人才能解锁它,所以在线程终止前,需要解锁互斥量。

线程的退出

  • 线程终止
    如果进程中的任意一个线程调用了exit,_Exit,_exit,那么整个进程就会终止。

普通的线程有以下3中方式退出,线程不会终止:
1.从启动例程中返回,返回值是线程的推出码
2.线程可以被同一进程中的其他线程取消
3.线程调用pthread_exit(void *rval)函数,rval是退出码。

案例——三种退出线程方式

#include <app.h>

void *fun(void *arg){
//如果输入参数是1,使用return退出
    if(strcmp("1",(char *)arg) == 0){
        printf("new thread return\n");
        return (void*)1;		
    }
//如果输入参数是2,使用pthread_exit退出
    if(strcmp("2",(char *)arg) == 0){
        printf("new thread pthread_exit\n");
        pthread_exit((void *)2);//参数是退出码
    }
//如果输入参数是1,使用exit退出
    if(strcmp("3",(char *)arg) == 0){
        printf("new thread exit\n");
        exit(3);//参数是退出码。
    }
}

int main(int args,char *argv[]){
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid,NULL,fun,(void*)argv[1]);

    if (ret != 0)
    {
        printf("create new thread failed\n");
        return -1;
    }

    sleep(2);
    printf("main thread\n");
    return 0;
    
}

编译
执行
使用return退出线程
Linux系统编程 多线程
可以看到主线程最好打印了日志

使用pthread_exit退出线程
Linux系统编程 多线程

使用exit退出线程。
Linux系统编程 多线程
可以看到,使用return和pthread_exit退出线程,主线程都会打印出日志,只有只用exit退出线程时主线程没有打印日志。说明:exit退出线程会终止进程

线程的连接

等待新线程的运行。
线程连接使用**pthread_join(pthread_t tid, void ral)
调用该函数的线程会一直阻塞,知道指定的线程tid调用pthread_exit,从启动例程返回或者被取消
参数tid是指定线程的id
参数ral是指定线程的返回码,如果线程被取消,那么ral被置为PTHREAD_CANCELED
该函数调用成功会返回0,失败返回错误码。

调用pthread_join函数会使指定的线程处于分离状态,如果指定线程已处于分离状态,那么调用就会失败pthread_detach可以分离一个线程,线程可以分离自己

int pthread_detach(pthread_t thread)
成功返回0,失败返回错误码。

线程的取消

int pthread_cancel(pthread_t tid)

取消指定id的线程,成功返回0,
但是取消只是发送一个请求码,并不意味着等待线程终止
而且发送成功也不是意味着tid一定会终止
  • 取消状态,就是线程对取消信号的处理方式,忽略或响应。线程创建时默认响应取消信号
int pthread_setcancelstate(int state,int *oldstate)
设置本线程对Cancel信号的反应,
state有两种值:
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_DISABLE

分别表示收到信号后设为CANCELD状态和忽略CANCEL信号继续运行,
oldState如果不为NULL则存入原来的cancel状态以便恢复。
  • 取消类型
    取消类型,是线程对取消信号的响应方式,立即取消或者延时取消。线程创建时默认延时取消。
int pthread_setcancelType(int type,int*oldtyep)
设置本线程取消动作和时机。type有两种取值:
PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONUS,
仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行
至下一个取消点在退出和立即执行取消动作;
oldtype如果不为null则存入原来的取消动作类型值。
  • 取消点
    取消一个线程,他通常需要被取消线程的配合。线程在很多时候会查看自己是否有取消请求,如果有主动退出,这些查看是否有取消的地方称为取消点。

很多地方都包含取消点,包括:
pthread_join(),pthread_testcancel(),pthread_coud_wait(),pthread_coud_timedwait(),sem_wait(),sigwait(),write,read大多数会阻塞系统的调度。

案例——线程取消

#include <app.h>

void *fun(void *arg){
    int statval;
    statval = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
    if(statval != 0){
        printf("set cancel state failed\n");
    }

    printf("Im new Thread\n");
    sleep(4);
    printf("about to cancel \n");
    statval = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);

    if(statval!=0){
        printf("set cancel state failed\n");
    }   
    printf("first cancel point\n");
    printf("second cancel point\n");
    return (void *)20;
}

int main(){
    pthread_t tid;

    int ret;

    int jval,cval;
    void *rval;

    ret = pthread_create(&tid,NULL,fun,NULL);

    if(ret != 0){
        printf("crate thread failed\n");
        return -1;
    }

    sleep(2);

    cval = pthread_cancel(tid);

    if(cval != 0){
        printf("cancel thread failed\n");
    }

    jval = pthread_join(tid,&rval);

    printf("new thread exit ccode is %ls\n",(int*)rval);

    return 0;
}

向线程发送信号

  • pthread_kill
int pthread_kill(pthread_t thread,int sig)
pthread_kill是向线程发送signal,还记得signal吗?大部分的signal默认动作
是终止线程的运行,所以,我们需要用sigaction()去抓取信号,并加上处理函数。

函数功能:
向指定Id的线程发送sig信号,如果线程代码内不做处理。则照信号默认的行为影响整个进程,也就是说,如果你给一个线程发送了SIGOUT但线程没有实现signal处理
函数则退出整个进程。

如果要获得正确行为,就需要在先出内实现sigactionl。

所以,如果int sig的参数不是0,那一定要清除到底干什么,而且一定要实现现充的信号处理函数,否则就会影响整个进程。,

如果int sig是0,这是一个保留信号,其实并没有发送信号,作用是用来判断线程是不是还活着。

返回值:成功是0 ,失败返回错误吗。
  • 信号处理
    进程信号处理:
int sigaction(int signum,const sigaction *act,struct sigaction *oldact)
给信号signum设置可以处理函数,处理函数在sigaction指定
act.sa_mask信号屏蔽字
act.sa_handler信号处理程序
int sigemptyset(sigset_t *set)信号清空集
int sigfillset(sigset_t *set)将所有信号加入信号集
int sigaddset(sigset_t *set,int signum)增加一个信号到信号集
int sigdelset(sigset_t *set,int signum)删除一个信号到信号集

多线程信号屏蔽处理

int sigprocmask(int how ,consy sigset_t *set, sigset *oldset)
int pthread_sigmask(int how,const sigset_t *set,sigset_t *oldset)
how = SIG_BLOCK:向当前信号掩码中添加set,其中set表示要阻塞的信号值。
sig_UNBLOCK:向当前的信号掩码中删除set,其中set表示要取消阻塞的信号组
SIG_SETMASK:将当前的信号掩码替换为set,其中set表示新的信号掩码。
在多线程中,新线程的当前掩码会继承创造它的那个线程的信号掩码

一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如SIGSEGV;另外不能被忽略处理的信号SIGKILL和SIGSTOP也无法被阻塞。

线程的清理

线程可以安排它退出时的清理操作,这与进程的可以使用atexit函数安排进程退出时需要调用的函数类似。这样的函数成为线程清理处理程序。线程可以建立多个清理处理程序处理程序记录在线程中,所以这些处理程序执行的顺序与他们注册的顺序相反。

pthread_cleanup_push(void(rtn),(void),void *args) 注册处理程序
第一个参数:清理程序函数名,传递参数
pthread_cleanup_pop(int excute) //清除处理程序

这两个函数成对的出现,否则编译失败。

当执行以下操作时调用清理函数,清理函数的参数由args传入。
1.调用pthread_exit
2.响应取消请求(请你来验证)
3.用非零参数调用pthread_cleanup_pop

  • 案例
#include <app.h>

void *first_clean(void *arg){
    printf("%s first clean\n",arg);

    return (void*)0;
}

void *second_clean(void *arg){
    printf("%s second clean\n",arg);
    return (void *)0;
}

void *fun1(void *arg){
    printf("im new thread1\n");
    pthread_cleanup_push(first_clean,"thread1");
    pthread_cleanup_push(second_clean,"thread1");

    pthread_cleanup_pop(1);
    pthread_cleanup_pop(0);

    return (void*)1;
}

void *fun2(void *arg){
    printf("im  new thread2\n");
    pthread_cleanup_push(first_clean,"thread2");
    pthread_cleanup_push(second_clean,"thread2");
    pthread_exit((void*)2);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);

    return (void *)2;
}

int main(){
    pthread_t tid1,tid2;
    int ret;

    ret = pthread_create(&tid1,NULL,fun1,NULL);

    if(ret != 0){
        printf("create new thread1 failed\n");

        return -1;
    }

    ret = pthread_create(&tid2,NULL,fun2,NULL);

    if(ret != 0){
        printf("create new thread2 failed\n");
        return -1;
    }

    sleep(4);
}

线程的同步

互斥量

当多线程共享内存的时候,需要每一个线程看到一个相同的视图。当一个线程修改变量时,而其他线程也可以读取或者修改这个变量就需要对这些线程同步,确保他们不会访问到无效的变量。

在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器的读和写这两个周期交叉时,这种潜在的不一致性就会出错。当然这与处理器无关,但是在可移植的程序中并不能对处理器做出任何假设。

  • 不同线程同时操作一个变量。
#include <app.h>

struct student{
    int id;
    int age;
    int name;
}stu;

int i;

void *fun1(void *arg){
    while (1)
    {
        stu.id = i;
        stu.age = i;
        stu.name = i;
        i++;

        if(stu.id != stu.age || stu.id != stu.name || stu.age != stu.name){
            printf("%d %d %d\n",stu.id,stu.age,stu.name);
            break;
        }
    }

    return (void*)0;   
}

void *fun2(void *arg){
        while (1)
    {
        stu.id = i;
        stu.age = i;
        stu.name = i;
        i++;

        if(stu.id != stu.age || stu.id != stu.name || stu.age != stu.name){
            printf("%d %d %d\n",stu.id,stu.age,stu.name);
            break;
        }
    }

    return (void*)1;   
}

运行结果:
Linux系统编程 多线程
可以看到不同线程去操作同一个变量,会导致变量不一样。

  • 互斥量的使用
    为了让线程访问数据不产生冲突,这里就需要对变量加锁,使得同一个时刻只有一个线程可以访问变量。互斥量本质就是锁,访问共享资源前对互斥量加锁,访问完成后解锁。

当互斥量加锁以后,其他所有需要访问该互斥量的线程都将阻塞。

当互斥量解锁以后,所有因为这个互斥量阻塞的线程都将变为就绪状态,第一个获得CPU的线程会获得互斥量,变为运行状态,而其他线程会继续变为阻塞状态,在这种方式下访问互斥量每次只有一个线程能向前执行。

互斥量用pthread_mutex_t类型数据表示,下使用之前需要对互斥量初始化。
/usr/include/bits/pthreadtypes.h

1.如果是动态分配的互斥量,可以调用pthread_mutex_init()函数初始化、
2.如果是静态分配得互斥量,可以把他设置为常量PTHREAD_MUTEX_INITALIZER
3.动态分配的互斥量在释放内存需要调用pthread_mutex_destory()

  • 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)

第一个参数:需要初始化的互斥量。
第二个参数:互斥量的属性,默认为NULL
  • 销毁
int pthread_mutex_destory(pthread_mutex_t *restrict mutex, pthread_mutex_t mutex = OTHREAD_MUTEX_INITALZER)
  • 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex)
成功返回0,失败返回错误码;
如果互斥量已经被锁住了,那么会导致该线程被阻塞。


int pthread_mutex_trylock(pthread_mutex+t *mutex)
成功返回0,失败返回错误。如果互斥量已经锁住了,不会导致线程阻塞。
  • 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
成功返回0,失败返回错误码

读写锁

  • 什么是读写锁,它与互斥量有什么关系?
    读写锁与互斥量类似,不过读写锁有更高的并发性,互斥量要么加锁要么不加锁,而且同一时刻只允许一个线程对其加锁,对于一个变量的读取,完全可以让多个线程同时进行操作。

pthread_rwlock_t
读写锁有三种状态,读模式下加锁,写模式下加锁,不加锁,一次只有一个线程可以占有写模式下的读写锁,但是多个线程可以同时占有读模式的读写锁

读写锁在写加锁状态时,在它被解锁之前,所有试图对这个锁加锁的线程都会阻塞。读写锁在读加锁状态时,所有试图以读模式对其加锁的线程都会获得访问权,但是如果线程希望以写模式对其加锁,它必须阻塞直到所有线程释放锁。

当读写锁——读模式加锁时,如果有线程试图以写模式对其加锁,那么读写锁会阻塞随后的读模式锁请求。这样可以避免读锁长期占用,在写锁打达不到请求。

读写锁非常适合对数据结构读写次数大雨写次数的程序,当它以读模式锁住时,是以共享的方式锁住的,当它以写模式锁住时,是以独占的模式锁住的。

  • 读写锁初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr)
  • 使用完必须销毁锁
int pthread_rwlock_destory(pthread_rwlock_t *rwlock)
成功返回0,失败返回错误码
  • 读模式加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
  • 写模式加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)
  • 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
成功返回0,失败返回错误码。
  • 读写锁实例
#include <app.h>

int num = 0;
pthread_rwlock_t rwlock;

void *fun1(void *arg){
    pthread_rwlock_rdlock(&rwlock);

    printf("thread1 num is %d\n",num);
    sleep(5);
    printf("thread1 is over\n");
    pthread_rwlock_unlock(&rwlock);

    return (void*)0;
}

void *fun2(void *arg){
    pthread_rwlock_rdlock(&rwlock);

    printf("thread2 num is %d\n",num);
    sleep(5);
    printf("thread2 is over\n");
    pthread_rwlock_unlock(&rwlock);

    return (void*)1;
}

int main(){
    pthread_t tid1,tid2;

    int ret;

    ret = pthread_create(&tid1,NULL,fun1,NULL);

    if(ret != 0){
        printf("create new thread1 failed\n");
        return -1;
    }

    ret = pthread_create(&tid2,NULL,fun2,NULL);
    if(ret != 0){
        printf("create new thread2 failed\n");
        return -1;
    }

    pthread_rwlock_init(&rwlock,NULL);

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);

    return 0;
}

运行结果:
线程1,线程2同时打印thread2 num is %d\n,休眠10s后同时打印thread2 is over。

条件变量

一个典型的实例
在一条生产线上有一个仓库,当生产者生产时需要锁住仓库,而消费者取产品的时候也要锁住仓库独占。如果生产者发现仓库满了,那么他就不能生产了,变成了阻塞状态。但是由于生产者独占仓库,消费者又无法进入仓库消费产品,这样就会造成一个僵死的状态。

我们需要一种机制,当互斥量被锁住以后发现当前线程还是无法完成自己的操作,那么它应该释放互斥量,让其他线程工作。
1.可以采用轮询的方式,不停的查询你需要的条件,
2.让系统帮你查询条件,使用条件变量pthread_coud_t coud

  • 条件变量初始化
int pthread_coud_t coud = PTHREAD_COUD_INTIALIZER
int pthread_coud_init(pthread_coud_t *restrict coud, const pthread_coudattr_t *restrict attr)
  • 条件变量销毁
int pthread_coud_destory(pthread_coud_t *coud)
  • 条件变量的使用
    条件变量的使用需要配合互斥量
int pthread_coud_wait(pthread_coud_t *restrict coud,pthread_mutex_t *restrict mutex)
1.使用pthread_coud_wait等待条件变为真,传递给pthread_coud_wait的互斥量对条件进行保护,调用者把锁住的互斥量传递给函数
2.这个函数将线程释放到等待条件的线程列表上,然后对互斥量进行解锁,这个是原子操作。当条件满足时这个函数返回,返回后继续对互斥量加锁。
int pthread_coud_timewait(pthread_coud_t *restrict coud,pthread_mutex_t *restrict mutex,const struct timespesc *restict abstime)
3.这个函数与pthread_cond_wait类似,只是多一个abstime,如果到了指定时间还不满足,那么必须返回。时间用下面的结构体表示

struct timespesc{
	time_t tv_sec;
	long tv_nsec;
}

注意:这个时间是绝对时间。例如你要等3分钟,就要把当前时间加上3分钟后传到abstimspec,而不是直接等待3分钟。


当调价满足时,需要唤醒等待条件的线程
int pthread_coud_broadscast(pthread_coud_t *coud)
//唤醒所有等待条件的线程
int pthread_coud_signal(pthread_coud_t *coud)
//唤醒等待条件的某一个线程

注意:
一定要在条件改变以后唤醒线程。

线程一次性初始化

有些东西有且只能执行一次,比如互斥量初始化。通常当初始化应用程序时,可以比较容易地将其放在main函数中。当你写一个库函数时,就不能在main函数中初始化了,你可以使用静态初始化,但使用一次初始(pthread_once_t)比较容易一些。

首先要定义一个pthread_once_t变量,这个变量要用宏PTHREAD_ONCE_INIT初始化。然后创建一个与控制变量相关的初始化函数。

pthread_once_t once_control = PTHREAD_ONCE_INIT

void init_routine(){
	//初始化互斥量
	//初始化读写锁
}

接下来就可以在任何时刻调用pthread_once函数

int pthread_once(phread_once_t *once_control,void(*int_rotine)(void));

功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证int_routine函数在本进程执行序列中仅执行一次。
在多线程编程的环境下,尽管pthread_once()调用会出现在多个线程中,int_routine函数仅执行一次,究竟在哪个线程中执行是不定的,
是由内核调度来决定的。

Linux Threads使用胡吃啥和条件变量保证由pthread_once()指定的函数执行且执行一次。实际一次性函数的执行有状态有三种:NEVER(0),IN_PROGRESS(1),DONE(2)。用once_control表示pthread_once()的执行状态。
1.如果once_control的初值为0,那么pthread_once表示未执行过,int_routine函数会执行。
2.如果once_control的初值为1,则由于所有pthread_once()都必须等待其中一个激发“已执行一次”信号,因此所有pthread_once()都会陷入永久的等待中,init_routine就无法执行
3.如果once_control设为2,则表示pthread_once函数已经执行过一次了,从而所有的pthread_once()都会立即返回。int_routine就没有机会执行。

当pthread_once函数成功返回,once_control就会被设置为2。

线程的属性

线程的属性用pthread_attr_t 类型结构表示,在创建线程时可以不用传入NULL,而是传入一个pthread_attr_t结构体,由用户自己设置线程属性。

pthread_attr_t类型对应应用程序是不透明的,也就是说应用程序不需要了解有关属性对象内部结构的任何细节,因而可以增加程序的可移植性。

线程属性

名称 描述
detachstate 线程的分离状态
guardsize 线程栈末尾的警戒区域大小(字节数)
stacksize 线程栈的最低地址
stacksize 线程栈的大小(字节数)
  • 并不是所有的系统都支持线程的这些属性,因此你需要检查当前系统是否支持你设置的属性。
  • 当然还有一些属性不包含pthread_attr_t结构中,例如:线程的可取消状态,取消类型,并发度。

pthread_attr_t结构体在使用之前需要初始化,使用完成之后需要销毁。

线程属性初始化
int pthread_attr_init(pthread_attr_t *attr)


线程属性销毁
int pthread_attr_destory(pthread_attr_t *attr)

如果在调用pthread_attr_init初始化属性的时候分配了内存空间,那么pthread_attr_destory将释放内存空间,除此之外,pthread_attr_destory还会用无效的值初始化pthread_attr_t对象,因此如果该属性对象被调用,会导致创建线程失败。

分离属性概念

分离一个正在运行的线程并不影响它,仅仅是通知当前系统该线程结束时,其所属的资源可以回收,一个没有被分离的先出在终止时会保留他的虚拟内存,包括他们的堆栈和其他系统资源。有时这种线程被称为“僵尸线程”。创建线程时是非分离的。

如果线程具有分离属性,线程终止时会立即被回收,回收将释放掉所有在线程终止时未释放的系统资源和进程资源,包括保存线程返回的内存空间,堆栈,保存寄存器的内存空间。

如果在创建线程的时候就直接不需要了解线程的终止状态,那么可以修改pthread_attr_t结构体的detachstate属性,让线程以分离状态启动。可以使用pthread_attr_setdetachstate函数来设置线程的分离状态属性。线程的分离属性有两种合法值。

PTHREAD_CREATE_DETACHED 分离的
PTHREAD_CREATE_JOINABLE:非分离的,可连接的。

//设置线程的分离属性
int pthread_attr_setdatachstate(pthread_attr_t *attr,int detachstae)

int pthread_attr_getdatachstate(pthread_attr_t *attr,int *detachstae)
//获取线程的分离属性

设置线程分离属性的步骤:
1.定义线程属性变量pthread_attr_t attr
2.初始化attr,pthread_attr_init(&attr)
3.设置线程为分离或非分离pthread_attr_setdetachstate(&attr,datachstae)
4.创建线程pthread_create(&tid,&attr,fun,NULL)
所有系统都支持线程的分离状态属性。

线程栈属性

对于进程来说,虚拟地址空间的大小时固定的,进程中只有一个是固定的,进程中国只有一个栈,因此它的大小通常不是问题。但对线程来说同样的虚拟地址被所有的线程共享。如果应用程序使用了太多线程,致使线程累计超过可用的虚拟地址空间,这个时候就需要减少线程的默认大小。另外,如果线程分配了大量的自动变量或线程的栈帧太深,那么这个时候需要的栈要比默认的大。

如果用完了虚拟内存空间,那么可以使用malloc或者mmap来为其它栈分配空间,并修改栈的位置

修改栈属性
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize)

获取栈属性
int pthread_attr_getstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize)

参数stackaddr是栈的内存单元最低地址,参数stacksize是栈的大小。你需要注意stackaddr并不一定是栈的开始。
对于一些处理器,栈的地址是从高往低的,那么这是stackaddr是栈的结尾。

当然也可以单独获取或者修改栈的大小,而不会修改栈的地址。对于栈大小的设置,不能小于PTHREAD_STACK_MIN(需要头文件limits.h)

int pthread_attr_setstacksize(pthread_attr_t *attr,sizetstacksize)

int pthread_attr)getstacksize(pthread_attr_t *attr,sizestacksize)

对于栈大小可以在创建线程之后,还可以修改。

对于通道POSIX标准的系统来说,不一定要支持栈线程的栈属性,因此你需要
1.在编译阶段使用。
_POSIX_THREAD_ATTR_STACKADR 和 _POSIX_THREAD_ATTR_STACKSIZE宏来检查系统是否支持线程属性,

2.在运行阶段。
_SC_THREAD_ATTR_STACKADDR 和 _SC_THREAD_ATTR_STACKSIZE传递给sysconf函数检查系统对线程属性的支持。

  • 栈为警戒区
    线程属性guardsize控制着线程栈末尾以后避免栈溢出扩展的内存大小,这个属性默认是PAGESIZE个字节,你可以把它设置为0,这样就不会提供警戒区缓冲区,同样的,如果你修改了stackaddr,系统会认为你自己要管理栈,警戒缓冲区会无效。
设置guardsize
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize)

获取guardsize
int pthread_attr_getguardsize(pthread_attr_t *attr,size_t guardsize)

线程的同步属性

互斥量的属性

就像有线程属性一样,线程的同步互斥量也有属性,比较重要的就是进程共享属性和类型属性,互斥量的属性用pthread_mutexattr_t类型的数据表示,当然在使用之前必须初始化,使用完成之后需要进行销毁。

互斥量初始化
int pthread_mutexattr_init(pthread_mutexattr_t *attr)

销毁互斥量
int pthread_mutexattr_destory(pthread_mutexattr_t *attr)
进程共享属性

进程共享属性有两种值:
PTHREAD_PROCESS_PRIVATE,这个是默认值,同一个进程中的多个线程访问一个同步对象PTHREAD_PROCESS_SHARED,这个属性可以使互斥量在多个进程中同步,如果互斥量在多个进程的共享内存区域,那么具有这个属性的互斥量可以同步多个进程

设置互斥量进程共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr,int *restrict pshared);

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared)

进程共享属性需要检测系统是否支持:可以检测宏**_POSIX_THREAD_PROCESS_SHARED**

类型属性

类型属性

互斥量类型 没有解锁时再次加锁 不占用是解锁 已解锁时解锁
PTHREAD_MUTEX_NORMAL 死锁 未定义 未定义
PTHREAD_MUTEX_ERRORCHECK 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误
PTHREAD_MUTEX_DEFAULT 未定义 未定义 未定义

读互斥量类型

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,int *restrict type)

int pthread_mutexattr_settype(pthread_mutexattr_t *attr ,int type)

读写锁的属性

条件变量的属性

线程的私有数据

在使用私有数据之前,你首先要创建一个与私有数据相关的键,要来获取对私有数据的访问权限。这个键的类型是pthread_key_t

int pthread_key_create(pthread_key_t *key,void(&destructor)(void*))

创建的键放在可以指向的内存单元。destructor要与键相关的拆构函数。当线程调用pthread_exit()或者使用return返回。
析构函数就会被调用。当析构函数调用的时候,他只有一个参数,这个参数是与key关联的那个数据地址(也就是你的私有数据)因此你可以在这个析构函数中将这个数据销毁。
键使用之后可以销毁,当键销毁之后,与他相关的数据并没有被销毁。
上一篇:26、怎么回收线程?有哪几种方法?


下一篇:Linux中的线程