Linux编程入门三多线程

网友投稿 1071 2022-09-30

Linux编程入门三多线程

Linux编程入门三多线程

早期Linux不支持线程,直到1996年,Xavier Leroy等人才开发出第一个基本符合POSIX标准的线程库LinuxThreads,但其效率低而且问题多。自内核2.6开始,Linux才真正提供内核级的线程支持,并有两个组织致力于编写新的线程库:NGPT(Next Generation POSIX Threads,2003年放弃)和NPTL(Native POSIX Thread Library)。NPTL比LinuxThreads效率高,且更符合POSIX规范,所以它已经成为glibc的一部分。

线程可分内核线程和用户线程。内核线程,在有的系统上也称为LWP(Light Weight Process,轻量级进程),运行在内核空间,由内核调度;用户线程运行在用户空间,由线程库来调度。当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程。内核线程相当于用户线程允许的“容器”。一个进程可以拥有M个内核线程和N个用户线程,其中M小于等于N。并且在一个系统的所有进程中,M和N的比值都是固定的。按照M:N的取值,线程的实现方式可分为三种:

完全在用户空间实现完全由内核调度双层调度(two level scheduler)完全在用户空间实现的线程无须内核的支持,内核甚至根本不知道这些线程的存在。线程库负责管理所有执行线程,比如线程的优先级、时间片等。线程库利用longjmp来切换线程的执行,使它们看起来像是并发执行。但实际上内核仍然是把整个进程作为最小单位来调度。换句话说,一个进程的所有执行线程共享该进程的时间片,它们对外表现出相同的优先级。因此,对这种实现方式而言,N=1,即M个用户空间线程对应1个内核线程,而该内核线程实际上就是进程本身。完全在用户空间实现的线程的优点是:创建和调度线程都无须内核的干预,因此速度块。并且不占用额外内核资源,所以即使一个进程创建了很多线程,也不会对系统性能造成明显的影响。其缺点是:对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上,因为内核是按照其最小调度来分配CPU的。此外,线程的优先级只对同一个进程中的线程有效,比较不同进程中的线程的优先级没有意义。完全由内核调度的模式将创建、调度线程的任务都交给了内核,运行在用户空间的线程库无须执行管理任务。较早的Linux内核对内核线程的控制能力有限,线程库通常还要提供额外的控制能力,尤其是线程同步机制,不过现代Linux内核已经大大增强了对线程的支持。完全由内核调度的这种线程实现方式满足M:N=1:1,即1个用户空间线程被映射为1个内核线程。双层调度模式是前两种实现模式的混合体:内核调度M个内核线程,线程库调度N个用户线程。这种线程实现方式结合了前两种方式的优点:不但不会消耗过多的内核资源,而且线程切换速度也较快,同时它可以充分利用多处理器的优势。

在创建一个新的线程时,需要为这个线程建一个新的栈。多线程的进程在内存中有多个栈,多个栈之间以一定的空白区域隔开,以备栈的增长。

线程的创建

#include int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void *(*start_routine)(void *), void *arg);//参数:创建的线程id 线程参数 调用函数名 传入的函数参数

pthread_create函数第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。pthread_t的定义是​​typedef unsigned long int pthread_t​​ ,无符号长整型。attr参数用于指定各种不同的线程属性。新创建的线程从start_routine函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_routine函数传递的参数不止一个,那么需要把这些参数放到同一个结构中,然后把这个结构的地址作为arg的参数传入。若线程创建成功,则pthread_create函数的返回0,由thread指向的内存单元将被设置为新创建线程的线程ID;若创建失败,则返回出错编号,且*thread中的内容是未定义的。一个用户可打开的线程数量不能超过RLIMIT_NPROC软资源限制。此外,系统上所有用户能创建的线程总数也不能超过/proc/sys/kernel/threads-max内核参数锁定义的值。

等待线程or分离线程

int pthread_join(pthread_t thread, void **retval);

