兴發国际娱乐手机登录 40

深入理解计算机系统,Linux系统编程之循环创建N个子线程并顺序输出实现教程

Linux系统编程之循环创建N个子线程并顺序输出实现教程,linux个子

兴發国际娱乐手机登录 1

兴發国际娱乐手机登录,使用这个在线网页编辑真的是不习惯,还是
windows live writer 好。

实现代码

代码很简单,如下。但是也有坑!在给线程传参数的不能穿 循环遍历i 的
地址,因为 i 在主线程中
,被多个线程共享,所以不是唯一的。那么如何让每个线程
都有独自拥有自己的顺序编号呢?

1.方法一:当然可以在堆上开辟空间存储顺序编号呗。自己有自己的顺序编号的空间各自独立。

2.方法二:就是参数是void* 可以直接将循环变量i直接传给void* ,由于 arg
是每个线程 栈空间上的变量 故此 属于各个子线程,然后在使用的时候强转回
int,因为void* 和 int 刚好 都是4字节,这样做是安全的。

#include 
#include 
#include 
#include 
//线程 回调函数
void* myfun(void* arg)
{
    //int num = (int)arg;//利用值传递的方式,因为void* 和 int类型 刚好都是4个字节所有可以进行强转
    int num = *((int*)arg);
    sleep(num);
    printf("[%d] child thread id %lu\n",num,pthread_self());
    return NULL;


}
int main(void)
{
    pthread_t pthid[5];
    int i;
    for(i = 0;i< 5 ; i++)
    {
         int* temp = (int*)malloc(sizeof(int));
         *temp = i;
         pthread_create(&pthid[i],NULL,myfun,(void*)temp);
         //pthread_create(&pthid[i],NULL,myfun,(void*)i);//可以通过值传递的方式,每个线程都自己的栈空间 保存着各种的顺序
    }
    sleep(i);//让线程按顺序进程打印
    printf("parent thread id %lu\n",pthread_self());
    return 0;
}

实现效果:

兴發国际娱乐手机登录 2

实现代码 代码很简单,如下。但是也有坑!在给线程传参数的不能穿
循…

目  录

下面列一个清单用于最近的面试:(
清单是网上down的 )

我们在上一章节中讲到的Tiny
Web服务器只能为单个客服端提供访问,这一章里,我们将通过进程、多路复用和线程技术研究并发的服务器。

  1. static,final,transient
    等关键字的作用
  2. foreach 实现原理
  3. synchronized 和 volatile关键字

  4. List,Map,Set
    及其各种实现类,底层实现原理,实现类的优缺点

  5. 设计模式(单例【懒汉,饿汉,静态代码块,线程安全不安全】,工厂,观察,包装)

  6. 多线程(Runable 和 Thread)

  7. 什么是线程池,
    以及线程池的优点
  8. sleep() 和 wait() 的区别
  9. IO(字节,字符,序列,内存,对象,打印。。。)

  10. 反射机制和内省机制

  11. 解析 xml文件几种方式的原理与特点 [
    Dom, SAX, PULL ]
  12. sql 和 nosql(mysql 和 redis)

  13. 数据结构和算法(栈,队列,链表,树,图,排序,搜索)

  14. JDK 源码

  15. JVM(GC
    算法,垃圾回收器,类加载器,双委派类型,Java
    内存模型,happens-before 原则)
  16. web 分布式 session 实现的方式(4种)

  17. SOA,RPC,dubbo

  18. 再添加几个快烂掉的点:override 和
    overload,final 和 finally,

1.1 使用进程实现并发


我们实现过一个echo服务器,但是遗憾的是只能为一个客服端服务,这不是我们的初衷,现在我们来更新上一个版本,使得服务器在接收到连接请求的时候,创建子进程为该客户端提供服务,主进程会关闭已连接的描述符,继续监听下一个客服端,这一个过程我画了一个简图:

兴發国际娱乐手机登录 3

在这个过程中,客服端1连接上了服务器,并创建了一个已连接的描述符4,服务器立即派生子进程,子进程将继承原有的已连接描述符4,通过这个子进程的描述符为客服端1提供给服务。这时候,主进程必须要关闭已连接描述符4,使得不至于发生内存泄漏。

兴發国际娱乐手机登录 4

