简介 提到爬虫,大部分人都会想到使用Scrapy工具,但是仅仅停留在会使用的阶段。为了增加对爬虫机制的理解,我们可以手动实现多线程的爬虫过程,同时,引入IP代理池进行基本的反爬操作。 本次使用天天
顺晟科技
2021-06-16 10:37:52
429
我们在编程的时候,有时候会用多线程来解决问题。比如你的程序需要在后台处理大量数据,但也需要使用户界面具有可操作性;或者您的程序需要访问一些外部资源,如数据库或网络文件。您可以创建一个子线程来处理这些情况。但是多线程必然会带来一个问题,就是线程同步的问题。如果这个问题处理不好,我们会得到一些意想不到的结果。
我也在网上看过一些关于线程同步的文章。其实线程同步有几种方法,下面我简单总结一下。
首先,易变关键字
Volatile是最简单的同步方法,当然简单是有代价的。只能在可变级别同步。volatile意味着告诉处理器不要把我放在工作内存中,而是直接在主内存中操作我。(【转自www.bitsCN.com】)因此,当多个线程同时访问变量时,会直接操作主存,从而在本质上共享变量。
可以确定为易变的必须是以下类型:(摘自MSDN)
任何引用类型。
任何指针类型(在不安全的上下文中)。
类型有字节、字节、短字节、短字节、int、uint、char、float、bool。
枚举类型,其枚举基本类型为字节、字节、短字节、短字节、整数或整数。
例如:
复制代码
公共甲级
{
private volatile int _ I;
公共国际
{
get { return _ I;}
set {_ I=value;}
}
}
复制代码
但是volatile无法实现真正的同步,因为它的操作级只停留在变量级,而不是原子级。如果是在单处理器系统中,没有问题。变量在主存中没有被别人修改的机会,因为只有一个处理器,这叫处理器自洽。但是在多处理器系统中,可能会有问题。每个处理器都有自己的数据缓存,更新后的数据可能不会立即写回主存。所以可能会造成异步,但是很难发生,因为cach的读写速度相当快,刷新的频率也相当高,只能发生在压力测试的时候,概率非常非常小。
二、锁定关键词
锁是一种简单易行的线程同步方法,它通过获取给定对象的互斥锁来实现同步。它可以保证当一个线程在临界代码段时,另一个线程不会进来,只能等到那个线程对象被释放,也就是说线程出了临界区。用法:
复制代码
公共无效函数()
{
对象锁定此=新对象();
锁定(锁定此)
{
//访问线程敏感资源。
}
}
复制代码
lock的参数必须是基于引用类型的对象,而不是像bool、int这样根本不能同步的基本类型。原因是锁的参数是对象。如果传入int,必然会发生打包操作,所以每次锁的时候都是一个新的不同的对象。更好避免使用不受程序控制的公共类型或对象实例,因为这可能会导致死锁。特别是不要用string作为lock的参数,因为string是由CLR“持久化”的,也就是说整个应用程序中只有一个给定字符串的实例,所以更容易造成死锁。建议使用未“驻留”的私有或受保护成员作为参数。实际上,有些类提供了专门用于被锁定的成员,例如,数组类型提供了同步根,许多其他集合类型也提供了同步根。
因此,使用锁应注意以下几点:
1.如果一个类的实例是公共的,更好不要锁定(这个)。因为使用您的类的人可能不知道您使用了锁,如果他新建了一个实例并锁定了它,很容易造成死锁。
2.如果我的类型是公共的,不要锁定(类型为(我的类型))
3.永远不要锁住一根绳子
三.系统。穿线。互锁
对于整数数据类型的简单操作,线程同步可以通过系统中存在的互锁类的成员来实现。线程命名空间。互锁类有以下方法:递增、递减、交换和比较交换。使用增量和减量可以确保整数的加减是一个原子操作。交换方法自动交换指定变量的值。CompareExchange方法结合了两种操作:比较两个值,并根据比较结果在其中一个变量中存储第三个值。比较和交换操作也作为原子操作来执行。例如:
复制代码
int I=0;
系统。螺纹。互锁。增量(参考I);
控制台。write line(I);
系统。穿线。互锁。减量(参考I);
控制台。write line(I);
系统。穿线。互锁。交换(参考文献100);
控制台。write line(I);
系统。螺纹。互锁。比较交换(参考文献I,10,100);
复制代码
输出:
四.班长
Monitor类提供了类似锁的功能,但不同于lock,它可以更好地控制同步块。当调用Monitor的Enter(Object o)方法时,它将获得o的独占权,并且直到调用Exit(Object o)方法时才会释放o的独占权。可以多次调用输入(对象0)方法。只需要调用Exit(Object o)方法相同的次数,Monitor类还提供了一个重载的TryEnter(Object o,[int])方法,试图获取o对象的独占权限,独占权限获取失败返回false。
但是,使用锁通常比直接使用监视器更好,一方面是因为锁更简单,另一方面,锁确保即使受保护的代码引发异常,底层监视器也可以被释放。这是通过最后调用Exit来实现的。事实上,锁是用监视器类实现的。以下两段代码是等效的:
复制代码
锁(x)
{
dosomesing();
}
同族的
object obj=(object)x;
系统。线程化. monitor . enter(obj);
尝试
{
dosomesing();
}
最后
{
系统。线程化. monitor . exit(obj);
}
复制代码
有关用法,请参考以下代码:
查看代码
当线程1获得m _ monitorcoid对象的独占权限时,线程2尝试调用try enter(m _ monitorcoid),此时会因为无法获得独占权限而返回false,输出信息如下:
此外,Monitor还提供了三种静态方法:Monitor。脉搏(对象0),监视器。对象和监视器。等待(对象0),用于实现唤醒机制的同步。至于这三种方法的用法,可以参考MSDN,这里就不详细描述了。
动词(verb的缩写)互斥(体)…
在使用上,Mutex类似于上面提到的Monitor,但是Mutex不具备Wait、Pulse、PulseAll的功能,所以我们不能用Mutex来实现类似的唤醒功能。但是mutex有一个很大的特点,因为Mutex是跨进程的,所以我们可以在同一台机器甚至远程机器的多个进程上使用同一个Mutex。虽然Mutex也可以实现进程内线程同步,而且功能更强大,但是在这种情况下,建议使用Monitor,因为Mutex类是win32封装的,所以需要的互操作性转换比较耗费资源。
不及物动词读写锁
在考虑资源访问时,我们会在惯性中实现对资源的锁定机制,但在某些情况下,我们只需要读取资源的数据,而不需要修改资源的数据。在这种情况下,获得资源的独占权无疑会影响运营效率。因此,Net提供了一种机制。使用ReaderWriterLock访问资源时,如果资源在某个时刻没有获得写的独占权限,那么就可以获得多次读的访问权限和单次写的独占权限。如果在某个时间获得了写入的独占权限,则其他读取的访问权限必须等待。请参考以下代码:
复制代码
私有静态reader writerlock m _ reader writerlock=new reader writerlock();
私有静态int m _ int=0;
[STAThread]
静态void Main(字符串[]参数)
{
线程读取线程=新线程(新线程启动(读取));
readThread。名称=' ReadThread1
线程读取线程2=新线程(新线程启动(读取));
readThread2。名称=' ReadThread2
Thread writeThread=new Thread(new Thread start(Writer));
writeThread。Name=' WriterThread
readThread。start();
readThread2。start();
writeThread。start();
readThread。join();
readThread2。join();
writeThread。join();
控制台。ReadLine();
}
私有静态无效读取()
{
while (true)
{
控制台。写线('线程名'线程。current thread . Name ' acquidateaderlock ');
m_readerWriterLock。收购方锁定(10000);
控制台。写线(字符串。格式(' ThreadName : { 0 } m _ int : { 1 } ',线程。CurrentThread.Name,m _ int));
m_readerWriterLock。ReleaseReaderLock();
}
}
私有静态void Writer()
{
while (true)
{
控制台。写线('线程名'线程。current thread . Name ' acquire writerlock ');
m_readerWriterLock。acquire writerlock(1000);
互锁。增量(ref m _ int);
线程。睡眠(5000);
m_readerWriterLock。release writerlock();
控制台。写线('线程名'线程。current thread . Name ' release writerlock ');
}
}
复制代码
在程序中,我们启动两个线程获取m_int的读访问权限,用一个线程获取m_int的写独占权限。执行代码后,输出如下:
可以看出,WriterThread获得写独占权限后,任何其他读线程都必须等到WriterThread释放写独占权限后才能获得数据访问权限。需要注意的是,上面的打印信息清楚的显示了多个线程可以同时获得数据读取权限,从ReadThread1和ReadThread2的信息交互输出可以看出。
七、同步属性
当我们确定一个类的实例在同一时间只能被一个线程访问时,我们可以直接将该类标识为同步的,这样CLR就会自动为该类实现同步机制。其实这涉及到同步域的概念。当类按如下方式设计时,我们可以确保该类的实例不能被多个线程同时访问。
1).添加系统。类声明中的runtime . remoting . contexts . synchronizationattribute属性。
2).继承到系统。ContextBoundObject
需要注意的是,要实现上述机制,类必须从System继承。换句话说,类必须是上下文绑定的。
下面是一个示例类代码:
【系统。运行时。远程处理。上下文。同步]
公共类同步类:系统。ContextBoundObject
{
}
八、MethodImplAttribute
如果关键部分跨越整个方法,即需要锁定整个方法内部的代码,那么使用MethodImplAttribute属性更容易。这样就不需要在方法内部加锁,只需要添加[methodimpl (MethodImplOptions。同步)]。mehtodimpl和methodimploptions都在名称空间系统中。runtime.compilerservices.但是,请注意,该属性将锁定整个方法,直到该方法返回。所以使用起来不灵活。如果您想提前释放锁,您应该使用监视器或锁。让我们看一个例子:
查看代码
这里我们有两个方法,我们可以比较一下,一个是DoSomeWorkSync()带属性MethodImpl,一个是DoSomeWorkNoSync()不带。方法中的Sleep(1000)是为了让第二个线程有足够的时间进入,而个线程仍然在方法中。每个方法有两个线程。我们先来看看结果:
可以看到,对于线程1和2,也就是调用方法的线程没有属性,线程2进入方法的时候还没有离开,但是线程1进来了,也就是说方法没有同步。我们再来看看线程3和4。当线程3进入时,方法被锁定,直到线程3释放锁定,线程4才进入。
九、同步事件和等待句柄
Lock和Monitor在线程同步中可以起到很好的作用,但是不能在线程之间传递事件。如果您想要同步线程并相互交互,您需要使用同步事件。同步事件是具有两种状态(已终止和未终止)的对象,可用于激活和挂起线程。
有两种同步事件:自动设置事件和手动重置事件。它们之间的区别是线程被激活后状态是否自动从终止变为不终止。自动设置事件自动变为不终止,这意味着自动设置事件只能激活一个线程。手动重置事件在调用其重置方法之前不会变为非终止状态。在此之前,手动重置事件可以激活任意数量的线程。
您可以调用WaitOne、WaitAny或WaitAll来使线程等待事件。他们之间的差异可以在MSDN找到。当事件的Set方法被调用时,事件将被终止,等待的线程将被唤醒。
我们来看一个例子。这个例子发生在MSDN。因为事件仅用于激活一个线程,所以您可以使用自动设置事件或手动设置事件类。
查看代码
我们先来看看输出:
在主函数中,首先创建一个自动设置事件的实例。参数false表示初始状态未终止,如果为true,则初始状态终止。然后创建并启动一个子线程,在子线程中,通过调用AutoResetEvent的WaitOne方法,使子线程等待指定的事件。然后等待一秒钟后,主线程调用AutoResetEvent的set方法,将状态从未终止改为终止,重新激活子线程。