pthread_join函数用来等待一个线程的结束,第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

错误码

描述

EDEADLK

可能引起死锁。比如两个线程互相针对对方调用pthread_join,或者线程对自身调用pthread_join

EINVAL

目标线程是不可回收的,或者已经有其他线程在回收该目标线程

ESRCH

目标线程不存在

当不需要线程的终止状态时,可以分离线程(调用pthread_detach函数)若在线程创建的时候,就已经知道以后不需要使用线程的终止状态,可以在线程创建属性里面指定该状态,那么线程一开始就处于分离状态。

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *state);int pthread_attr_setdetachstate(pthread_attr_t *attr, int state);

使用pthread_detach分离线程

1 #include 2 #include 3 #include 4 #include 5 #include 6 void* tfnl(void* arg) 7 { 8 printf("the sub thread sleeping for 5 seconds\n"); 9 sleep(5);//休眠5秒,等待主线程将该线程设置为分离状态 10 printf("the thread done\n"); 11 return NULL; 12 } 13 int main(void) 14 { 15 int iRet; 16 pthread_t tid; 17 iRet = pthread_create(&tid,NULL,tfnl,NULL); 18 if(iRet) 19 { 20 printf("can't create thread %s\n", strerror(iRet)); 21 return iRet; 22 } 23 //设置线程为分离状态 24 iRet = pthread_detach(tid); 25 if(iRet) 26 { 27 printf("can't detach thread %s\n", strerror(iRet)); 28 return iRet; 29 } 30 31 iRet = pthread_join(tid,NULL);//由于状态分离,因此得不到线程的结束状态,此函数出错 32 if(iRet) 33 { 34 printf("thread has been detached\n"); 35 } 36 printf("the main thread sleeping for 8 seconds\n"); 37 sleep(8); 38 printf("the main thread done.\n"); 39 return 0; 40 }

线程创建属性里面指定分离

1 #include 2 #include 3 #include 4 #include 5 #include 6 void* tfnl(void* arg) 7 { 8 printf("the sub thread done\n"); 9 return NULL; 10 } 11 int main(void) 12 { 13 int iRet; 14 pthread_t tid; 15 pthread_attr_t attr; 16 iRet = pthread_attr_init(&attr); 17 if(iRet) 18 { 19 printf("can't init attr %s\n",strerror(iRet)); 20 return iRet; 21 } 22 iRet = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 23 if(iRet) 24 { 25 printf("can't set attr %s\n",strerror(iRet)); 26 return iRet; 27 } 28 iRet = pthread_create(&tid,&attr,tfnl,NULL); 29 if(iRet) 30 { 31 printf("can't create thread %s\n", strerror(iRet)); 32 return iRet; 33 } 34 iRet = pthread_attr_destroy(&attr); 35 if(iRet) 36 { 37 printf("can't destroy the attr %s\n", strerror(iRet)); 38 return iRet; 39 } 40 iRet = pthread_join(tid,NULL);//由于状态分离,因此得不到线程的结束状态,此函数出错 41 if(iRet) 42 { 43 printf("thread has been detached\n"); 44 } 45 printf("the main thread sleeping for 8 seconds\n"); 46 sleep(8); 47 printf("the main thread done.\n"); 48 return 0; 49 }

结束线程

一个线程的结束有三个途径:1.线程运行的函数结束2.通过pthread_exit了结自己3.其他线程调用pthread_cancel终止本线程

void pthread_exit(void *retval);

唯一的参数是线程运行的函数的返回代码

pthread_join和pthread_exit的区别如下:

pthread_join一般是主线程来调用,用来等待子线程退出,因为是等待,所以是阻塞的,一般主线程会依次添加所有它创建的子线程。pthread_exit一般是子线程调用,用来结束当前线程。子线程可以通过pthread_exit传递一个返回值,主线程通过pthread_join获得该返回值,从而判断该子线程的退出是正常还是异常。

#include int pthread_cancel(pthread_t thread);