客服端2的连接过程和客服端1的过程是一样的,还是由服务器创建子进程2提供服务,并关闭服务器中的已连接描述符5。我们来看看改进代码:只是加入了回收子进程,在子进程中关闭监听描述符和主进程中关闭已连接描述符。运行的效果如下:

兴發国际娱乐手机登录 5

可以同时为多个客服端提供服务,实现进程并发,进程级并发的一个明显的缺点是,各个进程都有独立的地址空间,使得共享信息相当困难而且慢速需要IPC,原理已经讲解了,代码就不难理解了:

兴發国际娱乐手机登录 6

 

1.2 使用IO多路复用实现并发


应用程序在一个进程的上下文中显示的调度它们的逻辑流,逻辑流被模型化为状态机,数据到达文件描述后,主程序显示的从一个状态切换到另一个状态。

① 响应键盘输入和客服端连接

我们使用select函数创建一个描述符集合,当其中之一的描述符做好准备的时候,将控制权返回给程序,select函数原型如下:

int select(int n, fd_set *fdset, NULL, NULL, NULL);

fdset被称为一个描述符集合,我们将需要处理的描述符添加到fdset结合中去;第一个参数n是描述符集合中最大的数。select函数会一直阻塞,直到相应的集合中的描述符准备好可以读;

我们来演示一个例子:

兴發国际娱乐手机登录 7

当我们打开了监听描述符以后,我们将一个read_set集合清空,并添加上标准输入和监听描述符3形成集合{0,3},随后,我们进入一个无限循环,每次调用Select函数会阻塞,直到描述符0或者3到达时。

我们启动以后,随意输入内容,就会看到服务器首先响应了标准输入:

兴發国际娱乐手机登录 8

我们接下来启动 已连接描述符,就会发现一个问题:

兴發国际娱乐手机登录 9

不论是服务端的标准输入,还是新启动的客户端2都被阻塞了。只有当已连接描述符客户端1关闭的时候才能使用。

兴發国际娱乐手机登录 10

一个解决之道是服务器每次循环最多回送一个文本行,就不会让已连接的描述符连续回送了。

② 多路复用实现并发

兴發国际娱乐手机登录 11

服务器为每一个客户端创建一个状态机,每个状态机三个阶段:

【准备】——【输入事件】——【写回】

我们来看看main函数主要部分:

兴發国际娱乐手机登录 12

说明:活动的客户端是在pool池塘中,通过调用init_pool完成初始化后进入一个while循环,select函数检测两种不同的输入(新的连接、已经连接的描述符准备好可以读),当新的连接到达时,accept并add_client。最后使用check_clients函数将文本行回送。

分析:init_pool函数

兴發国际娱乐手机登录 13

分析:add_client函数

兴發国际娱乐手机登录 14

分析:check_clients函数

兴發国际娱乐手机登录 15

运行的效果如图:

兴發国际娱乐手机登录 16

总结:我们这个版本的并发服务器,使用的是事件驱动的形式,它的优点就是共享数据的效果好很多,因为都是同一个进程上下文。开销也没有多进程的版本大,缺点就是复杂度要高些。总之,是优秀很多的。

 

1.3 基于线程的并发


线程是一个运行在进程上下文中的逻辑流,由内核自动调度,集成了多进程与多路复用的优点,每个线程就像在舞台上跳舞的演员一样,各自分工和角色不一样,共享舞台的地址空间,当然也有自己私有的服装和台词。

① 执行模型

兴發国际娱乐手机登录 17

每个线程在开始的时候都是单一的主线程,这个主线程可以创建对等线程,然后两个线程并发执行,不断的切换上下文,分别执行一段时间。与进程之间不同的是线程的上下文切换要小的多,还有就是线程之间是完全对等的关系,也就是一个线程可以杀死它的对等线程。

我们来看一个简单的例子:

兴發国际娱乐手机登录 18

主线程main中通过使用Pthread_create创建了一个新的tid线程,成功以后两个线程同时运行,主线程还使用了Pthread_join函数等待对等线程终止。对等线程只是简单的打印了一下Hello
world。

② 创建线程

原型:int pthread_create(pthread_t *tid, pthread_attr_t *attr,
func *f, void *arg);

