System.Threading.Timer এর জন্য কি কোনও কার্য ভিত্তিক প্রতিস্থাপন রয়েছে?


91

আমি নেট নেট's.০ এর টাস্কগুলিতে নতুন এবং আমি টাইমারকে টাস্ক ভিত্তিক প্রতিস্থাপন বা বাস্তবায়ন যেমন উদাহরণস্বরূপ একটি পর্যায়ক্রমিক টাস্ক হিসাবে বিবেচনা করব তা সন্ধান করতে পারিনি। সেখানে কি এমন জিনিস আছে?

আপডেট আমি আমার প্রয়োজনীয়তার সমাধান হিসাবে যা নিয়েছি তা নিয়ে এসেছি যা শিশুদের সাথে একটি টাস্কের ভিতরে "টাইমার" কার্যকারিতাটি মোড়ানো হয় যা বাতিলকরণ টোকেনের সমস্ত সুবিধা গ্রহণ করে এবং আরও টাস্ক পদক্ষেপগুলিতে অংশ নিতে সক্ষম হয়ে টাস্কটি ফিরিয়ে দেয়।

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      

7
থ্রেড.স্লিপ মেকানিজম ব্যবহার না করে আপনার টাস্কের ভিতরে একটি টাইমার ব্যবহার করা উচিত। এটি আরও দক্ষ
ইয়োন বি 12 ই

উত্তর:


87

এটি 4.5 এর উপর নির্ভর করে তবে এটি কাজ করে।

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

অবশ্যই আপনি একটি জেনেরিক সংস্করণ যুক্ত করতে পারেন যা তর্কগুলিও গ্রহণ করে। এটি প্রকৃতপক্ষে অন্যান্য প্রস্তাবিত পদ্ধতির সাথে সমান যাহেতু টুথ টাস্কের অধীনে eডেলা টাস্কের সমাপ্তির উত্স হিসাবে টাইমার সমাপ্তি ব্যবহার করছে।


4
আমি এই মুহুর্তে এই পদ্ধতির দিকে পরিবর্তন করেছি। তবে আমি শর্তাধীন action()একটি পুনরাবৃত্তি সঙ্গে কল !cancelToken.IsCancellationRequested। এটা ভাল, তাই না?
শুভনোমাদ

4
এর জন্য ধন্যবাদ - আমরা একই ব্যবহার করছি তবে ক্রিয়া না হওয়া অবধি বিলম্ব সরিয়ে
মাইকেল পার্কার

4
এর জন্য ধন্যবাদ. তবে এই কোডটি "প্রতি X ঘন্টা" চলবে না এটি "প্রতি X ঘন্টা + actionকার্যকর হওয়ার সময়" চালাবে আমি ঠিক আছি?
অ্যালেক্স

সঠিক। যদি আপনি মৃত্যুদন্ড কার্যকর করার সময়টির জন্য অ্যাকাউন্ট করতে চান তবে আপনার কিছু গণিতের প্রয়োজন হবে। তবে মৃত্যুদন্ড কার্যকর করার সময় যদি আপনার সময়কাল, ইত্যাদি ছাড়িয়ে যায় তবে তা জটিল হয়ে
জেফ

57

আপডেট আমি নীচের উত্তরটিকে "উত্তর" হিসাবে চিহ্নিত করছি কারণ এটি এখন যথেষ্ট পুরানো যেহেতু আমাদের async / প্রতীক্ষিত প্যাটার্নটি ব্যবহার করা উচিত। এটিকে আর ডাউনভোট করার দরকার নেই। হাঃ হাঃ হাঃ


অ্যামির উত্তর হিসাবে, এখানে কোনও টাস্ক ভিত্তিক সাময়িকী / টাইমার বাস্তবায়ন নেই। তবে, আমার আসল আপডেটের ভিত্তিতে, আমরা এটিকে বেশ কার্যকর এবং প্রযোজনার পরীক্ষায় পরিণত করেছি। ভেবেছিলাম আমি ভাগ করে নেব:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

আউটপুট:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .

4
এটি দুর্দান্ত কোডের মতো দেখায় তবে আমি ভাবছি যে এখন এটির প্রয়োজন / এসিড / কীওয়ার্ড কীওয়ার্ডগুলি দরকার। আপনার পদ্ধতির সাথে এখানে কীভাবে তুলনা করা যায়: স্ট্যাকওভারফ্লো . com / a / 14297203 / 122781 ?
শুভনোমাদ

4
@ হ্যাপি নোমাদ, দেখে মনে হচ্ছে পিরিওডিকটাস্কফ্যাক্টরি ক্লাসটি অ্যাপ্লিকেশনকে লক্ষ্য করে অ্যাসিঙ্ক / অপেক্ষা করতে পারে এছাড়াও, পিরিওডিকটাস্কফ্যাক্টরি কিছু অতিরিক্ত "টাইমার" সমাপ্তির প্রক্রিয়া সরবরাহ করে যেমন সর্বোচ্চ সংখ্যক পুনরাবৃত্তি এবং সর্বাধিক সময়কাল পাশাপাশি প্রতিটি পুনরাবৃত্তি শেষ পুনরাবৃত্তির জন্য অপেক্ষা করতে পারে তা নিশ্চিত করার একটি উপায় সরবরাহ করে। তবে আমি এ্যাসএনসি ব্যবহার করতে / অপেক্ষার জন্য এটিটি রূপান্তর করতে দেখছি যখন আমরা .Net 4.5
জিম