异常终止一个线程,thread参数是目标线程的标识符。该函数成功时返回0,失败则返回错误码。不过,接收到取消请求的目标线程可以决定是否允许被取消以及如何取消。 分别由以下两个函数完成:

#include int pthread_setcancelstate(int state, int * oldstate);int pthread_setcanceltype(int type, int * oldtype);

举例

1 #include 2 #include 3 #include 4 class Hello{ 5 public: 6 static void* say_hello(void* args) 7 { 8 printf("thread id in pthread = %lu\n", pthread_self()); 9 arg_type arg_temp = *(arg_type*)args; 10 printf("hello from thread, arg_temp.a = %d, arg_temp.b = %s\n",arg_temp.a,arg_ temp.b); 11 pthread_exit((void*)1); 12 } 13 14 struct arg_type{ 15 int a; 16 char b[100]; 17 }hello_arg; 18 }; 19 int main() 20 { 21 pthread_t tid; 22 Hello hello_temp; 23 hello_temp.hello_arg.a = 10; 24 char temp[100] = "I am number one."; 25 strncpy(hello_temp.hello_arg.b,temp,sizeof(temp)); 26 27 int iRet = pthread_create(&tid, NULL, Hello::say_hello,&hello_temp.hello_arg); 28 if(iRet){ 29 printf("pthread_create error: iRet = %d\n", iRet); 30 return iRet; 31 } 32 void *retval; 33 iRet = pthread_join(tid,&retval); 34 if(iRet) 35 { 36 printf("pthread_join error: iRet = %d\n", iRet); 37 return iRet; 38 } 39 printf("retval = %ld\n",(long)retval); 40 return 0; 41 }

读取和设置线程属性

typedef struct{ int etachstate; //线程的分离状态 int schedpolicy; //线程调度策略 structsched_param schedparam; //线程调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; //线程的栈设置 void* stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小}pthread_attr_t;

属性值不能直接设置,必须使用相关函数进行操作,初始化函数为pthread_attr_init,且这个函数必须在pthread_create函数之前调用,之后必须用pthread_attr_destroy函数来释放资源。线程属性主要包括:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)等。默认的属性为非绑定、非分离、默认1MB大小的堆栈、与父进程同样级别的优先级。

POSIX线程同步方式:POSIX信号量、读写锁、互斥锁和条件变量

对于多线程程序来说,同步是指在一定时间内只允许某一个线程访问某个资源。而在此时间内,不允许其他的线程访问该资源。编写多线程程序,通过定义宏_REENTRANT来告诉编译器需要可重入功能,这个宏的定义必须出现于程序中的任何#include语句之前。 _REENTRANT为我们做了三件事:

它会对部分函数重新定义它们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串。stdio.h中原来以宏的形式实现的一些函数将变成可安全重入函数。在error.h中定义的变量error现在将成为一个函数调用,它能以一种安全的多线程方式来获取真正的errno的值。

原子操作

死锁现象

无锁CAS

互斥锁

互斥锁是一个特殊的变量,它有上锁(lock)和打开(unlock)两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开,其他想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。 互斥锁主要有pthread_mutex_init、pthread_mutex_destory、pthread_mutex_trylock、pthread_mutex_lock和pthread_mutex_unlock这几个函数。pthread_mutex_trylock()在锁已经被占据时返回EBUSY而不是挂起等待。

锁的创建有两种方式:静态和动态。 静态方式创建锁

pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;

动态方式创建锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t* attr);