其中调用成功后tid是运行中的线程ID,attr设置线程默认属性,f是线程函数,arg是传递参数

可以使用:pthread_t pthread_self(void)函数获取当前线程的ID;

③ 终止线程

原型:int pthread_cancel(pthread_t tid); 终止当前线程

原型:void pthread_exit(void *thread_return);等待所有对等线程终止

④ 回收已经终止的线程

原型:int pthread_join(pthread_t tid, void **thread_return);

函数会阻塞,直到线程tid终止并回收所有存储器资源。与wait不同的是该函数只能回收一个特定的线程;

⑤ 分离线程:分离后的线程终止以后由系统自动释放

原型:int pthread_detach(pthread_t tid); 

⑥ 初始化线程

原型:int pthread_once(pthread_once_t *once_control, void
(*init_routine)(void));

⑦ 一个基于线程的并发服务器

兴發国际娱乐手机登录 19

这个版本同线程的版本没有多大的变化,有两个地方需要注意,我们使用了一个connfdp指针指向一个动态分配的空间来传递已连接的描述符,避免出现竞争。同时在每个线程的函数中使用deatach进行分离,每个线程终止后由系统释放。

运行效果:

兴發国际娱乐手机登录 20

static 关键字的作用:

  1. 作用在类上(该类必须只能是内部类,访问内部类的成员时,可以直接内部类名点调用)

  2. 作用在方法上(可以通过类名点调用,但是不能修饰构造方法,因为构造方法本身就是静态方法)

  3. 作用在代码块上(类被加载时,代码块自动执行,并且只执行一次,一般用于加载驱动或者单例设计模式)
  4. 静态导包(使被导包内的静态成员可以直接在我们直接的类中直接使用,不需要带类名,比如
    Math 类的 PI 常量,但缺点很明显,可读性性降低)

1.4 多线程中的共享变量


我们前面说过线程集中了多路复用中的共享的优点,也举例说了就像同一个舞台表演的不同演员一样,整个舞台空间是共享的。那么多线程中的共享是如何实现的,工作原理是什么?

我们看一个简单的例子,加入一些说明:

兴發国际娱乐手机登录 21

① 线程存储器模型

寄存器是不共享的,虚拟存储器总是共享的。就像同一个家庭的两个孩子一样,可以在一个饭厅吃饭,在客厅看电视,甚至共享同一个厕所,但是各自的房间通常是不一样的,各自的个人物品也不同。

② 将变量映射到存储器

全局变量:如ptr,可以使得本地变量msgs变成了共享(有时候两个孩子要共享一个厕所);

本地自动变量:如myid是不能共享的,每个线程的myid都不一样;

本地静态变量:加入static如cnt,只有一个实例,两个对等线程访问的是同一个地方


共享变量:
被1个以上的线程访问过的变量,如cnt。需要注意的是msgs也变成了共享的。

final 关键字的作用:

  1. 作用在类上
    (该类变为最终类,不能被继承)
  2. 作用在属性上
    (属性变为常量,常量的地址不允许被修改)
  3. 作用在方法上
    (方法变为最终方法,不能被子类重写)
  4. 作用在形参列表上(在方法类,值类型不允许被修改,引用类型对象的属性可以被修改,但是引用类型对象不能被改变)

对于
final 的第二点,要记住这句话,使用  final
关键字修饰一个值类型变量时,值类型的值不允许修改;修饰的是引用型变量时,是指引用变量地址不能变,但是引用变量所指向的对象中的内容还是可以改变的。**
归结到一点就是常量的地址不能改。**】

1.5 用信号量同步线程


智人在进化意义上最成功的由于其合作的规模,单个的智人个体虽然远远不及同时代的尼安德特人,但是合作的规模更大,力量也就更大。我们今天探讨的就是线程的同步,如果每个线程都各顾各的,势必会影响到程序的正常运行。我们来看一个未经同步的线程的运行情况:

兴發国际娱乐手机登录 22

这个程序的运行结果就不OK了,原因在于每个单独的进程对共享变量cnt的访问不是独占式的,这种不同步导致了错误的结果。我们来研究一下最核心的代码的运行过程:

兴發国际娱乐手机登录 23