4
+1 আমি এখন আপনার ক্লাস ব্যবহার করছি, ধন্যবাদ। এটি ইউআই থ্রেডের সাথে সুন্দরভাবে খেলতে পেতে, যদিও TaskScheduler.FromCurrentSynchronizationContext()সেট করার আগে আমাকে কল করতে হবে mainAction। আমি MainPeriodicTaskActionএর subTaskসাথে এটি তৈরি করার জন্য ফলাফলের শিডিয়ুলারটি পাস করি ।
শুভনোমাদ

4
আমি নিশ্চিত না, কোনও থ্রেডকে ব্লক করা ভাল ধারণা, যখন এটি দরকারী কাজ করতে পারে। "থ্রেড.স্লিপ (বিলম্বআইনিমিলিসেকেন্ডস)", "পিরিয়ডসেটসেন্ট.ওয়েট (ইন্টারভালআইনমিলিসেকেন্ডস, বাতিল টোকেন)" ... তারপরে আপনি একটি টাইমার ব্যবহার করেন, আপনি হার্ডওয়ারে অপেক্ষা করেন, সুতরাং কোনও থ্রেড ব্যয় হয় না। তবে আপনার সমাধানে থ্রেডগুলি ব্যয় করার জন্য ব্যয় করা হয়।
রোলিংস্টোন

4
@ রোলিংস্টোন আমি সম্মত আমি মনে করি এই সমাধানটি মূলত async- জাতীয় আচরণের উদ্দেশ্যকে পরাস্ত করে। টাইমার ব্যবহার করা এবং থ্রেড নষ্ট না করা আরও ভাল। এটি কেবল কোনও সুবিধা ছাড়াই অ্যাসিঙ্কের উপস্থিতি দিচ্ছে।
জেফ

12

প্রতিক্রিয়াশীল এক্সটেনশন লাইব্রেরি থেকে এটি হুবহু নয় System.Threading.Tasks, তবে Observable.Timer(বা সহজ Observable.Interval) সম্ভবত আপনি যা খুঁজছেন তা।


4
যেমন পর্যবেক্ষণযোগ্য.ইনটারভাল (টাইমস্প্যান.ফ্রমসেকেন্ডস (1))। সাবস্ক্রাইব করুন (v => ডিবাগ.ওরাইটলাইন (ভি));
মার্টিন ক্যাপোডিসি

4
ভাল, কিন্তু যারা প্রতিক্রিয়াশীল কনস্ট্রাক্টস কি ছাঁটাই যায়?
Shmil The Cat

9

এখনও অবধি আমি থ্রেডিং টাইমারের পরিবর্তে চক্রীয় সিপিইউ বাউন্ড ব্যাকগ্রাউন্ড কাজের জন্য একটি লংআরনিং টিপিএল টাস্ক ব্যবহার করেছি, কারণ:

  • টিপিএল টাস্ক বাতিলকে সমর্থন করে
  • প্রোগ্রামিং বন্ধ হয়ে যাওয়ার সময় থ্রেডিং টাইমার আরেকটি থ্রেড শুরু করতে পারে যখন নিষ্পত্তিযোগ্য সংস্থানগুলির সাথে সম্ভাব্য সমস্যা তৈরি করে
  • ওভাররুনের সুযোগ: থ্রেডিং টাইমারটি অন্য থ্রেড শুরু করতে পারে যখন অপ্রত্যাশিত দীর্ঘ কাজের কারণে পূর্ববর্তীটি এখনও প্রক্রিয়া করা হয় (আমি জানি, এটি টাইমারটি থামিয়ে এবং পুনরায় চালু করে প্রতিরোধ করা যেতে পারে)

যাইহোক, টিপিএল সমাধান সর্বদা একটি উত্সর্গীকৃত থ্রেডের দাবি করে যা পরবর্তী ক্রিয়াটির (যা বেশিরভাগ সময়) অপেক্ষা করার সময় প্রয়োজন হয় না। আমি জেফের প্রস্তাবিত সমাধানটি ব্যাকগ্রাউন্ডে সিপিইউ বাউন্ড সাইক্লিক কাজ সম্পাদন করতে ব্যবহার করতে চাই কারণ যখন কেবল এমন কাজ করার সময় থ্রেডপুলের থ্রেডের প্রয়োজন হয় যা স্কেলিবিলিটির জন্য ভাল (বিশেষত যখন অন্তরকাল বড় হয়)।