1 #include 2 #include 3 #include 4 #include 5 #include 6 pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER; 7 int total_ticket_num = 20; 8 void* sell_ticket(void* arg) 9 { 10 for(int i = 0; i < 20; i++) 11 { 12 pthread_mutex_lock(&mutex_x); 13 if(total_ticket_num>0) 14 { 15 sleep(1); 16 printf("%lu sell the %dth ticket\n", pthread_self(), 20-total_ticket_n um+1); 17 total_ticket_num--; 18 } 19 pthread_mutex_unlock(&mutex_x); 20 } 21 return 0; 22 } 23 int main() 24 { 25 int iRet; 26 pthread_t tids[4]; 27 for(int i = 0; i < 4; i++) 28 { 29 iRet = pthread_create(&tids[i],NULL,&sell_ticket,NULL); 30 if(iRet) 31 { 32 printf("pthread_create error, iRet=%d\n",iRet); 33 return iRet; 34 } 35 } 36 sleep(30); 37 void *retval; 38 for(int i = 0; i < 4; i++) 39 { 40 iRet = pthread_join(tids[i],&retval); 41 if(iRet) 42 { 43 printf("tid = %d join error, iRet = %d\n", tids[i], iRet); 44 return iRet; 45 } 46 printf("retval = %ld\n",(long*)retval); 47 } 48 return 0; 49 }

1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER; 8 int total_ticket_num = 20; 9 void* sell_ticket1(void* arg) 10 { 11 for(int i = 0; i < 20; i++) 12 { 13 pthread_mutex_lock(&mutex_x); 14 if(total_ticket_num>0) 15 { 16 printf("sell1 %lu sell the %dth ticket\n", pthread_self(), 20-total_ti cket_num+1); 17 total_ticket_num--; 18 } 19 pthread_mutex_unlock(&mutex_x); 20 sleep(1); 21 } 22 return 0; 23 } 24 void* sell_ticket2(void *arg) 25 { 26 int iRet = 0; 27 for(int i = 0; i < 10; i++) 28 { 29 iRet = pthread_mutex_trylock(&mutex_x); 30 if(iRet == EBUSY) 31 { 32 printf("sell2 the variable is locked by sell1.\n"); 33 }else if(iRet == 0){ 34 if(total_ticket_num>0) 35 { 36 printf("sell2 %lu sell the %dth ticket\n", pthread_self(), 20- total_ticket_num+1); 37 total_ticket_num--; 38 } 39 pthread_mutex_unlock(&mutex_x); 40 } 41 sleep(1); 42 } 43 return 0; 44 } 45 int main() 46 { 47 int iRet; 48 pthread_t tids[2]; 49 iRet = pthread_create(&tids[0],NULL,&sell_ticket1,NULL); 50 if(iRet) 51 { 52 printf("pthread_create error, iRet=%d\n",iRet); 53 return iRet; 54 } 55 56 iRet = pthread_create(&tids[1],NULL,&sell_ticket2,NULL); 57 if(iRet) 58 { 59 printf("pthread_create error, iRet=%d\n",iRet); 60 return iRet; 61 } 62 sleep(30); 63 void *retval; 64 iRet = pthread_join(tids[0],&retval); 65 if(iRet) 66 { 67 printf("tid = %d join error, iRet = %d\n", tids[0], iRet); 68 return iRet; 69 }else{ 70 printf("retval = %ld\n",(long*)retval); 71 } 72 iRet = pthread_join(tids[1],&retval); 73 if(iRet) 74 { 75 printf("tid = %d join error, iRet = %d\n", tids[1], iRet); 76 return iRet; 77 }else{ 78 printf("retval = %ld\n",(long*)retval); 79 } 80 return 0; 81 }

递归锁

自旋锁

条件变量

条件变量提供了一种线程间的通知机制:当线程在等待某些条件时使线程进入睡眠状态,一旦条件满足,可以唤醒因等待满足特定条件而睡眠的线程。

创建:条件变量和互斥锁一样,都有静态、动态两种创建方式静态方式使用PTHREAD_COND_INITIALIZER常量,函数原型是:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER

动态方式则使用pthread_cond_init函数,使用cond_attr指定的属性初始化条件变量cond,当cond_attr为NULL时,使用默认的属性。

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

注销:注销一个条件变量需要调用pthread_cond_destroy()

int pthread_cond_destroy(pthread_cond_t *cond)