这里我们将线程函数中的for循环翻译成汇编代码,其中:Li是循环头,Ti是循环尾,Li对应于加载cnt,Ui对应于更新cnt,Si对应于存储cnt。线程的执行顺序并不一定总是我们所期望的,如果遇到下面这种运行顺序,就可能会出错。

兴發国际娱乐手机登录 24

上图中左边是正确的运行顺序,(b)就会得到错误的结果,关键点在于线程1更新了eax的值以后并没有立即写入到cnt中,就开始运行了线程2,线程2由于cnt没有更新所有eax加载还是为0,当线程2完成写入命令以后cnt就仍然是1,不会得到累加。

为了帮助大家正确理解各个线程的执行顺序,我们来画图

① 进度图

兴發国际娱乐手机登录 25

上图展现了两个线程,1和2,分别用x轴和y轴表示,其中Hi、Li、Ui、Si、Ti分别代表对共享变成操作的for循环的关键步骤,其中Li、Ui、Si涉及对cnt临界区的操作,所有经过这一区域的执行顺序都是不安全的。为了使得线程之间的同步变得科学,不跨越临界区。我们发明了信号量这种特殊的变量。

② 信号量:非负整数全局变量

信号量s其实就是一个非负整数的全局变量,对这一变量有两个操作:P(s)使得s减1,而V(s)使得s加1。我们操作信号量s的时候,通常的情况是将其初始化为1,执行P操作的时候为加锁,执行V操作的时候为解锁。为了限定线程不经由不安全区域,我们将不安全区域的设置为-1,如下图:

兴發国际娱乐手机登录 26

我们的信号量s被初始化为1,只能在0和1之间变化:

1>加锁:执行P(s),有两种情况,如果原有的值为1,那么减至0;如果为0则挂起线程;

2>解锁:执行V(s),也有两种情况,如果s=0就加1;如果s=1就等待;

③ 更新我们的badcnt程序

兴發国际娱乐手机登录 27

这样以来我们的全局共享变量cnt在运行的各个线程中就会经由加锁执行++和解锁,得到正确的结果了。

④ 信号量调度共享资源

生产者——消费者问题

兴發国际娱乐手机登录 28

以小区的自动售货机为例,消费者如果直接以下订单的方式与生产者沟通,这样的效率就太低下了。我不可能想要喝一瓶可能才让可口可乐公司给我生产。这时候缓冲区就是一个很好的发明,我们发现在小区建立几个自动售货机,假设每个自动售货机可以装100瓶饮料。这样一来只要自动售货机不为空生产者就可以将饮料放入到自动售货机中去,当然只要售货机有饮料消费者也直接从自动售货机购买饮料。这样一来就方便的多了。

我们前面讲过信号量,P操作遇到为0的情况就会等待。但是现实的生活中,这样的情况就不很科学。回到我们上面的自动售货机的例子。如果我们的消费者发现了自动售货机是空的,我们就开始在原地等待,直到生产者将生产好的饮料送到自动售货机上的时候,再购买。这样以来对个人来说是精力的极大浪费。我们有什么好的方法没有,就像我们滴滴打车一样,我们下单以后就可以去做其他事情了,一有车子接单以后就会电话联系我们。

我们使用一种新的数据结构来解决这种问题:

兴發国际娱乐手机登录 29

操作函数

兴發国际娱乐手机登录 30

读者——写者问题

这个问题类似于上一个,有点儿像我们的购票系统,票数就是我们的共享变量,同一时刻我们允许多个客户从不同的端口登录查看票数在售情况(读者优先),但是当有一个购买者(写者)的买票的时候,写会独占票数。有一个解答如下:

兴發国际娱乐手机登录 31

⑤ 实现一个预线程化的并发服务器

我们通常所用到的线程并发服务器,要求服务器为每个客户端单独生成一个线程来提供服务,就相当于一种下订单再生产的落后经济模型,我们学习了生产者消费者模型以后,尝试加入新的内容:服务器
由一个主线程和一组工作线程构成,主线程接收客户端的连接请求,并将连接的描述符放入到一个缓冲区中,每个工作线程反复的从缓冲区中取出描述符,提供服务,然后等待下一个描述符。

兴發国际娱乐手机登录 32

我们来看看实现代码:

兴發国际娱乐手机登录 33

transient 关键字的作用:

  1.  

