Lecture15
1. 网络编程中采用多线程的必要性(29:00-33:00)
线程在休眠时将会失去对处理器的控制权。设一台计算机中,要分别建立10个网络连接,每申请一个网络连接,要等待时间t,如果依次申请,等一个连接成功再申请建立下一个连接,则共要花费时间10*t。若将建网络连接的任务分配到独立的线程中,则10线程同时发起申请,同时等待,则创建所有连接的总时间将会省很多。
多线程使访问网页时, 图像解码与文字呈现同时进行.
2 . 多线程共享某数据区时的处理
原子操作( atomic operation ), 是系统进行线程调度的最小操作单位. 也就是说, 当原子操作正被执行时, 是不会被打断, 不会进行context switch的.
在单处理器( UniProcessor )系统中, 一条指令就可以被看作是一个原子操作, 因为中断只能发生在指令之间( 参见: http://baike.baidu.com/view/809659.htm )
void SellTickets(int agent, int* numTicketsp)
{
while(*numTicketsp > 0) <1
{ <2
(*numTicketsp) -- ; <3
} <4
}
假设<1 到 <4之间所有语句生成了9条指令,那么在把SellTickets设为线程函数后,在执行完这9条指令中的任何一条后,此线程都有可能被从运行状态调度到就绪状态,甚至是静止就绪状态.然后其他以SellTickets为线程函数的线程,又会执行同样的这9条指令.为阻止一个线程被调度出运行状态后,其他同函数线程执行同样的代码段,可以为此段代码设置临界区( critical region )来加以保护.
临界区一般与信号量( Semaphore )共同使用来起到保护的作用.
将上面函数改写为:
void SellTickets( int agent, int* numTicketsp, Semaphor lock )
{
while(true)
{
SemaphoreWait(lock); // lock - - 注:当lock = 0时,不会再执行- -, 同时此线程会被阻塞. 调度到阻塞队列中.
if(*numTicketsp == 0) break;
(*numTicketsp) - -;
pritf(".....");
SemaphoreSignal(lock); // lock ++
}
SemaphoreSignal(lock); // lock ++
}
int main()
{
int numAgents = 10;
int numTickets = 150;
Semaphore lock = SemaphoreNew(..., 1 ); // 在本例中 Semaphore被用来限制对临界区的访问. 所以, 把它设为0或>1的值都是错误的. 正确情况, 只能设为1.
InitThreadPackage(false);
for(int agent; agent <= numAgents; agent++)
{
char name[32];
sprintf(name, "Agent %d Thread", agent);
ThreadNew(name, SellTickets, 3, agent, &numTickets, lock);
}
RunAllThreads();
return 0;
}
Lecture16
线程间通信与线程死锁
1.
Lecture16中的Semaphore是一个同步计数器, 它的值要么为1, 要么为0. 所以实际上它是被当作一个开关量来用的. 而位于SemaphoreWait()与SemaphoreSignal()之间的代码段就是临界区( critical region ).
同一函数作为不同线程的线程函数时, 不同的线程将会执行相同的代码段, 因此使得用Semaphore来划定并保护临界区成为了可能. 临界区要尽可能地小, 使得执行临界区中的代码总时间尽可能地少.
2. 线程间通信的必要性及方法
通过Semaphore也可以实现线程间的通信
char bufer[8];
void Writer()
{
for(int i=0; i<40; i++)
{
char c = PrepareRandomChar();
buffer[i%8] = c;
}
}
void Reader()
{
for(int i=0; i<40; i++)
{
char c = buffer[i%8];
processchar(c);
}
}
int main()
{
ITP(false);
ThreadNew("Writer", Writer, 0);
ThreadNew("Reader", Reader, 0);
RunAllThread();
}
以上两个线程在执行时, 若Writer执行的速度比Reader的速度快( 可以假设processchar(c)执行时, 所需的时间会比较多. 故Reader会经常在执行到一半时被调度到就绪队列中),那么在Reader()还未完成第一轮读取循环时, Writer就有可能已经开始第二轮的写循环了. 这样一来就会导致buffer中数据的错误.
引入Semaphore对象, 建立两线程间的通信机制, 可以避免以上问题, 下例中的emptyBuffer与fullBuffer两个信号量对象就是用来进行线程间通信的:
char bufer[8];
Semaphore emptyBuffer(8);
Semaphore fullBuffer(0);
void Writer()
{
for(int i=0; i<40; i++)
{
char c = PrepareRandomChar();
SemaphoreWait( emptyBuffer );
buffer[i%8] = c;
SemaphoreSignal( fullBuffer );
}
}
void Reader()
{
for(int i=0; i<40; i++)
{
SemaphoreWait( fullBuffer );
char c = buffer[i%8];
SemaphoreSignal( emptyBuffer );
processchar(c);
}
}
int main()
{
ITP(false);
ThreadNew("Writer", Writer, 0);
ThreadNew("Reader", Reader, 0);
RunAllThread();
}
3. 五位哲学家围坐圆桌, 五个线程运行同一个函数, 作为线程函数. 每两人间有一把叉子( 共五把叉子 ). 哲学家只坐两件事, 思考和吃饭, 反复进行. 但在吃饭前必须要拿到叉子,( 要同时拿到位于左手边和右手边的共两把叉子 ),吃完后, 把叉子放下, 以便别人可以使用. 要吃饭而拿不到叉子时, 需要等待他人用完. 此例用线程模拟如下:
Semaphore forks[] = {1, 1, 1, 1, 1};
void Philoshopher(int id)
{
for(int i=0; i<3; i++)
{
Think();
SemaphoreWait( forks[id] ); // 先抓左手的叉子
SemaphoreWait( forks[(id+1)%5] ); // 再抓右手的叉子
Eat();
SemaphoreSignal( forks[id] );
SemaphoreSignal( forks[(id+1)%5] );
}
}
以上当每个人都拿到右手的叉而等待左手叉被他人释放时, 会造成死锁,
改进如下:
Semaphore forks[] = {1, 1, 1, 1, 1};
Semaphore numAllowedToEat(4);
void Philoshopher(int id)
{
for(int i=0; i<3; i++)
{
Think();
SemaphoreWait( numAllowedToEat );
SemaphoreWait( forks[id] );
SemaphoreWait( forks[(id+1)%5] );
Eat();
SemaphoreSignal( forks[id] );
SemaphoreSignal( forks[(id+1)%5] );
SemaphoreSignal( numAllowedToEat );
}
}
信号量numAllowerToEat, 用来限制同时准备吃饭的最大人数值.
Lecture17
从Lecture16中讨论的内容可知, 一个信号量本质上代表着一个资源的可用性.
消除死锁的原则: 用最小的改动来避免死锁.( 例如Lecture16中的哲学家问题,把numAllowedToEat初始值设为2也可以避免死锁, 但这样一来, 同一时刻只会有两个线程有争抢时间片, 而有三个线程处于block状态.但若把numAllowedToEat设为4, 则同时会有四个线程争抢时间片,而只有一个线程处于block状态.) 从而,可以留出足够多的弹性空间给线程管理器做规划.
子线程与父线程间的通信.FTP例子.
int DownloadSingleFile( const char* server, const char* path ); // 返回下载文件内容的总字节数 server为服务器名 path为文件及所在路径名
int DownloadAllFiles(const char* server, const char* files[], int n) // files[] 表要下载的文件列表 n 表要下载的文件数
{
int totalBytes = 0;
Semaphore lock = 1;
for(int i=0; i<n; i++)
{
ThreadNew(..., DownloadHelper, 4, server, files[i], &total, lock);
}
return totalBytes;
}
void DownloadHelper( const char* server, const char* path, int* numBytesp, Semaphore lock)
{
int bytesDownloaded = DownloadSingalFile(server, path);
SemaphoreWait( lock ); // 若将此行加在 DownloadSingalFile之前,则将会使得DownloadAllFiles一次只有一个线程在DownloadSingal,其他则都会处于
// block状态.
(*numBytesp) += bytesDownloaded;
SemaphoreSignal( lock );
}
这里存在的问题是: 父线程中的DownloadAllFiles函数, 在用for循环创建完各子线程后, 有可能在不等子线程运行结束就直接返回了.所以所得返回值为0. 为避免此类情况的出现, 需要建立父子线程间的通信.故修改如下:
int DownloadAllFiles(const char* server, const char* files[], int n) // files[] 表要下载的文件列表 n 表要下载的文件数
{
int totalBytes = 0;
Semaphore lock = 1;
Semaphore childrenDone = 0;
for(int i=0; i<n; i++)
{
ThreadNew(..., DownloadHelper, 4, server, files[i], &total, lock, childrenDone);
}
for(int i=0; i<n; i++) // 这个循环的每一轮都可能被block住, 直到DownloadHelper调用SemaphoreWait后, 才进行下一轮循环.
{
SemaphoreWait( childrenDone );
}
return totalBytes;
}
void DownloadHelper( const char* server, const char* path, int* numBytesp, Semaphore lock, Semaphore parentToSignal)
{
int bytesDownloaded = DownloadSingalFile(server, path);
SemaphoreWait( lock );
(*numBytesp) += bytesDownloaded;
SemaphoreSignal( lock );
SemaphoreSignal( parentToSignal );
}
Lecture18
多线程模拟ice cream store的运作.
店中有四种角色, 每种角色若干人, 每个人一个线程. 角色分配如下:
Manager ( 1 )
Clerks( 10 - 40 )
Customers( 10 ) 每人可以买1-4个ice cream. 每买一个就需要一个Clerk( 线程 )来处理.
Cashier( 1 )
//-------
先讨论各线程的相互制约关系, 再进行算法设计和算法实现.
//-------
int main()
{
int totalCones = 0;
InitThreadPackage();
Semaphores();
for(int i=0; i<10; i++)
{
int numCones = RandomInteger(1, 4);
ThreadNew(..., Customer, 1, numCones);
totalCones = numCones;
}
ThreadNew(..., Cashier, 0);
ThreadNew(..., Manager, 1, totalCones);
RunAllThreads();
FreeSemaphores();
return 0;
}
struct Inspection
{
bool passed; // false
Semaphore requested(0);
Semaphore finished(0);
Semaphore lock(1);
}inspection;
void Manager( int totalConesNeeded )
{
int numApproved = 0;
int numInspected = 0;
while( numApproved < totalConesNeeded)
{
SemaphoreWait( inspection.requested ); // 等待clerk 提出检查申请
numInspected ++;
inspection.passed = RamderChannes(0, 1);
if( inspection.passed )
{
numApproved ++;
}
SemaphoreSignal( inspection.finished );
}
}
void Clerk( Semaphore SemaToSignal )
{
bool passed = false;
while( !passed )
{
MakeCone();
SemaphoreWait( inspection.lock ); // 锁上经理室的门
SemaphoreSignal( inspection.requested );
SemaphoreWait( inspection.finished );
passed = inspection.passed;
SemaphoreSignal( inspection.lock ); // 打开经理室的门 如果将此句移到 passed = inspection.passed之前, 或移到manager中去, 则clerk获得的可能是对他人数据的检 // 测结果.
}
SemaphoreSignal( SemaToSignal ); // 当检测通过后, 告诉customer可以进行下一步.
}
clerk与manager两个线程使用的是两个不同的线程函数, 当它们之间需要针对做某一件事达成相互同步时, 要用两个信号量. 这样, 一个线程等待另一个线程提交申请, 或者一个线程等待另一个线程回复的操作就可以通过SemaphoreWait()来实现.
一个clerk线程与另一个clerk线程, 使用的是两个相同的线程函数, 当它们之间需要针对某件事达成同步时, 只要用一个信号量, 设定临界区就可以了. 如本例中的inspection.lock.
void Customer( int numCones )
{
BrowX();
Semphore clerksDone(0);
for( int i=0; i<numCones; i++ )
{
ThreadNew( ..., clerk, 1, clerksDone );
}
for( int i=0; i<numCones; i++ )
{
SemaphoreWait( clerksDone ); // 等待为自已做ice cream的每一个clerk的回复.
}
SemaphoreFree( clerksDone );
WalkToCashier();
SemaphoreWait( line.lock );
int place = line.number ++;
SemaphoreSignal( line.lock );
SemaphoreSignal( line.requested );
SemaphoreWait( line.customers[ place ] );
}
customer与clerk两个线程使用的是两个不同的线程函数, 当其中一个的运行要等待另一个( 而不是相互等待 )时, 只需要一个信号量就可以了.
struct Line
{
int number; // 初始化为0
Semaphore requested(0);
Semaphore customers[10]; // 注意是数组 customers[10]
Semaphore lock(1);
} line;
void Cashier()
{
for( int i=0; i<10; i++ )
{
SemaphoreWait( line.requested );
checkout( i );
SemaphoreSignal( line.customers[i] );
}
}
customer间及customer与cashier的关系不同于clerk间及clerk与manager的关系.
Lecture19
1. C与C++的比较
在 C 中: 在C++ 中:
struct Vector v; Vector<int> v;
VectorNew(&v, ...); v.pushback( 4 );
VectorInsert(&v, ...); v.erase( v.begin() );
可以看到, 在面向过程的编程中,是以函数为核心的. 函数调用时, 首先看到的是例如: VectorNew, VectorInsert等函数名. 而在面向对象的编程中, 是以内存中的一块数据为核心的. 函数调用时, 首先看到的是例如: v 的对象名. 但不论是面向过程和面向对象, 都是从" 执行 "的角度出发进行编程的, 它更关心的是执行的过程( Procedural Paradigm ), 而不是返回值.
严格说来, 并发编程( concurrent programming ) , 多线程的编程范式, 与上面这种顺序编程的编程范式是不同的.
另外, 还有一种函数范型( functional paradigm )的编程模式, 更关心的是函数的返回值. 是面向返回值的.
2. Scheme. Kawa ( John McCarthy 的 lambda 演算 )
> (+ 1 2 3)
6
> (*(+ 4 4)
(+ 5 5))
80 通过递归来计算求值( 不要理解成函数调用 )
> (> 4 2)
#t
> (< 10 5)
#f
> (and (> 4 2)
(< 10 5))
#f
> (car '(1 2 3 4 5)) // 计算列表中0 slot中的值
1
> (cdr '(1 2 3 4 5)) // 计算除0 slot 之后剩余的所有值
(2 3 4 5)
> (car ( cdr ( cdr '(1 2 3 4 5))))
3
反括号")" 表示停止计算, 撇号" ' " 表示抑制计算?
> (cons 1 '(2 3 4 5)) cons 将一个元素和一个列表组合在一起, 形成一个新列表, 并返回它.
( 1 2 3 4 5 )
> (const '(1 2 3) '(4 5))
((1 2 3) 4 5)
> (append '(1 2 3) '(4 5)) // append 后可跟任意多个列表
(1 2 3 4 5)
> (append '(1 2) '(3) '(4 5) '(6 7 8))
(1 2 3 4 5 6 7 8)
> (cdr '(4))
()
> (cdr '())
NO
> (define add(x y) (+ x y)) // 定义一个称为add的函数
> (add 10 7)
17
> (add "Hi" "there")
error
数据结构, 线性列表, 是一个递归结构. 所以对它的计算可以采用递归的方式来求解, 例如求和运算符: Sum_of()
> ( Sum_of '(1 2 3 4))
其定义式为:
( define Sum_of( numlist )
(if (null ? numlist) 0
(+ (car numlist)
(Sum_of(cdr numlist)))))
Lecture20
> ( Sum_of '("hello" 1 2 3 4 5 ) )
会执行递归计算, 直到把后面的结果与 "hello" 相加时, 才会报错.
Scheme是一个runtime-language, 它实际上保存了所有数据的类型信息, 在"hello"的生存期内, 在内存中用一个小数据结构来保存它, 该数据结构附有一个枚举类型的标记, 表明这是一个字符串.
//----
> (define fib(n)
(if ( zero ? n) 0 // zero 是内置的零值检测标识
( if ( = n 1) 1 // = 是比较运算, 不是赋值操作
( + ( fib( - n 1))
( fib( - n 2))))))
此定义与以下定义等价:
> ( define fib(n)
( if ( or ( = n 0)
( = n 1)) n
( + ( fib ( - n 1))
( fib ( - n 2)))))
> ( if ( zero ? 0) 4
( + "hello" 4.5 '(8 2))) // 此句为错误的语法表达. 一开始, Scheme会对整个程序进行解析, 并建立一个幕后的数据结构, 每一个成员包括加号都会被当作一个 token. 这个 // 过程中, 不会报错. 只有在直正执行到这一句时, 系统才会检测以上被拆分诸项是否符合加法运算规则, 并报错.
虽有最后一句错误的语法表达, 但整个程序仍会运行并得到最终结果:
4
完全编译性语言会在编译阶段排除的错误, 在runtime language 和 script language 中就有可能被隐藏.
//------------------------------------
定义一个函数, 要求其功能如下:
> ( flatten '( 1 2 3 4 ))
( 1 2 3 4 )
> ( flatten '( 1 ( 2 3 ) "4" ((5)) ))
( 1 2 3 4 5)
> ( flatten '( 1 2 () 3 4 ))
( 1 2 3 4)
> ( define flatten( sequence )
( cond ( ( ( null ? sequence) '() // cond相当于switch
( ( list ? ( car sequence)) // list 判断是否为一个列表
( append ( flatten ( car sequence))
( flatten ( cdr sequence))))
( else ( cons ( car sequence) // 相当于switch中的default
( flatten ( cdr sequence))))))
以上程序是将flatten中所有出现的可能, 分为三种情况:
1. '()
2. '(1 ...)
3. '(( 1 2 ) ...)
再来编程.
Scheme或者说Lisp语言, 对于拆分成递归结构的数据, 及其上的运算有着天然的一致性.
//-------------------------------------------------
定义一个函数, 要求功能如下:
> ( sorted? '( 1 2 2 4 7))
#t
> ( sorted? '( 10 4 7 10))
#f
> ( define sorted? (num_list)
( or ( < ( length num_list ) 2 ) // 内置函数, 判断后面参数中列表的长度
( and ( <= ( car num_list )
( cadr num_list )) // cad 与 cdr 的嵌套, 用来得到num_list中的第二个元素.
( sorted? ( cdr num_list )))))
以算法是, 设长度为0和1的list 都是排好序的. 再来进行编程.
扩展函数, 要求:
> ( sorted? '( 1 3 5 7) <=)
#t
> ( sorted? '("a" "b" "d" "c") string<?) // string<? 内置运算符, 表对字符串进行"小于"比较.
#f
> ( define sorted? (seq comp) // Scheme 在参数中传递的不是comp的地址, 而是comp本身. 程序经解析后生成链表, comp作为链表的第0个元素, 知道后面的各元素类型, 及 如何对这些元素进行操作.
( or ( < ( length seq ) 2 )
( and ( comp ( car seq )
( cadr seq ) )
( sorted? ( cdr seq ) comp ))))
Scheme中程序解析及函数调用的原理: 40:00~51:00
Lecture21
> ( double-all '(1 2 3 4) )
( 2 4 6 8 )
> ( incr-all '(1 2 3 4) )
( 2 3 4 5 )
两个函数: 1. 都是挨个访问每个元素, 然后调用cadr递归. 2. 都把一些功能应用到每一次用car得到的元素上. 3. 都输出一个列表, 其长度和输入列表长度一样.
//----------------------------------
构建与double-all, incr-all类似的功能:
( define ( double x ) ( * x 2 ) )
( define ( incr x ) ( + x 1) )
> ( map double '(1 2 3 4) )
( 2 4 6 8)
> ( map incr '(1 2 3 4))
( 2 3 4 5 )
// map为内置函数, 可带 n 个参数 ( n >= 2 ), 其参数可以是内置也可以是自定义函数.上两例使用了map内置的递归调用方式. map可以关联到很多函数上.
> ( map car '( (1 2) (4 8 2) (11) ) ) // 将列表递归得到的每个元素的car值,转换成最终列表里的元素.
( 1 4 11)
> ( map cdr '( (1 2) ( 4 8 2) (11) ) )
( (2) (8 2) () )
> ( map cons '(1 2 8) '((4) () (2 5)))
( (1 4) (2) (8 2 5))
> ( map + '(1 2) '(3 4 1) '(6 8) )
(10 14) // 10=1+3+6 16=2+4+8
//----------------------------------------
自定义类似map的函数
( define (my_unary_map fn seq)
( if (null?seq) '()
(cons ( fn (car seq)
( my_unary_map fn (cdr seq) ) ) ) )
//----------------------------------------
两个特别的内置函数
apply 表函数和数据相同, 所有东西都可以用列表来表示, 允许用户指定函数用来处理后面的相关参数.
> ( apply + '(1 2 3) ) // 表apply接收两个参数, 第一个是和一段Scheme代码有关的操作.第二个是某种类型的数据列表, 第一个参数表示的操作可以作用其上.
6
eval 评估函数 Scheme程序中所有的输入都是文本字符流, eval 函数对作为输入的文本字符串进行操作, 找到匹配的括号对, 然后对其中内容进行评估.
> '(+ 1 2 3)
(+ 1 2 3)
> (eval '(+ 1 2 3))
6
// apply函数的应用实例: 定义一个求列表元素平均值的函数
( define (average num_list)
( / ( apply + num_list )
( length num_list ))) // length 为内置内函数, 用以求后面列表的长度.
//-------------------------------------------------
car, cdr, cadr 等函数用显示递归的方法, 而map, apply 等内置函数内部其实也是递归机制. 但map, apply 把这种递归机制隐含在内部, 使用户觉得对所传入列表各元素的操作是并列的, 平等的, 一视同仁的. 让人觉得是逐一线性进行而并未进行递归运算的.
Scheme语言的核心数据结构是列表. 列表是一个递归的数据结构.以这种数据结构为核心的语言自然天然就与递归运算有着密切的联系.
//-------------------------------------------------
apply 不能对比较运算进行操作, 而eval则可以用在apply不能用到的场合.
eval 使得程序在运行时还可以被修改.随机化, 遗传算法等进化函数的实现即可利用此机制.( 当然要有一套自已的定义 )
//-------------------------------------------------
所谓的"削平"问题.
例:输入 ((1 2) ((3) ((4) 5)) 10)
输出 (1 2 3 4 5 10)
用递归方法来实现flatten函数
(define ( flatten seq )
( if ( not (list ? seq))
( apply append 2. 再考虑此部分
( map flatten seq ) ) ) ) 1. 先考虑最核心的此部分
//-----------------------------------------------
Scheme能更直接地来描述算法, 言简意赅而无需进行分配内存, 构建链表等操作.
Scheme 几乎只有运行时间没有编译时间, 是弱输入型的.也就是说,所有的输入都会推迟到运行时再做类型检查.
//-----------------------------------------------
Scheme中的匿名函数:
( define ( translate points delta )
( map ( lambda (x) // lambda 是一个占位符, 表示这里定义了一个函数, 而此函数无函数名. 此匿名函数只能存活于map的范围中.
( + x delta)
) points ) )
等价定义:
( define ( translate seq delta )
( define ( shift_by x )
( + x delta ) )
( map shift_by seq ) )
第二种方式虽与第一种方式等价, 但并不完美. 函数中套函数定义不象一个真正的函数表达方式. Scheme应该是一个纯函数表达语言, 所以第一种表达方式更好些.
> ( translate '(2 5 8 11 25) 100)
( 102 105 108 111 125 )
//---------------------------------------------------
1. ( define ( sum x y )
( + x y ) )
2. ( define sum
( lambda ( x y ) ( + x y ) ) )
3. ( define pi 3.14 )
Scheme中的define实际上是将其后的两个参数所表示的对象永久地绑定起来( 如3中就是把 pi 与 3.14 绑定 ).
上例中"1" 是一种占位式定义, "2"更能体现函数名与函数体之间的关系, 本例是将sum与lambad 函数永久地绑定起来. 实际上"1"与"2"的作用都是一样的( 都是将符号与一个函数绑定 ), 只是"1"的可读性更强些.
//--------------------------------------
Scheme语言全都是关于符号, 符号评估, 函数评估, 从函数的实际定义到它们在内存中的存储方式等有关问题的讨论.
872

被折叠的 条评论
为什么被折叠?