只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。

等待:等待条件有两种方式 1. 条件等待 pthread_cond_wait()和计时等待pthread_cond_timedwait()。其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)

其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间。无论哪种等待方式,都必须和一个互斥锁配合,以防多个线程同时请求pthread_cond_wait()(或pthread_cond_timewait())的竞争条件。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex需保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

激发,激发条件有两种形式:pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。使用pthread_cond_signal不会有“惊群现象”(当有资源可用,所有的进程/线程都来竞争资源)产生,它最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么根据各等待线程优先级的高低确定哪个线程会接收到信号并开始继续执行;如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。

1 #include 2 #include 3 4 pthread_cond_t qready = PTHREAD_COND_INITIALIZER; 5 pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER; 6 int x = 10; 7 int y = 20; 8 void* func1(void *arg) 9 { 10 std::cout << "func1 start" << std::endl; 11 pthread_mutex_lock(&qlock); 12 while(xy) 29 { 30 pthread_cond_signal(&qready); 31 } 32 std::cout << "func2 end" << std::endl; 33 } 34 int main(int argc, char** argv) 35 { 36 pthread_t tid1,tid2; 37 int iRet; 38 iRet = pthread_create(&tid1,NULL,func1,NULL); 39 if(iRet) 40 { 41 std::cout << "pthread1 create error" << std::endl; 42 return iRet; 43 } 44 sleep(2); 45 iRet = pthread_create(&tid2,NULL,func2,NULL); 46 if(iRet) 47 { 48 std::cout << "pthread1 create error" << std::endl; 49 return iRet; 50 } 51 sleep(5); 52 return 0; 53 }

读写锁

对于某些资源的访问会存在两种可能的情况,一种是访问必须是排他性的,就是独占的意思,这称为写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。

当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求则长期阻塞。处理读者-写者问题的两种常见策略是强读者同步和强写者同步。在强读者同步中,总是给读者更高的优先权,只要写者当前没有进行写操作,读者就可以获得访问权限;而在强写者同步中,则往往将优先权交付给写者,而读者只能等到所有正在等待的或者正在执行的写者结束以后才能执行。

初始化和销毁读写锁 对于读写锁变量的初始化可以有两种方式:通过给一个静态分配的读写锁赋予PTHREAD_RWLOCK_INITIALIZER来初始化它;另一种方法就是通过调用pthread_rwlock_init()来动态地初始化。而当某个线程不再需要读写锁的时候,可以通过调用pthread_rwlock_destroy()来销毁该锁。当初始化读写锁完毕以后,该锁就处于一种非锁定状态。

int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);

在初始化某个读写锁的时候,如果属性指针attr是个空指针的话,表示使用默认属性,如果想要使用非默认属性,则要使用下面函数:

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

获取和释放读写锁 pthread_rwlock_rdlock()用来获取读出锁,如果相应的读出锁已经被某个写入者占有,那么就阻塞调用线程。pthread_rwlock_wrlock()以获取一个写入锁,如果相应的写入锁已经被其他写入者或者读出者占有(一个或多个),那么就阻塞该调用线程。pthread_rwlock_unlock()用来释放一个读出或写入锁。获取锁的两个函数的操作都是阻塞操作,也就是获取不到锁的话,那么调用线程不是立即返回,而是阻塞执行。

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

非阻塞获取读写锁的函数如果不能马上获得,就会立即返回一个EBUSY错误提示,而不是把调用线程投入到睡眠等待。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