1.6 使用线程提高并行性


现代的CPU往往是多核的,如何利用这个特性变得相当重要。我们这里所的并行是并发的一个子集,代表的是在多核处理器上运行的并发程序。

如果我们要计算1,2,3……
100各个数字相加的和,我们知道经典的答案是:(1+100)*50=5050,我们使用多线程求一个集合数字的和的方法,就是将100个数字分成5个区域,这样每个区域有20个数字,每个对等线程求出5个区域20个数字的和,然后由主线程将不同的和相加,就会得到这100个数字的和。我们来看一段代码:

兴發国际娱乐手机登录 34

再来看看求和线程函数sum:

兴發国际娱乐手机登录 35

运行结果如下:

foreach 实现原理:

    foreach就是 java
中的加强for循环,语法: for(Type agr : Coll){ }

    其实现原理就是使用了迭代器,所以会比普通
for
循环会快速一点,但是也有弊端,就是只能遍历元素,不能修改元素。

1.7 其他并发问题


我们在实现程序的并发操作中,要注意很多问题。包括对共享变量的互斥访问,使得程序无论何时何系统,都能得到正确的返回值。不安全的操作有以下四类:

1> 不保护共享变量的函数;

2> 保持跨越多个调用状态的函数(rand、srand);

3> 返回指向静态变量指针的函数(ctime);

4> 调用线程不安全函数的函数;

说明:对于第3类函数,我们通常使用的是加锁——拷贝模式:

兴發国际娱乐手机登录 36

① 在库函数中使用_r版本

兴發国际娱乐手机登录 37

以上我们列出的是线程不安全函数的_r版本,这些版本不会引用共享的数据,因而在线程中使用是安全的,我们推荐使用_r版本的这类函数。

② 竞争

要理解竞争我们最好先来看一个例子:

兴發国际娱乐手机登录 38

这是一个很简单的程序,在主线程中11-12行创建了4个对等线程,分别给每个对等线程传递了一个本地变量i,期望在线程函数中将每个对等线程的id号输出显示。

当竞争发生的时候:

如果:先创建了一个线程(1),传递了本地变量1到线程函数thread中,并显示,这是合理的

如果:创建线程后,thread函数还未输出结果,就切换到主线程又创建新线程就会发生竞争

在不同的系统上得到了不同的结果,我们的改进方法如下:

兴發国际娱乐手机登录 39

③ 死锁

兴發国际娱乐手机登录 40

死锁是由于我们交替对一对互斥变量(s、t)加锁,如上图所示,线程1先对s加锁,线程2先对t加锁,然后线程1要求对t加锁的时候就必须等待,线程2要求对s加锁的时候也陷入了等待,两个线程都在等待就死锁了。解决之道很简单:

线程按照相同的顺序对s、t加锁,也就是说线程1先加锁s再加锁t,线程2先加锁s再加锁t。

volatile 关键字作用:

 

 

单链集合中的知识点:

单链集合的根接口是Collection,其下有两个子接口
List 和 Set,前者能存储重复元素,后者不能存储重复元素。

1. List 集合特点:

         元素的存取顺序是一致的,也可以存储重复元素

     List 集合实现类的选择原则 :
查询多就选择ArrayList, 增删多就选择LinkedList,
如果都多就选择ArrayList。

     List 集合实现类中使用的
contains()和remove()方法底层依赖的equals()方法,如果要使用这些方法,存入的实体类要实现equals()方法,

     可以使用LinkedList
来模拟站和队列,需要使用的是其addFast(),addLast() 等方法,

     三种循环: 普通for ,迭代器,
增强for,

  • 普通 for
    循环能删除集合元素(记住要及时将循环变量减一)
  • 迭代器,能删除集合元素(只能使用迭代器自身的
    remove 方法)
  • 增强 for
    循环,不能删除集合元素,只能用来遍历集合(普通数组也可以遍历,而且比普通for快)

style=”font-size: medium;”>之前写过的List集合的博客: style=”color: #ff0000;”>【 style=”color: #ff0000;”>ArrayList
去除重复元素 style=”color: #ff0000;”>】

                                        
【List
实现非递归删除目录 style=”color: #ff0000;”>】

发表评论

电子邮件地址不会被公开。 必填项已用*标注