এটি অর্জনের জন্য, আমি 4 টি অভিযোজন প্রস্তাব করব:

  1. যোগ ConfigureAwait(false)করার জন্য Task.Delay()চালানো doWork, একটি থ্রেড পুল থ্রেডে কর্ম অন্যথায় doWorkযা উপমা ধারণা নয় কলিং থ্রেডে সঞ্চালন করা হবে
  2. একটি কার্যক্যান্সেলড এক্সসেপশন (এখনও প্রয়োজনীয়?) ছুঁড়ে দিয়ে বাতিল প্যাটার্নটিতে লেগে থাকুন
  3. doWorkকাজটি বাতিল করতে সক্ষম করার জন্য বাতিলকরণ টোকেনকে ফরোয়ার্ড করুন
  4. টাস্কের স্টেটের তথ্য সরবরাহ করার জন্য টাইপ অবজেক্টের একটি প্যারামিটার যুক্ত করুন (একটি টিপিএল টাস্কের মতো)

পয়েন্ট 2 সম্পর্কে আমি নিশ্চিত নই, async অপেক্ষা করার জন্য এখনও টাস্ক ক্যান্সেলড এক্সেকশন প্রয়োজন বা এটি কেবল সেরা অনুশীলন?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

প্রস্তাবিত সমাধানটিতে আপনার মন্তব্য দিন ...

আপডেট 2016-8-30

উপরের সমাধানটি তাত্ক্ষণিকভাবে কল করে না doWork()তবে await Task.Delay().ConfigureAwait(false)থ্রেড সুইচটির জন্য শুরু করে doWork()। নীচের সমাধানটি প্রথম doWork()কলটি একটিতে মোড়ক করে তার জন্য Task.Run()অপেক্ষা করে এই সমস্যাটি কাটিয়ে উঠেছে ।

নীচে উন্নত অ্যাসিঙ্ক replacement প্রতিস্থাপনের জন্য অপেক্ষা করা হয়েছে Threading.Timerযার জন্য বাতিলযোগ্য চক্রীয় কাজ সম্পাদন করে এবং স্কেলযোগ্য (টিপিএল সমাধানের সাথে তুলনা করে) কারণ পরবর্তী পদক্ষেপের জন্য অপেক্ষা করার সময় এটি কোনও থ্রেড দখল করে না।

নোট করুন যে টাইমার বিপরীতে, অপেক্ষার সময় ( period) স্থির এবং চক্র সময় নয়; চক্রের সময়টি অপেক্ষার সময়ের যোগফল এবং তার সময়কাল doWork()পরিবর্তিত হতে পারে।

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

ব্যবহার ConfigureAwait(false)থ্রেড পুলে পদ্ধতির ধারাবাহিকতা নির্ধারণ করে, সুতরাং এটি থ্রেডিং টাইমার দ্বিতীয় পয়েন্টটি সত্যই সমাধান করে না solve আমিও taskStateপ্রয়োজনীয় মনে করি না ; ল্যাম্বদা ভেরিয়েবল ক্যাপচারটি আরও নমনীয় এবং টাইপ-নিরাপদ।
স্টিফেন ক্লিয়ারি

4
আমি সত্যিই যা করতে চাই তা হল এক্সচেঞ্জ করা await Task.Delay()এবং doWork()তাই doWork()সাথে সাথে স্টার্টআপের সময় কার্যকর করা হবে। তবে কোনও কৌশল ছাড়াই doWork()প্রথমবার কলিং থ্রেডে চালিত হয়ে তা ব্লক করে দেওয়া হত। স্টিফেন, আপনার কি সেই সমস্যার সমাধান আছে?
এরিক স্ট্রোকেন

4
সবচেয়ে সহজ উপায় হ'ল কেবল একটিতে পুরো জিনিসটি মোড়ানো Task.Run
স্টিফেন ক্লিয়ারি

হ্যাঁ, তবে তারপরে আমি এখনই টিপিএল সলিউশনটিতে ফিরে যেতে পারি যা লুপটি চলমান থাকবে এমন থ্রেড দাবি করে এবং এই সমাধানটি তখন কম স্কেলেবল হয়।
এরিক স্ট্রোকেন

1

আমার একটি সিঙ্ক্রোনাস পদ্ধতি থেকে পুনরাবৃত্ত অ্যাসিনক্রোনাস কাজগুলি ট্রিগার করা দরকার।

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

এটি জেফের উত্তরের একটি অভিযোজন। এটি গ্রহণের জন্য এটি পরিবর্তন করা হয় Func<Task> এটি এটিও নিশ্চিত করে যে পরবর্তী দেরির জন্য সময়কাল থেকে টাস্কের রান সময় কেটে নিয়ে কত সময় চালানো হয়।

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}

0

আমি একই ধরণের সমস্যায় পড়েছি এবং একটি TaskTimerক্লাস লিখেছি যা টাইমার অনুসারে সম্পূর্ণ হওয়া কাজগুলির একটি সিরিজ প্রদান করে: https://github.com/ikriv/tasktimer/

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}

-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

সরল ...

আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.