1 #include 2 #include 3 #include 4 #include 5 #define THREADNUM 5 6 pthread_rwlock_t rwlock; 7 8 void *readers(void *arg) 9 { 10 pthread_rwlock_rdlock(&rwlock); 11 //sleep(THREADNUM - *(int*)arg); 12 printf("reader %ld got the lock\n",(long)arg); 13 pthread_rwlock_unlock(&rwlock); 14 pthread_exit((void*)0); 15 } 16 17 void *writers(void *arg) 18 { 19 pthread_rwlock_wrlock(&rwlock); 20 //sleep(THREADNUM - *(int*)arg); 21 printf("writer %ld got the lock\n",(long)arg); 22 pthread_rwlock_unlock(&rwlock); 23 pthread_exit((void*)0); 24 } 25 int main(int argc, char** argv) 26 { 27 int iRet; 28 pthread_t writer_id, reader_id; 29 pthread_attr_t attr; 30 int nreadercount = 1, nwritercount = 1; 31 iRet = pthread_rwlock_init(&rwlock, NULL); 32 if(iRet) 33 { 34 fprintf(stderr, "init lock failed\n"); 35 return iRet; 36 } 37 //设置线程为分离状态 38 pthread_attr_init(&attr); 39 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 40 for(int i = 0; i < THREADNUM; i++) 41 { 42 if(i%3){ 43 pthread_create(&reader_id,&attr,readers,(void*)nreadercount); 44 printf("create reader %d\n", nreadercount++); 45 }else{ 46 pthread_create(&writer_id,&attr,writers,(void*)nwritercount); 47 printf("create writer %d\n", nwritercount++); 48 } 49 } 50 sleep(20); 51 return 0; 52 }

信号量

互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区。

1.sem_init函数

#include int sem_init(sem_t *sem, int pshared, unsigned int value);

该函数用于初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享。value为sem的初始值。 2.sem_wait函数 该函数用于以原子操作的方式将信号量的值减1。sem_trywait()与sem_wait()函数相似,不过它始终立即返回,而不论被操作的信号量是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值非0时,sem_trywait()对信号量执行减1操作。当信号量的值为0时,它将返回-1并设置errno为EAGAIN。

int sem_wait(sem_t *sem);int sem_trywait(sem_t *sem);

3.sem_post函数 该函数用于以原子操作的方式将信号量的值加1.。当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。

int sem_post(sem_t *sem);

4.sem_destroy函数 该函数用于对用完的信号量进行清理

int sem_destroy(sem_t *sem);

1 #include 2 #include 3 #include 4 #include 5 #include 6 #define CUSTOMER_NUM 10 7 //营业厅同时只能服务两个顾客,如果服务窗口已满,就等待 8 sem_t sem; 9 10 void* get_service(void* thread_id) 11 { 12 int customer_id = *((int*)thread_id); 13 if(sem_wait(&sem)==0) 14 { 15 usleep(100); 16 printf("customer %d receive service ...\n",customer_id); 17 sem_post(&sem); 18 } 19 } 20 int main(int argc, char* argv[]) 21 { 22 //初始化信号量,初始值为2 23 sem_init(&sem,0,2); 24 pthread_t customers[CUSTOMER_NUM]; 25 int iRet; 26 for(int i = 0; i < CUSTOMER_NUM; i++) 27 { 28 iRet = pthread_create(&customers[i],NULL,get_service,&i); 29 if(iRet) 30 { 31 perror("pthread_create error"); 32 return iRet; 33 }else{ 34 printf("customer %d arrived.\n",i); 35 } 36 usleep(10); 37 } 38 39 for(int i = 0; i < CUSTOMER_NUM; i++) 40 { 41 pthread_join(customers[i],NULL); 42 } 43 sem_destroy(&sem); 44 return 0; 45 }

一个多线程程序的某个线程调用了fork函数,新创建的子进程不会自动创建和父进程相同数量的线程。子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制。并且子进程将自动继承父进程中互斥锁(条件变量与之类似)的状态。也就是说,父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就引起了一个问题:子进程可能不清除从父进程继承而来的互斥锁的具体状态(是加锁还是解锁)。

