多线程编程
多线程编程
- 使用多线程的函数在编译的时候要加
-lpthread
。
1. 什么是线程?
- 在操作系统原理的术语中,线程是进程的一条执行路径。
- 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲。
- 所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
- 同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(threadlocal storage)。
- 一个进程可以有很多线程,每条线程并行执行不同的任务。
2. 为什么要使用线程?
- 线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。
- 在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。
3. 创建线程
线程
- 每个线程都有自己的线程ID,可以通过pthread_self()函数获取。
- 最常见的线程模型中,除主线程较为特殊之外=,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。
- 每个进程可创建的最大线程数由具体实现决定。
- 主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。
- 我们在创建子线程之后,在子线程的执行函数里一般都会用while(1)的死循环来让子线程一直运行,否则子线程将按代码顺序执行,执行完毕就线程退出了。同样的,我们主线程也应该要用一个while(1)循环一直行,否则主线程退出会导致进程退出,而进程退出会导致所有子线程退出了。
- 如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做
临界资源
,那么对于该资源访问修改相关的代码就叫做临界区
。
主线程
- 一个进程创建后,会首先生成一个缺省的线程,通常称这个线程为主线程(或称控制线程)。
- C/C++程序中,主线程就是通过main函数进入的线程,main函数就是主线程的入口函数。
子线程
- 由主线程调用pthread_create()函数创建的线程称为子线程。
- 子线程有自己的入口函数,该函数由用户在创建子线程的时候指定。
主线程和子线程的关系
- 线程函数执行完毕退出或以其他非常方式终止,线程进入终止态。但是为线程分配的系统资源不一定释放,可能在系统重启之前一直都不能释放。终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
可会合(joinable)
:这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。相分离(detached)
:表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。
- 线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
总结:
- 在创建一个线程的时候,默认为可会合状态,可会合状态一定要控制主线程等待子线程会合才能退出,如果不等待,子线程在退出的时候,它所占用的系统资源不会释放,所以我们一般使用分离态(相分离)。分离态下线程一旦退出,系统马上释放它的系统资源。
将线程设置为分离状态
- 每个线程创建后默认是joinable状态,该状态需要主线程调
pthread_join ()
等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。所以我们创建线程时一般会将线程设置为分离状态,具体有两种方法:
- 线程里面调用
pthread_detach(pthread_self())
- 在创建线程的属性设置里设置
PTHREAD_CREATE_DETACHED
属性
pthread_join ()函数
函数原型:
#include <pthread.h>
int pthread_join(pthread_t thread, void ** retval);
功能:
- 初始化一个线程属性对象
参数:
- thread:线程标识符,即线程ID,标识唯一线程。
- retval:表示接收到的返回值,如果 thread 线程没有返回值,或者不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。
返回值:
- 成功返回0,失败返回非0。
pthread_create()函数
函数原型:
#include <pthread.h>
int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg );
函数功能:
- 创建一个线程,并执行第三个参数start_routine所指向的函数。
参数说明:
- thread:一个 pthread_t 类型的指针,用来返回该线程的线程ID。每个线程都能够通过 pthread_self() 来获取自己的线程ID(pthread_t类型)。
- attr:线程的属性,其类型是 pthread_attr_t 类型,其定义如下:
typedef struct
{
int detachstate; 线程的分离状态
int schedpolicy; 线程调度策略
struct sched_param schedparam; 线程的调度参数
int inheritsched; 线程的继承性
int scope; 线程的作用域
size_t guardsize; 线程栈末尾的警戒缓冲区大小
int stackaddr_set; 线程的栈设置
void *stackaddr; 线程栈的位置
size_t stacksize; 线程栈的大小
} pthread_attr_t;
- start_routine:是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数);
- arg:传所调用函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
pthread_attr_init () 线程属性函数
函数原型:
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
功能:
- 初始化一个线程属性对象
参数:
- attr 线程属性结构体指针变量
返回值:
- 成功返回0,失败返回非0。
pthread_attr_destroy()线程属性函数
函数原型:
#include <pthread.h>
int pthread_attr_destroy(pthread_attr_t *attr);
功能:
- 销毁一个线程属性对象
参数:
- attr:线程属性结构体指针变量
返回值:
- 成功返回0,失败返回非0。
pthread_attr_getdetachstate()线程属性函数
函数原型:
#include<pthread.h>
int pthread_attr_getdetachstate(pthread_attr_t *attr,int detachstate);
功能:
- 获取线程的状态属性
参数:
- attr:线程属性结构体指针变量
- detachstate:线程的状态属性
返回值:
- 成功返回0,失败返回非0。
pthread_attr_setdetachstate()线程属性函数
函数原型:
#include<pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate);
功能:
- 设置线程的状态属性
参数:
- attr:线程属性结构体指针变量
- detachstate:线程的状态属性
detachstate | 说明 |
---|---|
PTHREAD_CREATE_DETACHED | 以分离状态启动线程 |
PTHREAD_CREATE_JOINABLE | 默认状态启动线程 |
返回值:
- 成功返回0,失败返回非0。
4. 互斥锁
- 解决资源共享问题。
- 当子线程1要使用临界资源,子线程1在进入到临界区后,就首先上锁; 然后用完离开临界区之后,把锁释放供其他线程使用。
- 如果线程想去临界区时发现被锁上了,线程有两种策略:
- 在临界区那里等(阻塞)
- 暂时先离开等会再过来看(非阻塞)。
5. 死锁
- 如果多个线程要调用多个对象,则在上锁的时候可能会出现“死锁”。举个例子: A、B两个线程会同时使用到两个共享变量m和n,同时每个变量都有自己相应的锁M和N。 这时A线程首先拿到M锁访问m,接下来他需要拿N锁来访问变量n; 而如果此时B线程拿着N锁等待着M锁的话,就造成了线程“死锁”。
死锁产生的4个必要条件:
- 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
- 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
- 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
- 当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。
破坏死锁
- 产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
- 破坏“占有且等待”条件
方法1
:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。
方法2
:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。 - 破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。 - 破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。
多线程改写服务器程序
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <getopt.h>
#include <pthread.h>
#include <ctype.h>
typedef void *(THREAD_BODY) (void *thread_arg);
void *thread_worker(void *ctx);
int thread_start(pthread_t * thread_id, THREAD_BODY * thread_workbody, void *thread_arg);
void print_usage(char *progname)
{
printf("%s usage: \n", progname);
printf("-p(--port): sepcify server listen port.\n");
printf("-h(--Help): print this help information.\n");
return ;
}
int main(int argc, char **argv)
{
int sockfd = -1;
int rv = -1;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t len;
int port = 0;
int clifd;
int ch;
int on = 1;
pthread_t tid;
struct option opts[] = {
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
while( (ch=getopt_long(argc, argv, "p:h", opts, NULL)) != -1 )
{
switch(ch)
{
case 'p':
port=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return 0;
}
}
if( !port )
{
print_usage(argv[0]);
return 0;
}
sockfd=socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
printf("Create socket failure: %s\n", strerror(errno));
return -1;
}
printf("Create socket[%d] successfully!\n", sockfd);
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* listen all the IP address on this host */
//inet_aton("192.168.0.16", &servaddr.sin_addr); /* Only listen specify IP address on this host */
rv=bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if(rv < 0)
{
printf("Socket[%d] bind on port[%d] failure: %s\n", sockfd, port, strerror(errno));
return -2;
}
listen(sockfd, 13);
printf("Start to listen on port [%d]\n", port);
while(1)
{
printf("Start accept new client incoming...\n");
clifd=accept(sockfd, (struct sockaddr *)&cliaddr, &len);
if(clifd < 0)
{
printf("Accept new client failure: %s\n", strerror(errno));
continue;
}
printf("Accept new client[%s:%d] successfully\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
/* !!!! Think about here: why pass clifd but not &clifd ? !!! */
thread_start(&tid, thread_worker, (void *)clifd);
}
close(sockfd);
return 0;
}
int thread_start(pthread_t * thread_id, THREAD_BODY * thread_workbody, void *thread_arg)
{
int rv = -1;
pthread_attr_t thread_attr;
if( pthread_attr_init(&thread_attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
goto CleanUp;
}
if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
goto CleanUp;
}
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
goto CleanUp;
}
/* Create the thread */
if( pthread_create(thread_id, &thread_attr, thread_workbody, thread_arg) )
{
printf("Create thread failure: %s\n", strerror(errno));
goto CleanUp;
}
rv = 0;
CleanUp:
/* Destroy the attributes of thread */
pthread_attr_destroy(&thread_attr);
return rv;
}
void *thread_worker(void *ctx)
{
int clifd;
int rv;
char buf[1024];
int i;
if( !ctx )
{
printf("Invalid input arguments in %s()\n", __FUNCTION__);
pthread_exit(NULL);
}
clifd = (int)ctx;
printf("Child thread start to commuicate with socket client...\n");
while(1)
{
memset(buf, 0, sizeof(buf));
rv=read(clifd, buf, sizeof(buf));
if( rv < 0)
{
printf("Read data from client sockfd[%d] failure: %s and thread will exit\n", clifd, strerror(errno));
close(clifd);
pthread_exit(NULL);
}
else if( rv == 0)
{
printf("Socket[%d] get disconnected and thread will exit.\n", clifd);
close(clifd);
pthread_exit(NULL);
}
else if( rv > 0 )
{
printf("Read %d bytes data from Server: %s\n", rv, buf);
}
/* convert letter from lowercase to uppercase */
for(i=0; i<rv; i++)
{
buf[i]=toupper(buf[i]);
}
rv=write(clifd, buf, rv);
if(rv < 0)
{
printf("Write to client by sockfd[%d] failure: %s and thread will exit\n", clifd, strerror(errno));
close(clifd);
pthread_exit(NULL);
}
}
}