1 #include 2 #include 3 #include 4 #include 5 #include 6 7 pthread_mutex_t mutex; 8 //子线程运行的函数。它首先获得互斥锁,然后暂停5s,再释放互斥锁 9 void* another(void* arg) 10 { 11 printf("in child thread, lock the mutex\n"); 12 pthread_mutex_lock(&mutex); 13 sleep(5); 14 pthread_mutex_unlock(&mutex); 15 } 16 int main() 17 { 18 pthread_mutex_init(&mutex,NULL); 19 pthread_t id; 20 pthread_create(&id,NULL,another,NULL); 21 //父进程中的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得互斥量 22 sleep(1); 23 int pid = fork(); 24 if(pid<0) 25 { 26 pthread_join(id,NULL); 27 pthread_mutex_destroy(&mutex); 28 return 1; 29 }else if(pid==0) 30 { 31 printf("I am in the child, want to get the lock\n"); 32 //子进程从父进程继承了互斥锁mutex的状态,该互斥锁处于锁住的状态。 33 //这是由父进程中的子线程执行pthread_mutex_lock引起的 34 pthread_mutex_lock(&mutex); 35 printf("I can not run to here, oop...\n"); 36 pthread_mutex_unlock(&mutex); 37 exit(0); 38 }else{ 39 wait(NULL); 40 } 41 pthread_join(id,NULL); 42 pthread_mutex_destroy(&mutex); 43 return 0; 44 }

互斥锁被加锁了,但不是由调用fork函数的那个线程锁住的,而是由其他线程锁住的。如果子进程再次对该互斥锁执行加锁就会导致死锁。

不过,pthread提供了一个专门的函数pthread_atfork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态。该函数的定义如下:

#include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

该函数建立3个fork句柄来帮助我们清理互斥锁的状态。prepare句柄将在fork调用创建出子进程之前被执行。它可以用来锁住所有父进程中的互斥锁。parent句柄则是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行。它的作用是释放所有在prepare句柄中被锁住的互斥锁。child句柄是fork返回之前,在子进程中被执行。和parent句柄一样,child句柄也是用于释放所有在prepare句柄中被锁住的互斥锁。该函数成功时返回0,失败则返回错误码。

每个线程都可以独立地设置信号掩码。设置进程信号掩码的函数sigpromask,但在多线程环境下应使用下面的函数设置线程信号掩码:

#include #include int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);

该函数的参数的含义与sigprocmask的参数完全相同。由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为统一信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。

在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会响应被屏蔽的信号了。在某个线程中调用如下函数来等待信号并处理之:

#include int sigwait(const sigset_t* set, int* sig);

set参数指定需要等待的信号的集合。我们可以简单地将其指定为在第1步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。sig指向的整数用于存储该函数返回的信号之。很显然,如果使用了sigwait,就不应该再为信号设置信号处理函数。

1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 8 #define handle_error_en(en, msg) do { errno = en; perror(msg); exit(EXIT_FAILURE); } while(0) 9 10 static void* sig_thread(void*arg) 11 { 12 sigset_t* set = (sigset_t*)arg; 13 int s,sig; 14 for(;;) 15 { 16 //调用sigwait等待信号 17 s = sigwait(set,&sig); 18 if(s!=0) 19 { 20 handle_error_en( s, "sigwait" ); 21 } 22 printf("Signal hadling thread got signal %d\n",sig); 23 } 24 } 25 26 int main(int argc, char* argv[]) 27 { 28 pthread_t thread; 29 sigset_t set; 30 int s; 31 32 //在主线程中设置信号掩码 33 sigemptyset(&set); 34 sigaddset(&set,SIGQUIT); 35 sigaddset(&set,SIGUSR1); 36 s = pthread_sigmask(SIG_BLOCK,&set,NULL); 37 if(s!=0) 38 { 39 handle_error_en(s,"pthread_sigmask"); 40 } 41 s = pthread_create(&thread,NULL,&sig_thread,(void*)&set); 42 if(s!=0) 43 { 44 handle_error_en(s,"pthread_create"); 45 } 46 pause(); 47 }

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:arthas jprofiler做复杂链路的调用分析
下一篇:【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~